Flutter & Web 3.0

GitHub repository.

Alejandro Ferrero
Flutter España
13 min readJun 23, 2022

--

What happens when you mix the world’s most used cross-platform mobile framework with the current buzziest tech concept? If you’ve answered a not-so-short blog post packed with loads of interesting information, you’re totally right!

This blog post will walk you through the creation of a basic decentralized Flutter application (dApp), where I will demystify and simplify some intimidating Web 3.0 concepts along the way. Moreover, we can’t build a proper App without leveraging a suitable state-management solution and a scalable software architecture. Therefore, we’ll use the BLoC pattern and its Flutter implementation, flutter_bloc, to manage the state of the proposed App while relying on a layered architecture heavily influenced by the concepts introduced by my colleague Marcos Sevilla. Lastly, this application includes 100% code coverage through unit testing, highlighting the importance of the architectural concepts and patterns underpinning the App’s implementation.

Grab a cup of coffee, buckle up, and let’s get right into it!

⚙️ Setup

Before diving into any Flutter or Web 3 implementation, you’ll need to set up your local environment with the following tools:

  • Flutter 3.0.x
  • Node.js
  • Your Code Editor of choice — I recommend Visual Studio Code since it’s the one I use in this tutorial.
  • Xcode and/or Android Studio — if you don’t use a Mac, you’ll be limited to Android Studio, but worry not, as you’ll be able to follow along without any issues.
  • Ganache

Luckily for you, there’s a tutorial including a step-by-step guide about setting up the first four items of the tools list and some other convenient stuff — written by yours truly 😉

All set? Let’s move on then.

🛠 Project Creation

We’ll use very_good_cli to create our Flutter project. Go to your terminal, open a window, and execute the following command to install the Very Good CLI in your system

Next, navigate to the directory where you want to place your new Flutter App and execute the following command

This command will populate a new app called hello_web3 — Notice you can name it whatever you like. You can find more information about why we used this tool to create our Flutter App here.

Once you’ve created the App project, open it on your code editor… and let’s get coding!

📝 Smart Contract

I will not be going too in-depth about Blockchain or Smart Contract concepts to not overwhelm you with information that would require a separate, dedicated blog. Therefore, I’ll only cover all the tools, configurations, and code strictly related to this blog that will help you better understand the proposed Flutter dApp.

Implementation

Firstly, you need to install truffle, the most popular development framework for Smart Contract development. Assuming you’ve correctly installed Node.js on your machine, execute the following command

Let’s now initialize a truffle project within our already-created Flutter App. Directly under your main App directory (hello_web3 in my case), create a new directory and name it web3.

Next, open a terminal window and navigate into the newly created web3 directory and execute the following command

Your directory structure should look like this:

Web 3 directory structure
  • contracts/ : Contains solidity contract files.
  • migrations/: Contains migration script files (Truffle uses a migration system to handle contract deployment).
  • test/: you can remove this directory and its contents since we won’t be diving into smart contract testing in this blog.
  • truffle-config.js: Contains truffle deployment configuration information.

We can now create the Smart Contract that’ll act as the back-end logic and storage for our dApp. Create a new file named HelloWeb.sol inside the contracts directory, and paste the following Solidity code.

HelloWeb3.sol

Even if you’re unfamiliar with the Solidity programming language, the code is quite straightforward:

  • Declare the minimum version of Solidity required at the top of the contract
  • Create a contract named HelloWeb3
  • Declare a public variable of type string, getName
  • Include a constructor for the contract and initialize the getName property to “Unknown”
  • Create a set function, setName, that sets the getName property to the same value as the string parameter newName

Having created our smart contract, let’s now compile it — make sure you’re in the web3/ directory on your terminal (my relative path is hello_web3/web3/)

You should see a similar output on your terminal window to the one below:

truffle compile results

Deployment

Under the migrations/ directory, you’ll notice there’s already a file named 1_initial_migration.js, which handles the Migrations.sol contract migration. In other words, it allows future Smart Contract migrations while ensuring unchanged contracts aren’t migrated (double-migrate prevention). We now need to create a new file named 2_deploy_contracts.js under migrations and include the following JavaScript code:

2_deploy_contracts.js

Moreover, open the truffle-config.js file, remove all of its contents and paste the following code:

truffle-config.js

You can find more detailed info about the contents of this file by referring to the official truffle docs.

Before migrating (aka deploying) our newly created Smart Contract, we need to kick off a Blockchain. This is where Ganache comes into play, as it provides an easy-to-use personal blockchain for Web 3 development where we can deploy contracts, develop dApps, and run tests. So let’s open Ganache, and follow these steps in order:

  • Click on NEW WORKSPACE
  • Under WORKSPACE NAME, type the name for your workspace (hello-web3, for instance)
  • Click on ADD PROJECT, and select the truffle-config.js file we just modified
Truffle Workspace Config
  • Click on SAVE WORKSPACE, and wait for the workspace to be created

You should now have an Ethereum blockchain running locally on port 7545, including a few addresses with a balance of 100.00 fake ETH.

Truffle Workspace Created

⚠️ Keep Ganache open, and ensure your newly created workspace’s blockchain is active for the remainder of this blog.

We can finally deploy our HelloWeb3 Smart Contract by running the following command from a terminal (make sure you’re working directory is still <app_name>/web3/)

This command should produce a result on your terminal similar to the one below. Also, notice that the balance of the first address listed on your Ganache blockchain has decreased its ETH balance.

truffle migrate results

👏 Congrats! You’ve successfully created and deployed a Smart Contract to a local Blockchain.

‪‪📦‬ Dart Packages

It’s time to pull up our sleeves and do some Dart coding. We’re going to create two packages:

  • smart_contract_client, which is part of our Data Layar, the lowest layer of our architecture, and will interact directly with the deployed Smart Contract
  • smart_contract_repository, which decouples the business logic and data layers while composing the smart_contract_client package, acting as a middleman between these layers

smart_contract_client

Firstly, type the following command on your terminal to create a new directory named packages directly under your main project folder and navigate into it:

Let’s now use the Very Good CLI to create our client package by executing the following command

From now on, all the files referenced under this subsection are located under packages/smart_contract_client/.

Next, open the newly created package’s pubspec.yaml on your code editor, add web3dart: ^2.4.0 under dependencies, and save the file.

Moreover, create two files under lib/src, exceptions.dart and smart_contract.dart, and export these files by adding them to lib/smart_contract_client.dart

smart_contract_client.dart barrel file

Paste the following code into exceptions.dart, which includes the different exceptions that may be thrown by this client:

exceptions.dart in smart_contract_client

And now paste the following code into smart_contract.dart:

smart_contract.dart

SmartContract extends DeployedContract to simplify the creation of a Web3Client contract and its functions. Additionally, it has a private default constructor, forcing developers to instantiate this class via its factory constructor .fromData. This factory constructor handles the String manipulation required to get the deployed contract’s Application Binary Interface data, address, and the credentials to interact with it. Lastly, it returns an instance of SmartContract allowing access to its credentials property and the contract’s functions, getName and setName.

Moving on, we now focus on the core file of our client package, lib/src/smart_contract_client.dart. Copy and paste the following code into this file and let’s discuss this class’ purpose:

smart_contract_client.dart

SmartContractClient is a class wrapping around Web3Client, provided by the web3dart dependency. We created this custom client package to leverage proper Dependency Injection practices by passing Web3Client as an argument to our SmartContractClient constructor, allowing us to thoroughly test its behavior. Additionally, it abstracts and simplifies the creation of a DeployedContract instance, which is lazily handled by the private getter _contract. Lastly, it exposes two methods, getName and setName, which facilitate the desired interactions with our deployed Smart Contract.

smart_contract_repository

Once again, let’s use the Very Good CLI to create our repository package under the packages/ directory by executing the following command

From now on, all the files referenced under this subsection are located under packages/smart_contract_repository/.

Much like we did with the client package, let’s create an exceptions.dart file under lib/src/ and then paste the following code:

exceptions.dart in smart_contract_repository

Do not forget to export this file from lib/smart_contract_repository.dart:

smart_contract_repository.dart barrel file

Moreover, let’s now focus on the lib/src/smart_contract_repository.dart file and paste the following code:

smart_contract_repository.dart

Notice the SmartContractRepository is a pretty simple class that will behave as a middleman between the Flutter App (presentation + business logic layers) and the SmartContractClient (data layer). Most importantly, it requires a SmartContractClient instance as a constructor parameter, allowing us to thoroughly test this class. Notice that this repository package may seem somewhat redundant and overkill in this particular case. However, should the App grow larger, we may need to apply layer-specific error handling or include domain-specific models. Therefore, leveraging this layered architecture provides a great foundation to build our App upon while enhancing its maintainability and scalability.

‪🟦 ‬State Management — BLoC

State management is arguably one of the most controversial, debated-about, and critical decisions software architects and engineers must make when implementing any software App. Thus, we will leverage the BLoC pattern through its Flutter implementation with flutter_bloc as it provides a predictable, simple, and highly-testable solution — read more about this selection criteria here.

Open the pubspec.yaml file located directly under your main App directory, and paste the following dependencies:

If you get the following output error when saving the pubspec.yaml

async dependency error

Please, add async: ^2.8.2 as a dependency override:

From now on, all the files referenced under this subsection are located under lib/.

Create a new directory named hello, and create a bloc named hello. You can simplify this process using the bloc extension for VS Code. Otherwise, you may need to manually create this directory structure and its files.

hello bloc directory

Having this setup ready, let’s look at the code that goes into each of the created bloc files.

Paste the following code into hello_event.dart:

hello_event.dart

Notice how HelloEvent is an immutable, abstract class that extends Equatable to enable value comparison. We use this class to extend the two bloc events we will trigger from the Flutter presentation layer.

Paste the following code into hello_state.dart:

hello_state.dart

Once again, HelloState is an immutable class that extends Equatable to enable value comparison. However, notice this class is not abstract as we need to instantiate it. Let’s review the different parts of this class:

  • HelloAsyncStatus status is a property that allows identifying the current status of the state at any given point.
  • String? name is a property that stores the fetched/set value from/to the deployed Smart Contract.
  • List<String> txHashes is a property that stores a list of all the transaction hashes returned by the Smart Contract after setting a new name value.
  • hasData, hasError, and isLoading are helper getters that allow us to readily determine the status of HelloState.
  • copyWith is a helper method that allows us to change the HelloBloc’s state by returning a new instance of HelloState, including any desired property changes.

Paste the following code into the hello_bloc.dart:

hello_bloc.dart

Much like we did with SmartContractRepository and SmartContractClient, we provide the required dependencies the HelloBloc class will leverage internally as constructor parameters. Notice that both the SmartContractRepository and UsernameGenerator instances are assigned to private bloc properties and then used as needed by the different bloc private methods, _nameRequested, and _randomNameSet. These private methods have a 1:1 relationship with the bloc events we created in hello_event.dart, and, in this case, HelloBloc relies on them to interact asynchronously with the repository layer. It is worth noting the use of the .copyWith method, which enables developers to emit state updates within the bloc method’s bodies. This approach is especially effective when dealing with asynchronous code execution and error handling.

‪🎨 ‬Flutter UI

Always under the hello/ directory, create a file named hello.dart, which we will use as a barrel file, and add the following code:

hello.dart barrel file

Let’s now create a new directory named view, and add the following files:

  • view.dart is a barrel file that exports all the other files found under the view directory
view.dart barrel file
  • hello_page.dart includes a StatelessWidget class, HelloPage, whose sole purpose is to provide the HelloView with access to the HelloBloc. Notice how we use BlocProvider to inject said bloc while passing the required instances to the bloc constructor, and immediately add the HelloNameRequested event. Thus, HelloView will have access to a HelloState reflecting the changes triggered by HelloNameRequested
hello_page.dart
  • hello_view.dart features a BlocConsumer where its listener handles the error states by notifying the user with the corresponding snack bars, while its builder controls the rendering of the correct UI content depending on HelloState. It is worth observing how the state getters make the if/else statements much more readable and concise.
hello_view.dart

At this point, you should see some compiling errors referring to HelloDataContent and HelloErrorContent. The reason behind these errors is that these widgets do not yet exist. So let’s take care of that!

Create a new directory named widgets under view/, and add the following files

  • widgets.dart is a barrel file that exports all the other files found under the widgets directory.
widgets.dart barrel file
  • hello_error_content.dart includes the stateless widgets shown in case the App is loaded with a state that has an error. Observe how we use context.watch to allow the UI to react to state changes. Moreover, notice how the TryAgainButton adds a HelloNameRequested event to prompt the bloc to fetch the name string value from the deployed Smart Contract.
hello_error_content.dart
  • hello_data_content.dart features the main view content shown on our Flutter app. Observe how we, once again, leverage context.watch to react to state changes and populate the view with correct state data. Lastly, the SetNameButton adds a HelloRandomNameSet event to prompt the bloc to set a new name string value to the deployed Smart Contract.
hello_data_content.dart

‪💉‬ App Dependency Injection and dotenv

If you’ve made it this far into this blog, you’re one step away from having a fully functional Web 3 Flutter dApp.

Open the lib/app/view/app.dart and copy the following code:

Here we’re simply requiring a SmartContractRepository instance as a constructor parameter for App. Moreover, we leverage the same page-view pattern introduced previously to provide AppView access to the SmartContractRepository instance. Notice how HelloPage also has access to this repository since it’s located further down in the Widget Tree.

Next, let’s take care of the private strings that will allow us to interact with the deployed Smart Contract by creating an extensions folder under the lib/ directory and add the following files

  • extensions.dart is a barrel file that exports all the files found under the extensions directory.
extensions.dart barrel file
  • dot_env_extension.dart includes an extension on DotEnv to access the necessary private strings more consistently and safely via the declared getters.
dot_env_extension.dart

But where are these private strings found? Good question. We’re going to create a .env file directly under your App’s directory and add the following fields

  • CONTRACT_ADDRESS — go to Ganache, click on the CONTRACTS button at the top, and copy the ADDRESS hex value next to the field showing your contract’s name.
  • PRIVATE_KEY — go to Ganache, click on the ACCOUNTS button at the top, click on the key icon next to the address whose balance has decreased, and copy the private key string value.
  • RCP_URL — should be http://127.0.0.1:7545, the same as your default RPC SERVER URL.
  • WS_URL — should be ws://10.0.2.2:7545/

Once you’ve gathered these string values and pasted them into the .env file, it should look something like this (notice your CONTRACT_ADDRESS PRIVATE_KEY values will be different):

⚠️ I have uploaded this file to my GitHub repository for learning purposes since they refer to a test Smart Contract. However, you should never, under any circumstances, push your .env file to any remote repository. Therefore, add .env to your .gitignore file to avoid committing this data.

Lastly, open the lib/main/main_common.dart file and paste the following code:

main_common.dart

This function allows us to instantiate and inject as dependencies all the necessary client and repository classes: Web3ClientSmartContractClient → SmartContractRepository. Also, notice how we asynchronously load the contents of web3/artifacts/HelloWeb3.json and .env to have access to the string values required by lower-layer components. So, let’s add web3/artifacts/HelloWeb3.json and .env as assets to your pubspec.yaml file to allow our app to access their contents.

🌯 And that’s a wrap! You should now be able to compile and execute this Flutter dApp and see the following results:

Flutter dApp

‪✨‬ Final remarks & conclusions

This blog walked you through all the necessary steps to create a simple Flutter dApp. Most importantly, it showed how any project that requires a UI may benefit from Flutter and Dart, as they provide developers with all the necessary tools to implement a front-end App. Ultimately, we combined Web 3.0 and Flutter to implement a maintainable, scalable, testable dApp with the help of the BLoC pattern and a sound layered architecture.

I would love to hear your feedback about this blog or discuss any related topics, so feel free to drop a comment or reach out to me on Twitter.

--

--

Alejandro Ferrero
Flutter España

🤤 Founder of Foodium 💙 Lead Mobile Developer at iVisa 💻 Former Google DSC Lead at PoliMi 🔗 Blockchain enthusiast 🇪🇸 🇺🇸 🇮🇹 🇦🇩 Been there, done that