使用 Flutter 和 Metamask 构建
对于软件开发人员和技术爱好者来说,开发应用程序是最酷的事情之一。应用程序不仅更便携、更易于使用,而且在某些情况下,它们还是特定应用的唯一选择。在本教程和后续系列中,我们将学习如何使用Flutter和Metamask构建一个炫酷的跨平台移动应用程序。
我们正在解决的问题
要与区块链交互,用户必须在区块链上拥有一个账户(公钥和私钥对),用于签署交易。与他人共享私钥相当于共享账户访问权限。因此,用户不愿将私钥提供给应用程序,因为这会引发诸多安全隐患。业界认可的方法是使用像Metamask这样的“非托管钱包” 。
虽然网上有很多关于如何使用 Metamask 浏览器扩展的教程,但移动应用版本的使用方法目前还没有完善的文档。在本系列教程中,我们将介绍如何将 Flutter 应用与 Metamask 连接以实现用户登录,后续文章还将介绍如何与智能合约交互。我们选择Flutter作为开发框架,是因为我们希望构建一个同时支持 Android 和 iOS 的跨平台应用。
在本教程中,我们将构建一个使用 MetaMask 进行登录的应用程序。它将从 MetaMask 获取公钥。Metamask 也可用于签署消息和交易,但这将在另一个教程中介绍。以下 GIF 展示了我们将要构建的内容:
先决条件
- 您的系统中已安装 Flutter。您可以点击此处查看 Flutter 官方指南。
- 请准备一台安卓/iOS模拟器或真机,用于测试应用程序。我建议使用安卓模拟器,因为iOS系统存在一些bug。
- 在您的安卓模拟器中安装并设置Metamask 移动应用。
已知挑战
在撰写本文的过程中,我们在 iOS 上构建和运行应用程序时遇到了一些挑战。
- iOS 不支持从 App Store 安装第三方应用程序到模拟器。因此,我们必须直接从其Github 代码库安装 Metamask 。
- 如果您使用的是搭载 Apple Silicon 芯片的 MacBook,则需要在模拟器中设置 Metamask 时执行一些额外的步骤。您可以点击此处了解详情。
- MetaMask 的深度链接功能无法正常工作。(本教程建议使用 Android 模拟器)
如果我找到上述问题的解决方案,我会更新这篇文章。在此之前,非常期待您的宝贵意见。
项目启动
我们首先运行命令flutter doctor,确保开发流程的一切设置都已就绪。您应该会看到类似以下内容:
现在我们首先创建一个新的 Flutter 项目。为此,请在终端中打开项目文件夹所在的目录,然后输入以下命令:
flutter create my_app
这my_app是我们要构建的项目的名称。这将创建一个同名文件夹,所有代码都将存放于此。您应该会看到类似如下的输出:
请使用您选择的代码编辑器打开此文件夹。您可以通过输入命令运行您的应用程序,flutter run或者如果您使用的是 VS Code,则可以使用Flutter 调试器。
安装依赖项
本项目需要以下依赖项:
- url_launcher:这将用于
Metamask通过 URI 从我们的应用程序打开。 - walletconnect_dart:这将用于生成一个 URI,该 URI 将用于启动 Metamask。
- google_fonts:用于在我们的应用程序中使用 Google Fonts 的可选依赖项。
- slider_button:用于登录的滑动按钮的可选依赖项。
要安装这些依赖项,请键入以下命令
flutter pub add url_launcher walletconnect_dart google_fonts slider_button
添加资源文件夹
我们希望在应用程序的用户界面中使用静态图片。为此,我们需要创建一个文件夹来存放我们的资源,并告诉 Flutter 将它们用作我们项目的资源。
assets在根文件夹内创建一个名为 `<path>` 的文件夹my_app。根文件夹的名称将与您创建 Flutter 项目时使用的名称相同。在该assets文件夹内,我们将创建一个images文件夹来存储图像资源。最后,在` pubspec.yaml<file>` 文件中,我们通过在 `<section>` 部分添加以下代码来添加此文件夹flutter:
flutter:
# The following line ensures that the Material Icons font is
# included with your application, so that you can use the icons in
# the material Icons class.
uses-material-design: true
# To add assets to your application, add an assets section, like this:
assets:
- assets/images/
了解流程
最后,在开始编写应用程序代码之前,了解用户流程至关重要。下图展示了用户从打开应用程序开始的整个流程:
代码跟随
我们将从文件夹main.dart内部开始lib。这main.dart是 Flutter 编译和执行的第一个文件。请清空此文件的所有内容,然后粘贴以下代码:
import 'package:flutter/material.dart';
void main(List<String> args) {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp();
}
}
我们首先创建一个新的无状态组件(Stateless Widget)。该组件将作为我们项目的起点。根据上面的流程图,第一步是创建登录页面。虽然本教程中的应用只有一个页面,但我们应该有一个完善的路由系统,以便在项目开发过程中轻松地添加新页面。
创建路线
Flutter 中的路由机制与 Web 应用的路由机制非常相似,都使用 ` /path<path>` 格式。简单来说,路由就是将路径映射到对应的 widget。路由的工作原理示例如下:
return MaterialApp(
initialRoute: "/login",
routes: {
"/login": (context) => const LoginPage(),
"/home": (context) => const HomePage()
},
);
在这里,routes我们定义了项目中将使用的所有路由及其对应的组件。在这个例子中,我们指定LoginPage当用户位于路由名称(router)时渲染组件/login,同样地,当用户位于/home路由名称(route)时HomePage渲染组件。该initialRoute字段指定要加载的初始路由。在这个例子中,用户打开应用后看到的第一个组件就是该LoginPage组件。
由于项目中会有多个路由,并且这些路由会在多个文件中使用,因此直接定义路由是不明智的。为了提高代码的健壮性,应该定义常量变量名。为此,在utils项目根目录下创建一个名为 `routes` 的新文件夹lib,并在该文件utils夹内创建一个名为 `routes.txt` 的新文件routes.dart。该文件将用于存储所有路由。在该文件中,按如下方式定义路由:
class MyRoutes {
static String loginRoute = '/login';
}
现在让我们回到main.dart文件,进行以下更改:
import 'package:flutter/material.dart';
import 'package:my_app/utils/routes.dart';
void main(List<String> args) {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
initialRoute: MyRoutes.loginRoute,
routes: {
MyRoutes.loginRoute: (context) => const LoginPage(),
},
);
}
}
这里我们导入新创建的routes.dart文件,并使用变量名而不是直接输入路由。由于我们还没有组件LoginPage,所以会收到错误信息。那么,让我们创建登录页面吧。
创建登录页面
在文件夹内lib,我们创建一个名为 `<pages_name>` 的新文件夹pages,用于存放所有页面。在该文件夹内pages,创建一个名为 `<pages_name>` 的新文件login_page.dart。将以下代码粘贴到该文件中:
import 'package:flutter/material.dart';
class LoginPage extends StatefulWidget {
const LoginPage({Key? key}) : super(key: key);
@override
State<LoginPage> createState() => _LoginPageState();
}
class _LoginPageState extends State<LoginPage> {
@override
Widget build(BuildContext context) {
return Scaffold();
}
}
这里我们创建了一个名为 `StatefulWidget` 的新有状态组件LoginPage。现在我们可以main.dart通过import 'package:my_app/pages/login_page.dart';在文件开头添加 `import <StatefulWidget>` 将其导入到我们的文件中。最终的 main.dart 文件如下所示:
import 'package:flutter/material.dart';
import 'package:my_app/utils/routes.dart';
import 'package:my_app/pages/login_page.dart';
void main(List<String> args) {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
initialRoute: MyRoutes.loginRoute,
routes: {
MyRoutes.loginRoute: (context) => const LoginPage(),
},
);
}
}
设计登录页面
现在是时候设计我们的登录页面了。在本教程中,我们将设计一个非常简单的登录页面。首先,它只有一个图片和一个Connect with Metamask按钮。当 MetaMask 连接成功后,页面会显示账户地址以及与之连接的区块链。如果连接的区块链不是官方支持的区块链(在本例中为 Mumbai Testnet),我们会显示一条警告信息,提示用户连接到正确的区块链。最后,如果用户已连接到正确的网络,我们会显示登录详情以及一个“滑动登录”的滑块。这三个部分分别在以下图中展示:
构建默认登录页面
我们首先编辑该login_page.dart文件。在类中进行以下更改_LoginPageState:
class _LoginPageState extends State<LoginPage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Login Page'),
),
body: SingleChildScrollView(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Image.asset(
'assets/images/main_page_image.png',
fit: BoxFit.fitHeight,
),
ElevatedButton(
onPressed: () => {}, child: const Text("Connect with Metamask"))
],
),
),
);
}
}
接下来我们将进行以下操作:
- 我们首先返回一个
Scaffold.Scaffold在 Flutter 中,它用于实现基本的 Material Design 布局。您可以在这里阅读更多相关信息。 - 然后,我们定义一个
AppBar名为“登录页面”的元素。这将是显示在应用程序顶部的标题。 - 我们首先定义一个组件来构建应用程序的主体
SingleChildScrollView。这在应用程序于屏幕较小的手机上运行时非常有用。它允许用户滚动浏览组件。点击此处了解更多信息。 - 在内部,
SingleChildScrollView我们定义一个元素Column来包含页面的各种组件children。 - 我们定义的第一个子对象是图像。我们想要渲染存储在
assets文件夹中的图像。为此,我们使用Image.asset()并传入图像的存储路径。请记住使用已添加为资源源的路径。之前我们已将assets/images/作为资源源添加。我使用的是下载到文件夹中并命名为的图像。imagesmain_page_image.png - 接下来,我们使用该类创建一个按钮
ElevatedButton。它接受两个参数:onPressed:按钮点击时要执行的函数。目前此处为空。child:一个子控件,它将决定按钮的外观。目前,它是一个Text带有字符串的控件“Connect with Metamask”。
现在运行该应用,您应该会看到类似这样的内容:
虽然现在按下按钮不会有任何反应,但我们已经准备好了默认外观。接下来只会更有趣 😎😎😎。
了解依赖关系
接下来,我们将编写“连接 Metamask”按钮背后的逻辑。为此,我们将使用两个重要的依赖项:
-
walletconnect_dart此依赖项将用于连接 Metamask。实际上,它也可以与其他钱包(例如 Trust Wallet)一起使用,但本教程将仅关注 Metamask。要理解其工作原理,我们首先必须了解Wallet Connect 的工作原理。Wallet Connect 是一种常用的协议,用于将 Web 应用与移动钱包连接起来(通常通过扫描二维码)。它会生成一个 URI,移动应用可以使用该 URI 通过远程连接安全地签署交易。我们的应用的工作方式是,使用下一个依赖项直接在 Metamask 中打开该 URI。
walletconnect_dart这是一个用编程语言编写的 Flutter 包dart。我们将使用此依赖项来生成 URI 并连接到 Metamask。此包还提供了回调函数,可用于监听 Metamask 中的任何更改,例如连接的网络发生变化。 -
url_launcher此依赖项用于在 Android 和 iOS 中启动 URL。我们将使用此依赖项来启动walletconnect_dartMetamask 应用生成的 URI。
在我们的代码中使用依赖项
我们首先在login_page.dart文件中导入依赖项。
import 'package:walletconnect_dart/walletconnect_dart.dart';
import 'package:url_launcher/url_launcher_string.dart';
接下来,在我们的_LoginPageState类中,我们定义一个连接器,该连接器将用于连接 Metamask。
var connector = WalletConnect(
bridge: 'https://bridge.walletconnect.org',
clientMeta: const PeerMeta(
name: 'My App',
description: 'An app for converting pictures to NFT',
url: 'https://walletconnect.org',
icons: [
'https://files.gitbook.com/v0/b/gitbook-legacy-files/o/spaces%2F-LJJeCjcLrr53DcT1Ml7%2Favatar.png?alt=media'
]));
我们使用WalletConnect类来定义连接器。它接受以下参数:
bridge:链接到 Wallet Connect 桥clientMeta这包含有关客户端的可选元数据。name应用程序名称description应用程序简介url网站的网址icon:要在 Metamask 连接弹出窗口中显示的图标
我们还定义了两个变量,分别称为_session和_uri,当我们的小部件状态更新时,它们将分别用于存储会话和 URI。
我们定义了一个名为 `login` 的函数loginUsingMetamask来处理登录过程,如下所示:
loginUsingMetamask(BuildContext context) async {
if (!connector.connected) {
try {
var session = await connector.createSession(onDisplayUri: (uri) async {
_uri = uri;
await launchUrlString(uri, mode: LaunchMode.externalApplication);
});
print(session.accounts[0]);
print(session.chainId);
setState(() {
_session = session;
});
} catch (exp) {
print(exp);
}
}
}
接下来我们将进行以下操作:
- 首先,我们通过检查变量的值来判断连接是否已建立
connector.connected。如果连接尚未建立,则继续执行代码if块内的代码。 - 我们使用
try-catch代码块来捕获建立连接过程中可能出现的任何异常,例如用户cancel在 Metamask 弹出窗口中点击。 - 在这个代码块中
try,我们使用一个connector.createSession()函数创建一个新的会话。该函数接收一个函数作为参数,该函数会在 URI 生成时执行。在这个函数内部,我们使用另一个launchUrlString()函数在外部应用程序中打开生成的 URI。我们将生成的 URI 作为参数传递,并且由于它将打开一个外部应用程序,因此我们将 `modeis` 设置为 `LaunchMode.externalApplication.`。最后,由于我们希望代码等待 Metamask 确认连接,因此我们await在launchUrlString()函数中使用了 `wait` 关键字。 - 我们可以使用 `get_accounts_connected_counts` 获取已连接的账户,
session.accounts并使用 `get_chain_id` 获取链 IDsession.chainId。目前,我们将选定的账户session.accounts[0]和链 ID 打印到控制台,以检查代码是否正常工作。 - 最后,我们使用更新应用程序状态的方法
setState,并将创建的会话存储在_session变量中。 - 如果上述任何语句中产生异常,则该
catch代码块将被执行。目前我们仅打印产生的异常,但在项目后期,我们可以使用更完善的异常处理机制。
最后,我们将该loginUsingMetamask函数作为onPressed参数传递给我们创建的按钮。最终代码如下所示:
import 'package:flutter/material.dart';
import 'package:walletconnect_dart/walletconnect_dart.dart';
import 'package:url_launcher/url_launcher_string.dart';
class LoginPage extends StatefulWidget {
const LoginPage({Key? key}) : super(key: key);
@override
State<LoginPage> createState() => _LoginPageState();
}
class _LoginPageState extends State<LoginPage> {
var connector = WalletConnect(
bridge: 'https://bridge.walletconnect.org',
clientMeta: const PeerMeta(
name: 'My App',
description: 'An app for converting pictures to NFT',
url: 'https://walletconnect.org',
icons: [
'https://files.gitbook.com/v0/b/gitbook-legacy-files/o/spaces%2F-LJJeCjcLrr53DcT1Ml7%2Favatar.png?alt=media'
]));
var _session, _uri;
loginUsingMetamask(BuildContext context) async {
if (!connector.connected) {
try {
var session = await connector.createSession(onDisplayUri: (uri) async {
_uri = uri;
await launchUrlString(uri, mode: LaunchMode.externalApplication);
});
setState(() {
_session = session;
});
} catch (exp) {
print(exp);
}
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Login Page'),
),
body: SingleChildScrollView(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Image.asset(
'assets/images/main_page_image.png',
fit: BoxFit.fitHeight,
),
ElevatedButton(
onPressed: () => loginUsingMetamask(context),
child: const Text("Connect with Metamask"))
],
),
),
);
}
}
现在,我们运行应用程序🤞🏾。如果一切操作正确,您将看到熟悉的登录页面。但是,点击按钮后Connect with Metamask,页面会跳转到 MetaMask。Metamask 会提示您连接钱包,并显示您在相应clientMeta字段中指定的 URL 和图标。
点击蓝色Connect按钮后,我们会跳转回钱包。现在可能看不到任何变化,但如果查看日志,应该可以看到 Flutter 打印出了账户地址和链 ID。
恭喜🥳🎉!您已成功连接 Metamask 钱包,就是这么简单。
还有一个挑战尚未解决。用户可能无法连接到部署智能合约的区块链。因此,在允许用户访问我们的平台之前,我们应该检查是否连接到了正确的区块链。此外,如果用户更改了连接的网络或选择的账户,我们也应该进行相应的更新。
订阅活动
connector我们可以使用变量connect订阅session_update事件disconnect。将以下代码粘贴到build函数中:
Widget build(BuildContext context) {
connector.on(
'connect',
(session) => setState(
() {
_session = _session;
},
));
connector.on(
'session_update',
(payload) => setState(() {
_session = payload;
print(payload.accounts[0]);
print(payload.chainId);
}));
connector.on(
'disconnect',
(payload) => setState(() {
_session = null;
}));
...
}
在这里,我们订阅了不同的事件。在事件触发时,session_update我们会更新应用程序的状态setState,并将更新后的有效负载赋值给_session变量。我们还会打印新的账户地址和链 ID,以便从终端检查代码是否正常运行。
对应用进行热重载,并按照相同的步骤将 MetaMask 连接到您的应用。现在,您可以在 MetaMask 中更改网络和已连接的账户,并在终端/控制台中观察链 ID 和账户地址的变化。
在屏幕上显示数据
我们已成功将 MetaMask 与我们的应用程序连接。虽然教程到此即可结束,但我还是希望将详细信息显示在屏幕上,以便用户进行验证,从而提供更好的登录体验。
我们首先希望用户登录 Metamask 后,显示详细信息而不是按钮。为此,我们将代码封装ElevatedButton在三元运算符中,如下所示:
(_session != null) ? Container() : ElevatedButton()
这里,如果_session变量为 0 null,即 Metamask 未连接,则会渲染 ,ElevatedButton否则Container将渲染 。
我们首先在代码中放入以下代码Container:
Container(
padding: const EdgeInsets.fromLTRB(20, 0, 20, 0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Account',
style: GoogleFonts.merriweather(
fontWeight: FontWeight.bold, fontSize: 16),
),
Text(
'${_session.accounts[0]}',
style: GoogleFonts.inconsolata(fontSize: 16),
),
]
)
)
20px我们首先在左右两侧添加少量内边距。- 我们希望从一开始就进行跨轴对齐,因此我们将其定义
crossAxisAlignment为CrossAxisAlignment.start。 -
我们希望列中的第一个组件显示一句简单的格言
Account,其下方显示已连接的 MetaMask 帐户的帐户地址。我们使用该Text组件来显示数据并设置GoogleFonts样式。您可以通过编写以下代码导入 Google 字体:import 'package:google_fonts/google_fonts.dart';在文件顶部。我们使用单引号 ( ) 来
${}访问变量。_session’’
接下来我们要展示的是用户所连接的链的名称。我们希望以下列方式显示:
由于大多数用户可能不熟悉不同区块链的链 ID,因此最好向他们显示区块链的名称,而不仅仅是链 ID。为此,我们可以编写一个简单的函数,该函数接收链 IDchainId作为输入并返回链的名称。在函数内部_LoginPageState定义getNetworkName如下函数:
getNetworkName(chainId) {
switch (chainId) {
case 1:
return 'Ethereum Mainnet';
case 3:
return 'Ropsten Testnet';
case 4:
return 'Rinkeby Testnet';
case 5:
return 'Goreli Testnet';
case 42:
return 'Kovan Testnet';
case 137:
return 'Polygon Mainnet';
case 80001:
return 'Mumbai Testnet';
default:
return 'Unknown Chain';
}
}
该函数使用switch-case语句根据条件返回链的名称chainId。
在我们的组件内部Container,在两个Text小部件之后,我们添加一个宽度为 20px 的SizedBox元素height来增加一些间隙。接下来,我们定义一个Row包含两个子组件的元素,分别是文本“Chain”和通过调用函数获得的链的名称getNetworkName。具体做法如下:
Row(
children: [
Text(
'Chain: ',
style: GoogleFonts.merriweather(
fontWeight: FontWeight.bold, fontSize: 16),
),
Text(
getNetworkName(_session.chainId),
style: GoogleFonts.inconsolata(fontSize: 16),
)
],
),
接下来,我们要检查用户是否连接到正确的网络。我们检查它是否与_session.chainId我们支持的区块链的链 ID 匹配(在本例中,孟买测试网的链 ID 为 80001)。如果它不等于所需的链 ID,我们创建一个Row用于显示图标和辅助文本的元素;否则,我们创建一个Container用于我们的元素的元素SliderButton。
(_session.chainId != 80001)
? Row(
children: const [
Icon(Icons.warning,
color: Colors.redAccent, size: 15),
Text('Network not supported. Switch to '),
Text(
'Mumbai Testnet',
style:
TextStyle(fontWeight: FontWeight.bold),
)
],
)
: Container()
接下来,我们添加依赖项SliderButton。我们在文件开头使用以下语句导入依赖项:
import 'package:slider_button/slider_button.dart';
最后,在我们的内部Container,我们SliderButton这样定义我们的:
Container(
alignment: Alignment.center,
child: SliderButton(
action: () async {
// TODO: Navigate to main page
},
label: const Text('Slide to login'),
icon: const Icon(Icons.check),
),
)
目前它还SliderButton没有任何作用,但在后续教程中,它会将我们导航到应用程序的主页。
现在您的应用已完全准备就绪,可以运行了。如果一切都按照本教程中的说明进行,您的应用现在应该已经可以运行了。您应该能够使用 Metamask 登录您的应用。虽然应用本身不会登录任何页面,但您仍然可以使用移动应用连接到 Metamask。是不是很棒?!
总结
哇!这篇教程内容很丰富。在本教程中,我们讲解了如何从零开始构建一个非常基础的 Flutter 应用。我们学习了如何从应用中与 Metamask 进行交互。我们探索了两个重要的依赖项,walletconnect_dart以及url_launcher它们的工作原理和如何利用它们将应用与 Metamask 等钱包连接起来。我们还学习了如何在用户更新 Metamask 会话时更新应用。最后,我希望大家都能从中学习到一些新知识,度过一段愉快的时光。
该项目的代码已上传至Github,链接在此。
我计划将这款应用扩展成一个功能更强大的应用,并深入探索 DeFi、区块链等领域。如果您喜欢这篇教程,请不要忘记点赞并分享到您的社交媒体,或者在讨论区留下您的反馈意见,帮助我改进。如果您想与我联系或推荐任何主题,可以通过LinkedIn、Twitter或邮件联系我。
我们下次再见,届时将推出新的教程或博客。在此之前,请注意安全,多陪伴家人,并继续努力!
文章来源:https://dev.to/bhaskardutta/building-with-flutter-and-metamask-8h5








