发布于 2026-01-06 0 阅读
0

使用 Flutter 和 Metamask 构建

使用 Flutter 和 Metamask 构建

对于软件开发人员和技术爱好者来说,开发应用程序是最酷的事情之一。应用程序不仅更便携、更易于使用,而且在某些情况下,它们还是特定应用的唯一选择。在本教程和后续系列中,我们将学习如何使用FlutterMetamask构建一个炫酷的跨平台移动应用程序。

我们正在解决的问题

要与区块链交互,用户必须在区块链上拥有一个账户(公钥和私钥对),用于签署交易。与他人共享私钥相当于共享账户访问权限。因此,用户不愿将私钥提供给应用程序,因为这会引发诸多安全隐患。业界认可的方法是使用像Metamask这样的“非托管钱包” 。

虽然网上有很多关于如何使用 Metamask 浏览器扩展的教程,但移动应用版本的使用方法目前还没有完善的文档。在本系列教程中,我们将介绍如何将 Flutter 应用与 Metamask 连接以实现用户登录,后续文章还将介绍如何与智能合约交互。我们选择Flutter作为开发框架,是因为我们希望构建一个同时支持 Android 和 iOS 的跨平台应用。

在本教程中,我们将构建一个使用 MetaMask 进行登录的应用程序。它将从 MetaMask 获取公钥。Metamask 也可用于签署消息和交易,但这将在另一个教程中介绍。以下 GIF 展示了我们将要构建的内容:

演示 GIF

先决条件

💡 建议将VS CodeFlutter扩展一起使用。

已知挑战

在撰写本文的过程中,我们在 iOS 上构建和运行应用程序时遇到了一些挑战。

  • iOS 不支持从 App Store 安装第三方应用程序到模拟器。因此,我们必须直接从其Github 代码库安装 Metamask 。
  • 如果您使用的是搭载 Apple Silicon 芯片的 MacBook,则需要在模拟器中设置 Metamask 时执行一些额外的步骤。您可以点击此处了解详情。
  • MetaMask 的深度链接功能无法正常工作。(本教程建议使用 Android 模拟器)

如果我找到上述问题的解决方案,我会更新这篇文章。在此之前,非常期待您的宝贵意见。

项目启动

我们首先运行命令flutter doctor,确保开发流程的一切设置都已就绪。您应该会看到类似以下内容:

运行 Flutter Doctor

现在我们首先创建一个新的 Flutter 项目。为此,请在终端中打开项目文件夹所在的目录,然后输入以下命令:

flutter create my_app
Enter fullscreen mode Exit fullscreen mode

my_app是我们要构建的项目的名称。这将创建一个同名文件夹,所有代码都将存放于此。您应该会看到类似如下的输出:

Flutter初始化

请使用您选择的代码编辑器打开此文件夹。您可以通过输入命令运行您的应用程序,flutter run或者如果您使用的是 VS Code,则可以使用Flutter 调试器。

安装依赖项

本项目需要以下依赖项:

要安装这些依赖项,请键入以下命令

flutter pub add url_launcher walletconnect_dart google_fonts slider_button
Enter fullscreen mode Exit fullscreen mode

添加资源文件夹

我们希望在应用程序的用户界面中使用静态图片。为此,我们需要创建一个文件夹来存放我们的资源,并告诉 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/
Enter fullscreen mode Exit fullscreen mode

了解流程

最后,在开始编写应用程序代码之前,了解用户流程至关重要。下图展示了用户从打开应用程序开始的整个流程:

流程图

代码跟随

我们将从文件夹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();
  }
}
Enter fullscreen mode Exit fullscreen mode

我们首先创建一个新的无状态组件(Stateless Widget)。该组件将作为我们项目的起点。根据上面的流程图,第一步是创建登录页面。虽然本教程中的应用只有一个页面,但我们应该有一个完善的路由系统,以便在项目开发过程中轻松地添加新页面。

创建路线

Flutter 中的路由机制与 Web 应用的路由机制非常相似,都使用 ` /path<path>` 格式。简单来说,路由就是将路径映射到对应的 widget。路由的工作原理示例如下:

return MaterialApp(
  initialRoute: "/login",
    routes: {
      "/login": (context) => const LoginPage(),
          "/home": (context) => const HomePage()
    },
);
Enter fullscreen mode Exit fullscreen mode

在这里,routes我们定义了项目中将使用的所有路由及其对应的组件。在这个例子中,我们指定LoginPage当用户位于路由名称(router)时渲染组件/login,同样地,当用户位于/home路由名称(route)时HomePage渲染组件。该initialRoute字段指定要加载的初始路由。在这个例子中,用户打开应用后看到的第一个组件就是该LoginPage组件。

由于项目中会有多个路由,并且这些路由会在多个文件中使用,因此直接定义路由是不明智的。为了提高代码的健壮性,应该定义常量变量名。为此,在utils项目根目录下创建一个名为 `routes` 的新文件夹lib,并在该文件utils夹内创建一个名为 `routes.txt` 的新文件routes.dart。该文件将用于存储所有路由。在该文件中,按如下方式定义路由:

class MyRoutes {
  static String loginRoute = '/login';
}
Enter fullscreen mode Exit fullscreen mode

现在让我们回到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(),
      },
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

这里我们导入新创建的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();
  }
}
Enter fullscreen mode Exit fullscreen mode

这里我们创建了一个名为 `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(),
      },
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

设计登录页面

现在是时候设计我们的登录页面了。在本教程中,我们将设计一个非常简单的登录页面。首先,它只有一个图片和一个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"))
          ],
        ),
      ),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

接下来我们将进行以下操作:

  • 我们首先返回一个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';
Enter fullscreen mode Exit fullscreen mode

接下来,在我们的_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'
    ]));
Enter fullscreen mode Exit fullscreen mode

我们使用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);
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

接下来我们将进行以下操作:

  • 首先,我们通过检查变量的值来判断连接是否已建立connector.connected。如果连接尚未建立,则继续执行代码if块内的代码。
  • 我们使用try-catch代码块来捕获建立连接过程中可能出现的任何异常,例如用户cancel在 Metamask 弹出窗口中点击。
  • 在这个代码块中try,我们使用一个connector.createSession()函数创建一个新的会话。该函数接收一个函数作为参数,该函数会在 URI 生成时执行。在这个函数内部,我们使用另一个launchUrlString()函数在外部应用程序中打开生成的 URI。我们将生成的 URI 作为参数传递,并且由于它将打开一个外部应用程序,因此我们将 ` modeis` 设置为 ` LaunchMode.externalApplication.`。最后,由于我们希望代码等待 Metamask 确认连接,因此我们awaitlaunchUrlString()函数中使用了 `wait` 关键字。
  • 我们可以使用 `get_accounts_connected_counts` 获取已连接的账户,session.accounts并使用 `get_chain_id` 获取链 ID session.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"))
          ],
        ),
      ),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

现在,我们运行应用程序🤞🏾。如果一切操作正确,您将看到熟悉的登录页面。但是,点击按钮后Connect with Metamask,页面会跳转到 MetaMask。Metamask 会提示您连接钱包,并显示您在相应clientMeta字段中指定的 URL 和图标。

Metamask 弹出窗口

点击蓝色Connect按钮后,我们会跳转回钱包。现在可能看不到任何变化,但如果查看日志,应该可以看到 Flutter 打印出了账户地址和链 ID。

显示链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;
            }));

    ...
  }
Enter fullscreen mode Exit fullscreen mode

在这里,我们订阅了不同的事件。在事件触发时,session_update我们会更新应用程序的状态setState,并将更新后的有效负载赋值给_session变量。我们还会打印新的账户地址和链 ID,以便从终端检查代码是否正常运行。

对应用进行热重载,并按照相同的步骤将 MetaMask 连接到您的应用。现在,您可以在 MetaMask 中更改网络和已连接的账户,并在终端/控制台中观察链 ID 和账户地址的变化。

更新 ChainId 和账户

在屏幕上显示数据

我们已成功将 MetaMask 与我们的应用程序连接。虽然教程到此即可结束,但我还是希望将详细信息显示在屏幕上,以便用户进行验证,从而提供更好的登录体验。

我们首先希望用户登录 Metamask 后,显示详细信息而不是按钮。为此,我们将代码封装ElevatedButton在三元运算符中,如下所示:

(_session != null) ? Container() : ElevatedButton()
Enter fullscreen mode Exit fullscreen mode

这里,如果_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),
      ),
    ]
  )
)
Enter fullscreen mode Exit fullscreen mode
  • 20px我们首先在左右两侧添加少量内边距。
  • 我们希望从一开始就进行跨轴对齐,因此我们将其定义crossAxisAlignmentCrossAxisAlignment.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';
  }
}
Enter fullscreen mode Exit fullscreen mode

该函数使用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),
      )
  ],
),
Enter fullscreen mode Exit fullscreen mode

接下来,我们要检查用户是否连接到正确的网络。我们检查它是否与_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()
Enter fullscreen mode Exit fullscreen mode

接下来,我们添加依赖项SliderButton。我们在文件开头使用以下语句导入依赖项:

import 'package:slider_button/slider_button.dart';
Enter fullscreen mode Exit fullscreen mode

最后,在我们的内部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),
  ),
)
Enter fullscreen mode Exit fullscreen mode

目前它还SliderButton没有任何作用,但在后续教程中,它会将我们导航到应用程序的主页。

现在您的应用已完全准备就绪,可以运行了。如果一切都按照本教程中的说明进行,您的应用现在应该已经可以运行了。您应该能够使用 Metamask 登录您的应用。虽然应用本身不会登录任何页面,但您仍然可以使用移动应用连接到 Metamask。是不是很棒?!

总结

哇!这篇教程内容很丰富。在本教程中,我们讲解了如何从零开始构建一个非常基础的 Flutter 应用。我们学习了如何从应用中与 Metamask 进行交互。我们探索了两个重要的依赖项,walletconnect_dart以及url_launcher它们的工作原理和如何利用它们将应用与 Metamask 等钱包连接起来。我们还学习了如何在用户更新 Metamask 会话时更新应用。最后,我希望大家都能从中学习到一些新知识,度过一段愉快的时光。

该项目的代码已上传至Github,链接在此

我计划将这款应用扩展成一个功能更强大的应用,并深入探索 DeFi、区块链等领域。如果您喜欢这篇教程,请不要忘记点赞并分享到您的社交媒体,或者在讨论区留下您的反馈意见,帮助我改进。如果您想与我联系或推荐任何主题,可以通过LinkedInTwitter或邮件联系我。

我们下次再见,届时将推出新的教程或博客。在此之前,请注意安全,多陪伴家人,并继续努力!

文章来源:https://dev.to/bhaskardutta/building-with-flutter-and-metamask-8h5