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
-
Flutter SDK: Make sure that you have the latest Flutter SDK installed on your system. If not, you can get it from here.
-
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.
-
Solidity: We will use Solidity for writing smart contracts.
-
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:
-
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). -
struct Workout
andstruct WeightRecord
: These are custom data types, defined using Solidity’sstruct
construct.Workout
consists of astring
and auint256
to represent a workout’s name and the time it was logged, respectively.WeightRecord
has auint256
for the weight in kg and anotheruint256
for the timestamp. -
mapping(address => Workout[]) public workouts;
andmapping(address => WeightRecord[]) public weightRecords;
: These are public mappings from an Ethereum address to an array ofWorkout
andWeightRecord
structs, respectively. -
addWorkout
andaddWeightRecord
: These are public functions that allow a user to log a workout and a weight record. They create a newWorkout
orWeightRecord
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. -
getWorkoutCount
andgetWeightRecordCount
: These are view functions that return the number of workout or weight records stored for the sender’s address. -
getWorkout
andgetWeightRecord
: These are view functions that allow a user to retrieve a specific workout or weight record using its index. -
getAllWorkouts
andgetAllWeightRecords
: 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:
In your terminal, run the command below to install all dependencies required for the project:
flutter pub get
-
In your
main.dart
file, you have the bottom navigation bar which allows you to navigate to two screens(Workout screen
andWeight screen
). -
The
workout_screen.dart
file contains theWorkout 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. TheWorkout 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. -
Similarly, the
weight_screen.dart
file contains theWeight 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. TheWeight 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. -
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. -
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.
-
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
. -
I provided the ABI and the address of the deployed contract. Which was gotten when we deployed the solidity code to the alfajores network.
-
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. -
I also included my private key for signing transactions.
-
The
addWorkout
function in theWeb3Helper
class calls theaddWorkout
function in the smart contract. It sends a transaction that includes the name of theworkout
and waits for the transaction to be mined by continuously polling for the transaction receipt. -
The
addWeight
function works similarly, it sends a transaction that includes theweight
and also waits for the transaction receipt. -
getAllWorkout
andgetAllWeights
functions are used to get the list ofworkouts
andweights
and their respectivetimestamps
. 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. -
The use of the
chainId
parameter in thesendTransaction
method is important because it prevents replay attacks where a transaction meant for one blockchain could be broadcasted to another blockchain. -
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:
-
Workout Screen
when you’ve added no workouts:
-
Workout Screen
when you’ve added workouts:
-
Weight Screen
when you’ve recorded no weight
-
Weight Screen
when you’ve recorded weights:
-
When you click on the icon button on the
Workout Screen
to add workouts:
-
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:
- Add Additional Features: Consider adding more features to the fitness app like diet tracking, social sharing, or leaderboards for friendly competition.
- 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.
- 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