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:
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 :
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:
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:
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:
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 aColumn
to contain the various components of our page as itschildren
. - The first child we define is an image. We want to render an image stored inside our
assets
folder. For this, we useImage.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 theassets/images/
as a source of assets. I am using this image that I downloaded into theimages
folder and namedmain_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 aText
with the string“Connect with Metamask”
.
-
If you run the app now, you should see something like:
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 indart
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 bywalletconnect_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 theif
block. - We use
try-catch
block to catch any exception that may arise during establishing the connection, like the user clicking oncancel
in the Metamask pop-up. - Inside the
try
block, we create a new session by using theconnector.createSession()
function. It takes in a function as an argument that is executed when the URI is generated. Inside this function, we use thelaunchUrlString()
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 themode
asLaunchMode.externalApplication
. Finally, since we want our code to wait until the connection is confirmed using Metamask, we use theawait
keyword withlaunchUrlString()
function. - We can fetch the accounts connected by using
session.accounts
and the chain id by usingsession.chainId
. For now, we print the selected account usingsession.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.
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.
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.
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
asCrossAxisAlignment.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 theText
widget for displaying the data and useGoogleFonts
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:
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!
Top comments (22)
Something I didn't know i was looking for until i found it, as a flutter developer who started his Blockchain journey, this is what i needed
Clearly explained 🎯
Glad I could help. I am a Blockchain Developer who got interested in Flutter. I have plans to bring more tutorials in this domain. 👨🏾💻
Looking forward to them, as i will be diving deep into learning Blockchain too
🙂
i used this but it's keep saying unable to find host for that bridge and when i open bridge url on chrome its unable to load what should i do now...?how could i connect my metamask wallet in my flutter app
Hi there. I was following your tutorial on my project to integrate metamask login.
Unfortunately for me however the action sheet / bottom sheet on the metamask app suddenly stopped appearing.
I suspect the deprecation of wallet connect v1 and retirement on June 28 is the issue.
Can we expect a tutorial to do similar things with wallet connect v2 ?
Thank you
same bro the connect pop up doesn't appear in mine too have you found a solution?
Well explained, thanks. Looking for further tutorials. Deposit, Withdraw, etc.
Hi bro, thanks so much for the post, im try to implement this in my project but i have a problem with this, the idea is implement this autoconnect for two wallets, metamask and rainbow but the problem is i dont know how can i say to de library witch one of them selected, i have two buttons one is for metamask and the other is for rainbow, but both are connected two rainbow, can you help me taks.
You can select default application in the popup. I am using Deeplinking, that is there is a specific URL that the app tries to open. Android finds which Apps are capable of opening that link and gives a popup asking which application to choose. You can set a default there. It's done by Android and no special code is needed.
It's an awesome tuto, Great thanks to bhaskar dutta
🙂
How about support in the browser?
Is this somehow possible yet?
I have not tested that yet. Thank you for the idea, I will look into it and reply. In the meantime, if you find something else, please do share.
Proper tutorial. Nicely explained and all the doubts were solved from within. Kudos!
I've been looking for a long time to connect the metamask in flutter. thanks you! Now I want to connect the smart contract.