Implementing Smart Contracts On Flutter DApps

Implementing Smart Contracts On Flutter DApps

Learn how to build a note-taking app on Flutter by integrating smart contracts through this tutorial

ยท

12 min read

Decentralized Application

A decentralized application(DApp) is an application that is meant to run in a decentralized computing system/blockchain and it was initially introduced by Ethereum Blockchain which is built using smart contracts. When using the Ethereum ecosystem, we have to use Solidity, a language which was built for writing smart contracts. Solidity is an OOP, high-level language for writing and implementing smart contracts that can later govern accounts and transactions on the Ethereum blockchain. In this article, we will build a web3 DApp project using Solidity and Flutter. We will use the web3dart package to interact with the Ethereum blockchain in our Flutter application.

Note: When following this tutorial, I am assuming that the reader has basic knowledge of Ethereum, smart contracts and the Flutter framework.

Things we will be covering in this article:

  • Setting up development for Solidity and Flutter app.
  • Setting up Truffle project
  • Creating smart contracts in solidity
  • Compiling and migrating smart contract
  • Linking smart contracts in the Flutter app
  • Creating awesome UI for the Flutter app

We will be creating a note-taking app in which all the notes will be stored in a blockchain using smart contracts.

Setting up development for Solidity

We will be using Truffle for developing smart contracts locally. Truffle is a popular framework for building and testing smart contracts using the Ethereum Virtual Machine(EVM). Truffle can be easily installed using the following command:

npm install -g truffle

We will use Ganache, which provides a testing Ethereum blockchain so that we can test and deploy smart contracts before publishing it.

Setting up the Truffle project

  • To initialize Truffle, we need to create a Flutter project first using the following command:
flutter create notetaking_dapp
cd notetaking_dapp
  • Once we have the basic Flutter app ready, we can initialize truffle by creating a folder named contracts and running:
mkdir contracts
cd contracts
truffle init

Screenshot 2021-08-19 at 11.59.58 AM.png

  • This command will create the following folders for you:

  • contracts/ : Contains smart contract code.

  • migrations/: Contains migrations scripts which will be used be truffle to handle deployment.
  • test/: Contains test scripts
  • truffle-config.js: Contains truffle configurations

Creating smart contracts in Solidity

  • Create NotesContract.sol file in contracts/ directory and add the following code:
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.5.16;

contract NotesContract {
    uint256 public notesCount;

    struct Notes {
        uint256 id;
        string noteTitle;
        string noteContent;
        uint256 timeStamp;
    }

    mapping(uint256 => Notes) public notes;

    constructor() public {
        notesCount = 0;
    }

    event NoteAdded(uint256 _id);
    event NoteDeleted(uint256 _id);
    event NoteEdited(uint256 _id);
}
  • notesCount is an unsigned public integer that stores the total number of notes in the smart contract.
  • struct Notes is the data structure for a note. It defines if and whether a note should contain an id, a title, content and timestamp. In this scenario, timestamp is an unsigned integer as we are already storing the time stamp in the Unix epoch. Unix epoch is the number of seconds that have elapsed since January 1, 1970.
  • notes is the map that stores all the notes with an id as key and Notes struct as value.
  • Create a constructor and set notesCount = 0. NoteAdded, NoteDeleted, and NoteEdited are the events that will be emitted by blockchain so that all the DApps can listen to these emitted events and function accordingly. All these events will emit IDs of specific notes along with them. Execute the following code to witness this:
    function addNote(
        string memory _noteTitle,
        string memory _noteContent,
        uint256 timeStamp
    ) public {
        notes[notesCount] = Notes(
            notesCount,
            _noteTitle,
            _noteContent,
            timeStamp
        );
        emit NoteAdded(notesCount);
        notesCount++;
    }
  • addNote function accepts note title, note content, and the time stamp as parameters. We have to create a new struct of Notes with these data and assign it to the current notesCount value in the notes map. Once we have added the Notes struct to the map, we have to emit the NotesAdded event with the current notesCount. After emitting the event we can increment the notesCount variable for the next note. Add the following function after the events to go to the next step:
    function deleteNote(uint256 _id) public {
        delete notes[_id];
        emit NoteDeleted(_id);
    }
  • Create a deleteNote function which accepts the ID of the note to be deleted. First, we have to remove the Notes from the notes map corresponding to the received ID. After that, we can emit the NoteDeleted event using the following code:
    function editNote(
        uint256 _id,
        string memory _noteTitle,
        string memory _noteContent
    ) public {
        notes[_id] = Notes(_id, _noteTitle, _noteContent, notes[_id].timeStamp);
        emit NoteAdded(_id);
    }
  • For updating notes, create the editNote function that accepts the ID of the note to be edited, title, and content. Now create a new struct with these values and assign it to the received ID in the notes map.

We are done with executing the smart contract code. Now we can compile and migrate smart contracts!

Compiling and migrating smart contract

In the terminal, go to the contracts directory and run the following command:

truffle compile

You should see an output similar to the image given below:

Screenshot 2021-08-19 at 12.32.46 PM.png

Migration

  • In the migrations/ directory, you will notice a file called 1_initial_migration.js which handles the deployment of the Migrations.sol contract. Each contract needs one migration file. Create a new file named 2_notes_contract_migration.js in the migrations/ directory and add the following code in that file:
const NotesContract = artifacts.require("NotesContract");

module.exports = function (deployer) {
  deployer.deploy(NotesContract);
};
  • Before running the migration, we need a local blockchain up and running on our system. For that, start the Ganache app which will start an instance of Blockchain on port 7545 as displayed below:

Screenshot 2021-08-19 at 12.39.26 PM.png

  • Now open the truffle-config.js and add the following content:
module.exports = {
  networks: {
    development: {
      host: "127.0.0.1",
      port: 7545,
      network_id: "*",
    },
    advanced: {
      websocket: true,
    },
  },
  contract_build_directory: "./src/abis",
  compilers: {
    solc: {
      optimizer: {
        enabled: true,
        runs: 200,
      },
    },
  },
};
  • Run the following code from the contract/ directory:
 truffle migrate
  • Once the above code is successfully executed, the first account will have slightly less than 100ETH as you can witness in Ganche which is due to the transaction cost of migrating smart contracts on the blockchain.

Linking smart contracts

  • In your pubspec.yaml file add the following packages:
  # Needed for interaction with smart contract
  http: ^0.13.3
  web3dart: ^2.1.4
  web_socket_channel: ^2.1.0
  provider: ^5.0.0

  # Needed for UI
  google_fonts: ^2.1.0
  flutter_staggered_grid_view: ^0.4.0
  • Add contracts/build/contracts/NotesContract.json file as an asset in your pubspec.yaml which can also generated by the truffle migrate command. Use the following code:
    assets:
    - contracts/build/contracts/NotesContract.json
  • Now create a note_controller.dart file in the lib directory and add the following code:
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:http/http.dart';
import 'package:notetaking_dapp/models/note.dart';
import 'package:web3dart/web3dart.dart';
import 'package:web_socket_channel/io.dart';

class NoteController extends ChangeNotifier {
  List<Note> notes = [];
  bool isLoading = true;
  int noteCount;
  final String _rpcUrl = "http://127.0.0.1:7545";
  final String _wsUrl = "ws://127.0.0.1:7545/";

  final String _privateKey =
      "e3beb61794a259037a8ca3379f15d61a82e9bb631ba248ffe14283fdc90c04c8";

  Web3Client _client;
  String _abiCode;

  Credentials _credentials;
  EthereumAddress _contractAddress;
  DeployedContract _contract;

  ContractFunction _notesCount;
  ContractFunction _notes;
  ContractFunction _addNote;
  ContractFunction _deleteNote;
  ContractFunction _editNote;
  ContractEvent _noteAddedEvent;
  ContractEvent _noteDeletedEvent;
}

Here are some important considerations to be looked into:

  • Importing all the required packages required.
  • Creating a ChangeNotifier class called NoteController.
  • Get the RPC URL from Ganache - ganache-rpc.png
  • Get the Private key of any account from Ganache by clicking on the Key icon. Screenshot 2021-08-19 at 1.08.42 PM.png
  • _client variable can used to connect to Ethereum node with WebSockets.
  • _abiCode is used to store the ABI of the smart contract. Read this to know more about ABI.
  • _credentials will contain credentials of the smart contract deployer.
  • _contractAddress will contain the smart contract's address on the blockchain.
  • _contract is the instance of our smart contract which will be used to call all the functions.
  • _notesCount, _notes, _addNote, _deleteNote, _editNote are the functions that are deployed on smart contracts.
  • _noteAddedEvent and _noteDeletedEvent are the events emitted by our contract which can be listed in the flutter app.

  • After variable declaration, create the following functions in note_controller.dart:

 NoteController() {
    init();
  }

  init() async {
    _client = Web3Client(_rpcUrl, Client(), socketConnector: () {
      return IOWebSocketChannel.connect(_wsUrl).cast<String>();
    });
    await getAbi();
    await getCreadentials();
    await getDeployedContract();
  }

  Future<void> getAbi() async {
    String abiStringFile = await rootBundle
        .loadString("contracts/build/contracts/NotesContract.json");
    var jsonAbi = jsonDecode(abiStringFile);
    _abiCode = jsonEncode(jsonAbi['abi']);
    _contractAddress =
        EthereumAddress.fromHex(jsonAbi["networks"]["5777"]["address"]);
  }

  Future<void> getCreadentials() async {
    _credentials = await _client.credentialsFromPrivateKey(_privateKey);
  }

  Future<void> getDeployedContract() async {
    _contract = DeployedContract(
        ContractAbi.fromJson(_abiCode, "NotesContract"), _contractAddress);
    _notesCount = _contract.function("notesCount");
    _notes = _contract.function("notes");
    _addNote = _contract.function("addNote");
    _deleteeNote = _contract.function("deleteNote");
    _editNote = _contract.function("editNote");

    _noteAddedEvent = _contract.event("NoteAdded");
    _noteDeletedEvent = _contract.event("NoteDeleted");
    await getNotes();
  }
  • In getAbi() function, we are fetching the NotesContract.json asset and decoding it to get json data. From that data, we can extract the ABI content and address of the deployed smart contract.
  • In getCreadentials() function we will be passing our private key in _client.credentialsFromPrivateKey() function which returns an instance of the Credentials class.
  • In getDeployedContract(), we will be creating an instance of DeployedContract using abiCode, contract name, and contract address. Once we have an instance of the contract we can create instances of all the functions and events we have in our smart contract as shown above.
  • After these functions paste the following code to perform the CRUD operation.
  getNotes() async {
    List notesList = await _client
        .call(contract: _contract, function: _notesCount, params: []);
    BigInt totalNotes = notesList[0];
    noteCount = totalNotes.toInt();
    notes.clear();
    for (int i = 0; i < noteCount; i++) {
      var temp = await _client.call(
          contract: _contract, function: _notes, params: [BigInt.from(i)]);
      if (temp[1] != "")
        notes.add(
          Note(
            id: temp[0].toString(),
            title: temp[1],
            body: temp[2],
            created:
                DateTime.fromMillisecondsSinceEpoch(temp[3].toInt() * 1000),
          ),
        );
    }
    isLoading = false;
    notifyListeners();
  }

  addNote(Note note) async {
    isLoading = true;
    notifyListeners();
    await _client.sendTransaction(
      _credentials,
      Transaction.callContract(
        contract: _contract,
        function: _addNote,
        parameters: [
          note.title,
          note.body,
          BigInt.from(note.created.millisecondsSinceEpoch),
        ],
      ),
    );
    await getNotes();
  }

  deleteNote(int id) async {
    isLoading = true;
    notifyListeners();
    await _client.sendTransaction(
      _credentials,
      Transaction.callContract(
        contract: _contract,
        function: _deleteeNote,
        parameters: [BigInt.from(id)],
      ),
    );
    await getNotes();
  }

  editNote(Note note) async {
    isLoading = true;
    notifyListeners();
    print(BigInt.from(int.parse(note.id)));
    await _client.sendTransaction(
      _credentials,
      Transaction.callContract(
        contract: _contract,
        function: _editNote,
        parameters: [BigInt.from(int.parse(note.id)), note.title, note.body],
      ),
    );
    await getNotes();
  }
  • getNotes() function is used to fetch all the notes present on the blockchain. First, we shall call the _notesCount function which will give us the total number of notes present in blockchain and then we can run a for loop, fetch each note and add it into a list of notes in Flutter.
  • addNote() function is used when we want to create a new note. Since we are adding data to the blockchain this is considered as a transaction and hence we shall call the _client.sendTransaction() function and pass it through our _credentials which will then be used to pay gas fees for that transaction.
  • deleteNote() function is used when we want to delete a specific note from the blockchain. Similar to the addNote function, we will be making changes in the state of the blockchain by deleting a note. Hence, we have to make a transaction as we did in the addNote function and pass _credentials.
  • editNote() function is used to modify any data in a specific note. This is similar to add and delete note function.
  • In addNote, deleteNote and editNote we will be calling getNote() function towards the end to get the updated notes from blockchain.

Creating a UI for the Flutter app

  • Before creating the UI we have to wrap out the MaterialApp widget in main.dart with ChangeNotifierProvider and pass an instance of NoteController so that we can use all the contract functions and values in our app. Refer to the code given below:
class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider(
      create: (_) => NoteController(),
      child: MaterialApp(
        title: 'Note Taking DApp',
        theme: ThemeData.dark(),
        home: HomeScreen(),
      ),
    );
  }
}
  • Once we are done with setting up the provider for NoteController, we can start creating UI. You can create a home_screen.dart file and paste the following code to do this:
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:notetaking_dapp/controllers/note_controller.dart';
import 'package:notetaking_dapp/models/note.dart';
import 'package:notetaking_dapp/screens/notes_details_screen.dart';
import 'package:notetaking_dapp/screens/notes_edit_screen.dart';
import 'package:notetaking_dapp/utils/themes.dart';
import 'package:provider/provider.dart';

class HomeScreen extends StatefulWidget {
  HomeScreen({Key key}) : super(key: key);

  @override
  _HomeScreenState createState() => _HomeScreenState();
}

class _HomeScreenState extends State<HomeScreen> {
  NoteController noteController;
  List<Note> notes;
  @override
  Widget build(BuildContext context) {
    noteController = Provider.of<NoteController>(context, listen: true);
    notes = noteController.notes;
    return Scaffold(
      floatingActionButton: GestureDetector(
        onTap: () {
          Navigator.of(context).push(
            MaterialPageRoute(
              builder: (context) => NotesEditScreen(),
            ),
          );
        },
        child: Container(
          height: 60,
          width: 60,
          decoration: BoxDecoration(
            color: Color(0xFF80DFEA),
            borderRadius: BorderRadius.circular(200),
          ),
          child: Center(
            child: Text(
              "+",
              style: TextStyle(
                fontSize: 40,
                fontWeight: FontWeight.w500,
              ),
            ),
          ),
        ),
      ),
      backgroundColor: ColorConstant.bg,
      body: AnnotatedRegion<SystemUiOverlayStyle>(
        value: SystemUiOverlayStyle.light,
        child: noteController.isLoading
            ? Center(
                child: CircularProgressIndicator(),
              )
            : SafeArea(
                child: Container(
                  padding: EdgeInsets.symmetric(horizontal: 25),
                  child: Column(
                    children: [
                      SizedBox(height: 30),
                      Row(
                        mainAxisAlignment: MainAxisAlignment.spaceBetween,
                        children: [
                          Text(
                            "Notes",
                            style: GoogleFonts.montserrat(
                              fontSize: 40,
                              color: Colors.grey[100],
                            ),
                          ),
                          Container(
                            decoration: BoxDecoration(
                              color: ColorConstant.bgAccent,
                              borderRadius: BorderRadius.circular(15),
                            ),
                            padding: EdgeInsets.all(10),
                            child: Icon(
                              Icons.search,
                              color: Colors.white,
                              size: 30,
                            ),
                          ),
                        ],
                      ),
                      SizedBox(height: 30),
                      Expanded(
                        child: new StaggeredGridView.countBuilder(
                          crossAxisCount: 2,
                          itemCount: notes.length,
                          itemBuilder: (BuildContext context, int index) {
                            bool isThirdNote = (index + 1) % 3 == 0;
                            return GestureDetector(
                              onTap: () {
                                Navigator.of(context).push(
                                  MaterialPageRoute(
                                    builder: (context) => NotesDetailsScreen(
                                      note: notes[index],
                                    ),
                                  ),
                                );
                              },
                              child: Container(
                                decoration: BoxDecoration(
                                  color: ColorConstant.notesBg[index % 5],
                                  borderRadius: BorderRadius.circular(12),
                                ),
                                padding: EdgeInsets.all(25),
                                child: Column(
                                  mainAxisAlignment:
                                      MainAxisAlignment.spaceBetween,
                                  crossAxisAlignment: CrossAxisAlignment.start,
                                  children: [
                                    Text(
                                      notes[index].title,
                                      style: GoogleFonts.montserrat(
                                        fontSize: isThirdNote ? 32 : 24,
                                        fontWeight: FontWeight.w500,
                                      ),
                                      maxLines: 4,
                                      overflow: TextOverflow.ellipsis,
                                    ),
                                    Text(
                                      "May 21, 2021",
                                      style: GoogleFonts.montserrat(
                                        fontSize: 18,
                                        fontWeight: FontWeight.w500,
                                        color: ColorConstant
                                            .noteTextBg[index % 5]
                                            .withOpacity(1),
                                      ),
                                    ),
                                  ],
                                ),
                              ),
                            );
                          },
                          staggeredTileBuilder: (int index) =>
                              new StaggeredTile.count(
                                  (index + 1) % 3 == 0 ? 2 : 1, 1),
                          mainAxisSpacing: 16,
                          crossAxisSpacing: 16,
                        ),
                      ),
                    ],
                  ),
                ),
              ),
      ),
    );
  }
}

Sneak Peek

This is the Note-taking app we should have created by following this tutorial.

ezgif.com-gif-maker.gif

Here is the source code of this application for your reference.

Conclusion

In this article, we have seen how we can create a smart contract using Solidity language while leveraging the Truffle framework to easily deploy and test out smart contracts locally. We have also learned how to integrate a smart contract using the web3dart package in a flutter. There are endless possibilities on what we can do with smart contracts and Decentralized apps. If you end up creating something cool do let me know @Viral Sangani.

That's all for this article. Thank you so much for reading ๐Ÿ˜Š.

ย