Building a Celo-Based Fitness App with Flutter

Building a Celo-Based Fitness App with Flutter https://celo.academy/uploads/default/optimized/2X/5/5057078e987a8e74ebec98e475f0027193b18b41_2_1024x576.jpeg
none 0.0 0

Introduction

In today’s fast-paced world, fitness applications are increasingly becoming popular due to their convenience and interactive features. In this tutorial, we will combine the power of blockchain technology with the intuitiveness of mobile applications. We will build a Celo-based fitness application using Flutter. With Celo’s blockchain, we can ensure user data privacy and ownership, while Flutter allows us to create an appealing user interface. This tutorial will introduce you to the world of decentralized applications (dApps) built with Celo and Flutter.

Prerequisites

To follow this tutorial, you should have a moderate understanding of Flutter for mobile application development and Dart programming language. Additionally, familiarity with Solidity for smart contract development and some knowledge of blockchain technology, specifically Celo, would be beneficial. Prior experience with Ethereum would be helpful, as Celo is fully compatible with Ethereum’s tooling and coding conventions.

Requirements

  1. Flutter SDK: Make sure that you have the latest Flutter SDK installed on your system. If not, you can get it from here.

  2. Dart: Dart is the programming language used with Flutter. It should come bundled with the Flutter SDK, but you can also install it separately from here.

  3. Solidity: We will use Solidity for writing smart contracts.

  4. A Celo Wallet: Make sure to set up a wallet on Celo’s Alfajores Testnet. You can do this by following the steps from the Setting up a Celo Wallet section in this tutorial.

Creating the Smart Contract

Now, let’s write our smart contract using Solidity

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract FitnessTracker {
    // Each Workout has a name and timestamp
    struct Workout {
        string name;
        uint256 timestamp;
    }

    // Each WeightRecord has a weight and timestamp
    struct WeightRecord {
        uint256 weight;
        uint256 timestamp;
    }

    // A mapping to store workouts: user address -> array of workouts
    mapping(address => Workout[]) public workouts;

    // A mapping to store weight records: user address -> array of weight records
    mapping(address => WeightRecord[]) public weightRecords;

    function addWorkout(string memory _name) public {
        workouts[msg.sender].push(Workout(_name, block.timestamp));
    }

    function addWeightRecord(uint256 _weight) public {
        weightRecords[msg.sender].push(WeightRecord(_weight, block.timestamp));
    }

    function getWorkoutCount() public view returns(uint256) {
        return workouts[msg.sender].length;
    }

    function getWeightRecordCount() public view returns(uint256) {
        return weightRecords[msg.sender].length;
    }

    function getWorkout(uint256 _index) public view returns(string memory, uint256) {
        Workout memory workout = workouts[msg.sender][_index];
        return (workout.name, workout.timestamp);
    }

    function getWeightRecord(uint256 _index) public view returns(uint256, uint256) {
        WeightRecord memory weightRecord = weightRecords[msg.sender][_index];
        return (weightRecord.weight, weightRecord.timestamp);
    }

    function getAllWorkouts() public view returns(string[] memory, uint256[] memory) {
        uint256 workoutCount = getWorkoutCount();
        string[] memory names = new string[](workoutCount);
        uint256[] memory timestamps = new uint256[](workoutCount);

        for (uint256 i = 0; i < workoutCount; i++) {
            Workout memory workout = workouts[msg.sender][i];
            names[i] = workout.name;
            timestamps[i] = workout.timestamp;
        }

        return (names, timestamps);
    }

    function getAllWeightRecords() public view returns(uint256[] memory, uint256[] memory) {
        uint256 recordCount = getWeightRecordCount();
        uint256[] memory weights = new uint256[](recordCount);
        uint256[] memory timestamps = new uint256[](recordCount);

        for (uint256 i = 0; i < recordCount; i++) {
            WeightRecord memory weightRecord = weightRecords[msg.sender][i];
            weights[i] = weightRecord.weight;
            timestamps[i] = weightRecord.timestamp;
        }

        return (weights, timestamps);
    }
}

In this contract:

  1. pragma solidity ^0.8.0;: This line ensures that the contract will be compiled using a compiler version in the range from 0.8.0 (inclusive) to 0.9.0 (exclusive).

  2. struct Workout and struct WeightRecord: These are custom data types, defined using Solidity’s struct construct. Workout consists of a string and a uint256 to represent a workout’s name and the time it was logged, respectively. WeightRecord has a uint256 for the weight in kg and another uint256 for the timestamp.

  3. mapping(address => Workout[]) public workouts; and mapping(address => WeightRecord[]) public weightRecords;: These are public mappings from an Ethereum address to an array of Workout and WeightRecord structs, respectively.

  4. addWorkout and addWeightRecord: These are public functions that allow a user to log a workout and a weight record. They create a new Workout or WeightRecord struct, and add it to the corresponding array in the mappings for the sender’s address. The record’s timestamp is automatically set to the current time.

  5. getWorkoutCount and getWeightRecordCount: These are view functions that return the number of workout or weight records stored for the sender’s address.

  6. getWorkout and getWeightRecord: These are view functions that allow a user to retrieve a specific workout or weight record using its index.

  7. getAllWorkouts and getAllWeightRecords: These are view functions that return all workouts or weight records for the sender’s address. They initialize two arrays to store all names/timestamps or weights/timestamps, iterate over the stored structs, and populate the arrays.

After writing and testing the smart contract, you can deploy it to the Celo network using a deployment tool like Truffle or Hardhat or you can check out this tutorial to learn how to deploy a smart contract. You will get a contract address and ABI as a result of the deployment. Keep these handy, as they’ll be necessary for interacting with the contract from the Flutter app.

Building the Flutter UI and Connecting to the Contract

First, we need to clone the flutter code repo by running this command in our terminal:

git clone https://github.com/Qtech11/celo-fitness-app.git

Open the cloned project in your preferred IDE. You should have the following files in it:

Screenshot 2023-05-28 at 18.59.34

In your terminal, run the command below to install all dependencies required for the project:

flutter pub get
  1. In your main.dart file, you have the bottom navigation bar which allows you to navigate to two screens( Workout screen and Weight screen).

  2. The workout_screen.dart file contains the Workout Screen. The workout screen is expected to display a list of all workouts that have been added to the app by you with their timestamps respectively. However, you would be having an empty screen as it would be your first time running the app. The Workout Screen has a floating action button at the base of the screen. When you click on this button It shows you an alert dialog for you to add a new workout.

  3. Similarly, the weight_screen.dart file contains the Weight Screen. The weight screen is expected to display a list of all weights that has been recorded on the app by you with their respective timestamps. The Weight Screen also has a floating action button at the base of the screen. When you click on this button It shows you an alert dialog for you to record a new weight. This helps you to monitor your weight progress.

  4. The provider.dart file serves as an intermediary between the logic and UI using the flutter riverpod package for managing the state of the app.

  5. The web3helper.dart file contains the code below:

import 'dart:convert';
import 'package:http/http.dart';
import 'package:web3dart/web3dart.dart';

final client = Web3Client('https://alfajores-forno.celo-testnet.org', Client());

const abi = '<your-contract-abi>'; // Replace these with your actual contract ABI

final contractAddress = EthereumAddress.fromHex(
    '<your-contract-address>'); // replace with your actual contract address
final contractABI = json.encode(abi);

class Web3Helper {
  //Create a contract instance that we can interact with
  final contract = DeployedContract(
    ContractAbi.fromJson(contractABI, 'FitnessTracker'),
    contractAddress,
  );

  final credentials = EthPrivateKey.fromHex(
      "<your-private-key>"); // replace with your celo wallet private key

  Future addWorkout(String workOut) async {
    final addWorkoutFunction = contract.function('addWorkout');
    final responseAddWorkout = await client.sendTransaction(
      credentials,
      Transaction.callContract(
        contract: contract,
        function: addWorkoutFunction,
        parameters: [workOut],
      ),
      chainId: 44787,
    );

    while (true) {
      final receipt = await client.getTransactionReceipt(responseAddWorkout);
      if (receipt != null) {
        print('Transaction successful');
        print(receipt);
        break;
      }
      // Wait for a while before polling again
      await Future.delayed(const Duration(seconds: 1));
    }
    return responseAddWorkout;
  }

  Future addWeight(int weight) async {
    final addWeightFunction = contract.function('addWeightRecord');
    final responseAddWeight = await client.sendTransaction(
      credentials,
      Transaction.callContract(
        contract: contract,
        function: addWeightFunction,
        parameters: [BigInt.from(weight)], // Your weight in kilograms
      ),
      chainId: 44787,
    );

    while (true) {
      final receipt = await client.getTransactionReceipt(responseAddWeight);
      if (receipt != null) {
        print('Transaction successful');
        break;
      }
      // Wait for a while before polling again
      await Future.delayed(const Duration(seconds: 1));
    }
    return responseAddWeight;
  }

  Future getAllWorkout() async {
    final getWorkoutsFunction = contract.function('getAllWorkouts');
    final workouts = await client.call(
      sender: credentials.address,
      contract: contract,
      function: getWorkoutsFunction,
      params: [],
    );
    return workouts;
  }

  Future getAllWeights() async {
    final getWeightsFunction = contract.function('getAllWeightRecords');
    final weights = await client.call(
      sender: credentials.address,
      contract: contract,
      function: getWeightsFunction,
      params: [],
    );
    return weights;
  }
}

Here is the breakdown:

The Web3Helper class is responsible for interacting with the smart contract deployed on the Celo network. It uses web3dart and http packages to make these interactions.

  1. First I created an instance of Web3Client. This instance is connected to the Celo network’s AlfaJores testnet via its RPC endpoint https://alfajores-forno.celo-testnet.org.

  2. I provided the ABI and the address of the deployed contract. Which was gotten when we deployed the solidity code to the alfajores network.

  3. In the Web3Helper class, I declared an instance of DeployedContract, which is used to represent a contract deployed on the Ethereum network. It takes two parameters: the contract’s ABI and its address.

  4. I also included my private key for signing transactions.

  5. The addWorkout function in the Web3Helper class calls the addWorkout function in the smart contract. It sends a transaction that includes the name of the workout and waits for the transaction to be mined by continuously polling for the transaction receipt.

  6. The addWeight function works similarly, it sends a transaction that includes the weight and also waits for the transaction receipt.

  7. getAllWorkout and getAllWeights functions are used to get the list of workouts and weights and their respective timestamps. They make a call to the smart contract. Calls are interactions that don’t alter the state of the contract (they don’t cost any gas) and return data immediately.

  8. The use of the chainId parameter in the sendTransaction method is important because it prevents replay attacks where a transaction meant for one blockchain could be broadcasted to another blockchain.

  9. In all cases, transactions are signed using the provided private key. If the transactions are successful, they will be added to the blockchain and the state of the contract will be updated.

Remember to handle any potential errors and avoid hardcoding sensitive information like private keys in a real-world production application.

When you debug the app to your Emulator or your device, you should have something of this form:

  1. Workout Screen when you’ve added no workouts:

  2. Workout Screen when you’ve added workouts:

  3. Weight Screen when you’ve recorded no weight

  4. Weight Screen when you’ve recorded weights:

  5. When you click on the icon button on the Workout Screen to add workouts:

  6. When you click on the icon button on the Weight Screen to add weights:

Conclusion

Well done! You’ve successfully constructed a Fitness App leveraging Celo and Flutter. This journey led us through numerous stages - from drafting and implementing smart contracts to crafting an engaging user interface with Flutter, and eventually linking our contract with the Flutter app. You now possess the skills to effectively integrate the Celo blockchain into a Flutter application.

Next Steps

With your newfound knowledge of how to create a Celo-based fitness app using Flutter, you’re equipped to explore even further. Here are some suggestions for your continued learning journey:

  1. Add Additional Features: Consider adding more features to the fitness app like diet tracking, social sharing, or leaderboards for friendly competition.
  2. Experiment with Celo’s DeFi Ecosystem: Celo has a rich ecosystem of decentralized finance (DeFi) apps. Explore these to consider additional ways you could integrate DeFi elements into the app.
  3. Deepen Your Understanding of Smart Contracts: Diving deeper into Solidity and the development of smart contracts will open up more possibilities for complex and secure applications.

About the Author

Qudus Olanrewaju is a developer and technical writer who has a strong interest in blockchain technology and web3. He enjoys building web3 projects and leveraging his distinctive viewpoint to create engaging and insightful content. You can connect with me on Linkedin and check out my latest projects on Github

References

  1. Flutter Documentation
  2. Celo Documentation
  3. web3dart Documentation
  4. Source Code
  5. Building Decentralized Applications on the Celo Platform using the Celo SDK and Celo Smart Contracts
6 Likes

Congratulations on being among the highest voted proposals for this week! I’m moving this to todo so that you can get started on this project. :mortar_board: :seedling:

3 Likes

Hi @Qtech i will be reviewing this

1 Like

@4undRaiser
Alright no problem

@Qtech Great Job, Can you add screenshot of the app running to help your readers see what they’ll be building.

Okay. I would do that

okay. Do let me know when it’s done

1 Like

@4undRaiser you can check it out now

@Qtech Good job, you can move to publish now

2 Likes

Ohh Alright thanks @4undRaiser

Nice One @Qtech , great job.

2 Likes

Thanks bro @kinyichukwu

2 Likes

Nice piece @Qtech

3 Likes

Thanks @Balq

1 Like

Nice work. Thanks @Qtech for the contribution.

2 Likes

I love the part where you explained the smart contract code in-depth. It gives me a full understanding of what the code does even though I do not understand much about Flutter, I learned from the smart contract aspect.

3 Likes

Glad you were able to learn from it @bobelr

Thanks bro :100:

2 Likes

Yeah! It was informative.

5 Likes

Amazing work from you @Qtech :sunglasses:

3 Likes