Building with Flutter and Metamask

Building with Flutter and Metamask

·

19 min read

This tutorial was originally posted here

Building apps is one of the coolest things to be done as a software developer and tech enthusiast. Apps are not only more portable and user friendly, based on the requirements, but sometimes they are the only option for a particular application In this tutorial and the upcoming series, we will learn how to build a cool cross-platform mobile application using Flutter and Metamask.

The problem we are solving

To interact with the blockchain, the users must have an account (public and private key pair) on the blockchain that is used to sign the transactions. Sharing the private key with someone is equivalent to sharing access to the account. Because of this, users will be reluctant to provide their private key to the app since this would raise a lot of security concerns. The industry-trusted method is to use a “Non-custodial wallet” like Metamask.

Although there are numerous tutorials on how to use the browser extension of Metamask, using the mobile app version isn’t properly documented yet. In this tutorial series, we will be covering how to connect a Flutter App with Metamask for user login and in later articles, interacting with smart contracts will also be covered. We are using Flutter as our development framework of choice because we want to build a cross-platform application that is supported in both Android and IOS.

In this tutorial, we will be building an App that will be using Metamask for login. It will get the public key from Metamask. Metamask can also be used for signing messages and transactions but that will be covered in a different tutorial. The following GIF shows what we are going to build:

Demo GIF

Pre-requisites

  • Flutter is installed in your system. You can follow the official guide of Flutter here.
  • Have an Android / Ios emulator or physical device connected that will be used for testing the application. I recommend using Android Emulator as there are some bugs while working with Ios.
  • Install and set up Metamask Mobile App in your Android emulator.
💡 It is recommended to use VS Code with Flutter extension.

Known challenges

While writing this article, there are some challenges with building and running the app on IoS.

  • IoS doesn’t support installing third-party applications from App Store for simulators. So we have to install Metamask directly from its Github repo.
  • If you are using a MacBook with Apple Silicon, there are some extra steps needed for setting up Metamask in a simulator. You can read about it here.
  • Deep Linking with Metamask is not working as expected. (For this tutorial it is recommended to use Android Emulator)

I will update this article if I find the solutions to the above problems. Till then your helpful comments are highly expected.

Project initiation

We start by running flutter doctor to make sure we have everything set up properly for our development journey. You should see similar to :

Running Flutter Doctor

Now we start by creating a new flutter project. To do this, open the location where you want to store your project folder in your terminal and type the following command:

flutter create my_app

Here my_app is the name of the project we are going to build. This will create a folder with the same name where all our code will reside. You should see an output similar to:

Flutter initiation

Open this folder in a code editor of your choice. You can run your app by typing flutter run or using the Flutter Debugger if you are using VS Code.

Installing dependencies

For this project we will require the following dependencies:

  • url_launcher: This will be used for opening Metamask from our app using a URI.
  • walletconnect_dart: This will be used for generating a URI that will be used to launch Metamask.
  • google_fonts: Optional dependency for using Google Fonts in our app.
  • slider_button: Optional dependency for using a Slider Button for login purposes.

To install these dependencies, type in the following command

flutter pub add url_launcher walletconnect_dart google_fonts slider_button

Adding assets folder

We want to use static images in our app’s UI. For that, we have to create a folder that will contain our assets and tell flutter to use them as assets for our project.

Create a folder called assets inside the root my_app folder. The name of the root folder will be whatever name you used for creating the flutter project. Inside the assets folder, we will create an images folder for storing our image assets. Finally, inside the pubspec.yaml file we add this folder by adding the following lines in the flutter section:

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/

Understanding the flow

Finally, before we start coding our app, it is important to understand the user flow. The following diagram represents the flow a user will go through starting from when he opens our app:

Flow diagram

Code Along

We will start from the main.dart inside the lib folder. The main.dart is the first file to be compiled and executed by Flutter. Clear all the contents of this file and paste in the following lines of code:

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();
  }
}

We start by creating a new Stateless Widget. This widget will act as the starting point of our project. Based on the flow diagram shown above, the first thing to do will be to create the Login Page. Although for this tutorial our app will have only one page, we should have a proper routing system so that we can easily keep on adding newer pages as we proceed with our project.

Creating routes

The way routing works in flutter is quite similar to how it works for web apps, i.e. using the /path format. In simple words, routes are nothing but a mapping of a path to its respective widget. An example of how routes work is:

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

Inside routes we define all the routes that will be used in our project and their respective widgets. In this example we are saying that the widget LoginPage will be rendered when the user is in the /login router and similarly when the user is in the /home route, the HomePage widget will be rendered. The initialRoute field tells the initial or starting route to be loaded. In this example, the first widget the user sees on opening the app will be the LoginPage widget.

Since there will be multiple routes present in a project which are used across multiple files, it is not wise to directly type the route. Rather, one should have constant variable names defined for the code to be more robust. For this create a new folder called utils inside the lib folder and inside the utils folder create a new file called routes.dart. This file will store all our routes. Inside this file define the routes like this:

class MyRoutes {
  static String loginRoute = '/login';
}

Now let’s get back to our main.dart file and make the following changes:

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(),
      },
    );
  }
}

Here we are importing our newly created routes.dart file and using the variable name instead of directly typing the route. Since we don’t have the LoginPage widget yet, we will be getting an error message. So let’s create our login page.

Creating Login Page

Inside the lib folder, let’s create a new folder called pages that will have all our pages. This folder will have all our pages. Inside the pages folder, create a new file called login_page.dart. Inside this file paste in the following code:

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();
  }
}

Here we are creating a new Stateful Widget called LoginPage. Now we can import this into our main.dart file by adding import 'package:my_app/pages/login_page.dart'; at the start of the file. The final main.dart file looks like this:

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(),
      },
    );
  }
}

Designing the Login Page

Now it’s time to design our Login Page. For this tutorial, we will be designing a very simple Login Page. To start with, it only has an image along with Connect with Metamask button. When Metamask is connected, it will display the account address and the chain connected with it. If the chain is not the officially supported chain (Mumbai Testnet for our case), we display a warning asking the users to connect to the appropriate chain. Finally, if the user is connected with the connected network, we show the details along with a “Slide to login” slider. These three are shown in the following diagrams respectively:

App Screens

Building the default Login Page

We start by editing the login_page.dart file. Make the following changes inside the _LoginPageState class:

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"))
          ],
        ),
      ),
    );
  }
}

Here we are doing the following:

  • We start by returning a Scaffold. Scaffold in flutter is used to implement the basic material design layout. You can read more about it here.
  • Then we are defining an AppBar with the title “Login Page”. This will be the title to be displayed on top of our app.
  • We start the body of our app by defining a SingleChildScrollView. This is helpful when our App is opened on a phone with a relatively smaller display. It enables the users to scroll through our widget. Read more about it here.
  • Inside the SingleChildScrollView we define a Column to contain the various components of our page as its children.
  • The first child we define is an image. We want to render an image stored inside our assets folder. For this, we use Image.asset() and pass in the path to where the image is stored. Remember to use a path already added as a source of assets. Previously we added the assets/images/ as a source of assets. I am using this image that I downloaded into the images folder and named main_page_image.png.
  • Next, we create a button using the ElevatedButton class. It takes two arguments:
    • onPressed: The function to be executed when the button is clicked. For now, this is blank.
    • child: A child widget that will determine how our button will look. For now, it is a Text with the string “Connect with Metamask”.

If you run the app now, you should see something like:

Initial look

Although pressing the button doesn’t do anything right now, we have our default look ready. It only gets more interesting from here 😎😎😎.

Understanding the dependencies

Next, we will be writing the logic behind the “Connect with Metamask” button. For these we will be using two important dependencies:

  • walletconnect_dart: This dependency will be used for connecting with Metamask. Practically it can be used with other wallets like Trust Wallet as well, but for this tutorial, we will focus only on Metamask.

    To understand how this works, we must first understand how Wallet Connect works. Wallet Connect is a popularly used protocol for connecting web apps with mobile wallets (commonly by scanning a QR code). It generates a URI that is used by the mobile app for securely signing transactions over a remote connection. The way our app works is, that we directly open the URI in Metamask using our next dependency.

    walletconnect_dart is a package for flutter written in dart programming language. We will use this dependency to generate our URI and connect with Metamask. This package also provides us with callback functions that can be used to listen to any changes done in Metamask, like changing the network connected with.

  • url_launcher: This dependency is used for launching URLs in android and ios. We will be using this dependency for launching the URI generated by walletconnect_dart in the Metamask app.

Using the dependencies in our code

We start by importing the dependencies in our login_page.dart file

import 'package:walletconnect_dart/walletconnect_dart.dart';
import 'package:url_launcher/url_launcher_string.dart';

Next, inside our _LoginPageState class we define a connector that will be used to connect with 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'
    ]));

We are using the WalletConnect class to define our connector. It takes in the following arguments:

  • bridge: Link to the Wallet Connect bridge
  • clientMeta: This contains optional metadata about the client
    • name: Name of the application
    • description: A small description of the application
    • url: Url of the website
    • icon: The icon to be shown in the Metamask connection pop-up

We also define two variables called _session and _uri, which will be used to store the session and URI respectively when our widget state is updated.

We define a function called loginUsingMetamask to handle the login process as follows:

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);
    }
  }
}

Here we are doing the following:

  • First, we check if the connection is already established by checking the value of connector.connected variable. If the connection is not already established, we proceed with the code inside the if block.
  • We use try-catch block to catch any exception that may arise during establishing the connection, like the user clicking on cancel in the Metamask pop-up.
  • Inside the try block, we create a new session by using the connector.createSession() function. It takes in a function as an argument that is executed when the URI is generated. Inside this function, we use the launchUrlString() function to open the generated URI in an external app. We pass in the generated URI as a parameter and since it will be opening an external application, we set the mode as LaunchMode.externalApplication. Finally, since we want our code to wait until the connection is confirmed using Metamask, we use the await keyword with launchUrlString() function.
  • We can fetch the accounts connected by using session.accounts and the chain id by using session.chainId. For now, we print the selected account using session.accounts[0] and the chain Id to the console to check if our code is working properly.
  • Finally, we update the state of our app using setState and store the created session in the _session variable.
  • If any exception is generated in any of the above statements, the catch block will be executed. Right now we only print the generated exception, but in the latter stages of the project, we can use more robust exception handling.

Finally, we call the loginUsingMetamask function as the onPressed argument of our created button. The final code looks something like this:

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"))
          ],
        ),
      ),
    );
  }
}

Now, we run our app 🤞🏾. If everything is done as described, we will be greeted with a familiar Login Page. But when we click on the Connect with Metamask button, it will redirect to Metamask. Metamask will prompt you to connect your wallet. It will show the URL and icon specified in the clientMeta field.

Metamask popup

When we click on the blue Connect button, we will be redirected back to our wallet. Right now we won’t see anything different, but if we check back the logs, you should see flutter printed the account address and the chain id.

Showing chainid and account

Congratulations 🥳 🎉!! You have successfully connected with your Metamask wallet and it was that simple.

There is still one challenge left. Users may not connect with the blockchain your Smart Contracts are deployed to. So before we let users inside our platform, we should check if connected with the correct blockchain. Also, we should update if the user changes the connected network and also the selected account.

Subscribing to events

Using our connector variable we can subscribe to connect, session_update and disconnect event. Paste the following code inside the build function:

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;
            }));

    ...
  }

Here we are subscribing to the different events. On a session_update we update the state of our app using setState and assign the updated payload inside the _session variable. We also print the new account address and the chain Id, so that we can check if our code is working properly from the terminal.

Perform a hot reload of your app and perform the same steps to connect Metamask with your app. Now you can change the network and the connected account from inside Metamask and observe the chain Id and account address change in your terminal/console.

Updating ChainId and Account

Displaying the data on the screen

We have successfully connected Metamask with our app. Although the tutorial can end right here, I would prefer to display the details on the screen for users to verify and create a better login experience.

The first thing we want is when the user has connected with Metamask, we want to display the details instead of the button. For this we wrap our ElevatedButton in a ternary operator as follows:

(_session != null) ? Container() : ElevatedButton()

Here, if the _session variable is null, i.e. Metamask is not connected, it would render the ElevatedButton else the Container will be rendered.

We start with the following code inside our 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),
      ),
    ]
  )
)
  • We start by adding small padding of 20px from left and right.
  • We want our cross-axis alignment to be from the start, so we define crossAxisAlignment as CrossAxisAlignment.start.
  • We want the first widget in our column to be a simple saying Account and below it shows the account address of the connected Metamask account. We use the Text widget for displaying the data and use GoogleFonts for styling. You can import Google fonts by writing

      import 'package:google_fonts/google_fonts.dart';
    

    on top of the file. We use the ${} notation to access the _session variable inside a pair of single-quote(’’).

The next thing we want to show is the name of the chain users are connected to. We want to display it in the following way:

Showing Network name

Since most of the users may not be familiar with the chain Ids of the different blockchains, it’s better to show them the name of the blockchain, rather than just the chain Id. To do this, we can write a simple function that takes in the chainId as input and returns the name of the chain. Inside the _LoginPageState define a function called getNetworkName as follows:

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';
  }
}

The function uses switch-case statements to return the name of the chain based on chainId.

Inside our Container, after the two Text widgets, we add a SizedBox with height of 20px to add some gap. Next we define a Row with two children widgets, the text “Chain” and the name of the chain obtained by calling the getNetworkName function. We do it like this:

Row(
  children: [
    Text(
      'Chain: ',
      style: GoogleFonts.merriweather(
        fontWeight: FontWeight.bold, fontSize: 16),
      ),
      Text(
        getNetworkName(_session.chainId),
        style: GoogleFonts.inconsolata(fontSize: 16),
      )
  ],
),

Next, we want to check if the user is connected to the correct network. We check with _session.chainId matches the chain id of our supported blockchain (in this case 80001 for Mumbai Testnet). If it’s not equal to the required chain id, we create a Row to display our icon and the helper text, otherwise, we create a Container that will be used for our 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()

Next, we add out SliderButton. We import our dependency with the following statement at the start of our file:

import 'package:slider_button/slider_button.dart';

Finally inside our Container, we define our SliderButton like this:

Container(
  alignment: Alignment.center,
  child: SliderButton(
    action: () async {
      // TODO: Navigate to main page
    },
    label: const Text('Slide to login'),
    icon: const Icon(Icons.check),
  ),
)

For now, the SliderButton doesn’t do anything, but in further tutorials, it will navigate us to the main page of our application.

Now your app is fully ready to be run. If everything was done as described in this tutorial, your app should be now ready. You should be able to Login In to your app using Metamask. Although the app doesn’t login into any page, still you can connect with Metamask using your mobile app. How awesome is that ?!!

Wrapping Up

Wow!! That was a log tutorial. In this tutorial, we covered how to build a very basic flutter app from scratch. We learned how to interact with Metamask from our app. We explored two important dependencies, walletconnect_dart and url_launcher, and learned how they work and how they can be used to connect an app with a wallet like Metmask. We also learned how to update our app when the user updates the Metamask session. And finally, I hope we all had a great time learning something new and interesting.

The code for this project is uploaded to Github here.

I plan to extend this application into an app that does more cool things and dive deeper into the world of Defi, Blockchain, and beyond. If you liked this tutorial, don’t forget to show your love and share it on your socials or help me improve by posting your feedback in the Discussion. If you want to connect with me or recommend any topic, you can find me on LinkedIn, Twitter, or through my mail.

We will meet again with another new tutorial or blog, till then stay safe, spend time with your family, and KEEP BUIDLING!

Did you find this article valuable?

Support Bhaskar by becoming a sponsor. Any amount is appreciated!