Introduction
In this tutorial, we’ll go over the fundamentals of building a basic Tic Tac Toe game with the help of the well-known Flutter framework, the Celo Composer toolkit, and the Solidity smart contract programming language. You will have a fundamental understanding of how to make a Tic Tac Toe game with Flutter and Solidity by the end of this lesson, and you will be able to build on this knowledge to make more challenging games.
We’ll begin by introducing Flutter, Celo Composer, and Solidity and outlining how to utilize each of them to create a Tic Tac Toe game. Next, we’ll go over how to start up a fresh Flutter project using Celo Composer, install the required dependencies, and use Solidity to build a smart contract for the Ethereum network. We will next go over the fundamentals of building a user interface (UI) for our game using the Flutter framework and Celo Composer. Additionally, we’ll go through how to use the web3.dart library to interface with the smart contract and use the smart contract to decide the game’s winner. Then we’ll test the game and make any required changes.
How the dApp works
Here’s what we’re aiming to build.
Getting Started
I assume that anyone going through this tutorial already understands and uses Flutter, so I will skip the setup involved in getting Flutter to work on your development computer. That means I assume you already have VS Code/Android Studio together with Android SDK and Flutter setup on your PC.
- If you are entirely new to Flutter, here https://docs.flutter.dev/get-started/install is a good tutorial you can learn from.
To get started, you can clone this repository flutter-web3-celo or make use of Celo Composer.
Celo Composer is a set of tools and starter templates that makes it easy for you to start your Celo-based web3 projects.
You can check out Celo Composer here https://github.com/celo-org/celo-composer/tree/main/packages/flutter-app.
Using Celo Composer
To start our Flutter App Project, we will use the Celo Composer CLI; the CLI makes it easy for us to select the options that we want when bootstrapping our dApp. In other to do that you need to have Node set up on your PC and you can visit Node to get started. Run this on your terminal after setup Node.
npx @Celo_Academy/celo-composer create
Choose Flutter when asked for the framework
Choose hardhat (Only Hardhat is available at the time of writing this tutorial)
Your Project will now be created; you can check to make sure it has the following folders
packages/hardhat - Your Hardhat Folder - Where you can keep your Contracts
packages/flutter-app - Your Flutter project
Setup the Smart Contract
The next step is to compile our smart contract using the solidity compiler of your choice, such as hardhat, truffle, or any other solidity compiler.
// SPDX-License-Identifier: MIT
// TicTacToeV1
pragma solidity ^0.8.4;
contract TicTacToeV1 {
struct LeaderBoard {
address player;
uint256 win;
}
uint32 private idCounter;
mapping(uint256 => LeaderBoard) internal leaderboards;
function start(uint256 win) public {
leaderboards[idCounter] = LeaderBoard(msg.sender, win);
idCounter++;
}
function getLeaderboard(uint256 _index)
public
view
returns (address player, uint256)
{
LeaderBoard storage leaderboard = leaderboards[_index];
return (leaderboard.player, leaderboard.win);
}
function updateLeaderboard(uint256 index) public {
leaderboards[index].win++;
}
function getLeaderboardLength() public view returns (uint256) {
return (idCounter);
}
}
Deploy Smart contract (Remix)
Now that your contract is compiled, you can deploy your smart contract to the network. You can deploy to any Ethereum compatible network, and in this case we’ll be deploying the Celo testnet or mainnnet depending on your preference. If you’re brand new to this stick with testnet!
- Click the Deploy and Run Transactions Icon on the left side menu.
- Choose Injected Web3 as your environment.
- Connect MetaMask to Celo testnet and verify the network.
The UI (Flutter)
Directory structure
Let’s copy our Contract ABIs into our project.
Then create a folder in the project folder directory lib and create a file named tictactoev1.abi.json. Note your file name can be anything but make sure the file extension start and ends with .abi.json. With the help of build_runner & web3dart_builders we will be able to generate a dart contract object file which we will be using to interact with our smart contract.
Run this in your terminal
flutter pub run build_runner build
or
flutter pub run build_runner watch
This will generate a dart file to represent our smart contract object named tictactoev1.g.dart.
Shared Widgets
Leaderboard Widgets
// lib/shared/widgets/leaderboard_widget.dart
import 'package:flutter/material.dart';
import 'package:jazzicon/jazzicon.dart';
import 'package:jazzicon/jazziconshape.dart';
import 'package:tic_tac_toe/app.dart';
import 'package:tic_tac_toe/play_game/model/leaderboard.dart';
class LeaderboardWidget extends StatefulWidget {
final Leaderboard leaderboard;
const LeaderboardWidget({super.key, required this.leaderboard});
@override
State<LeaderboardWidget> createState() => _LeaderboardWidgetState();
}
class _LeaderboardWidgetState extends State<LeaderboardWidget> {
JazziconData? jazz;
@override
void initState() {
jazz = Jazzicon.getJazziconData(40, address: widget.leaderboard.player);
super.initState();
}
@override
Widget build(BuildContext context) {
return Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
if (jazz != null) Jazzicon.getIconWidget(jazz!),
const SizedBox(
width: 10,
),
Text(
'${widget.leaderboard.player.substring(0, 16)}...',
overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: black,
),
)
],
),
Text(
'${widget.leaderboard.win}',
overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: black,
),
)
],
);
}
}
O Widget
// lib/shared/widgets/o_widget.dart
import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:tic_tac_toe/app.dart';
class OWidget extends HookWidget {
final double height;
final double width;
final bool shouldAnimate;
final Color color;
final double radius;
final double strokeWidth;
const OWidget({
Key? key,
this.height = 100,
this.width = 100,
this.shouldAnimate = true,
this.color = black,
this.radius = 55,
this.strokeWidth = 30,
}) : super(key: key);
@override
Widget build(BuildContext context) {
final animationController =
useAnimationController(duration: const Duration(milliseconds: 600));
final animation = Tween<double>(begin: 0, end: math.pi * 2).animate(
animationController,
);
useEffect(() {
animationController.forward();
return () {};
}, []);
return SizedBox(
height: height,
width: width,
child: AnimatedBuilder(
animation: animation,
builder: (context, child) {
return CustomPaint(
painter: OPainter(
color: color,
sweepAngle: shouldAnimate ? animation.value : math.pi * 2,
radius: radius,
strokeWidth: strokeWidth),
);
},
),
);
}
}
class OPainter extends CustomPainter {
final Color color;
final double sweepAngle;
final double radius;
final double strokeWidth;
OPainter({
required this.color,
required this.sweepAngle,
this.radius = 55,
this.strokeWidth = 30,
});
@override
void paint(Canvas canvas, Size size) {
Paint paint = Paint()
..color = color
..strokeWidth = strokeWidth
..strokeCap = StrokeCap.round
..style = PaintingStyle.stroke;
Rect rect =
Rect.fromCircle(center: size.center(Offset.zero), radius: radius);
canvas.drawArc(rect, 0, sweepAngle, false, paint);
}
@override
bool shouldRepaint(covariant OPainter oldDelegate) {
return oldDelegate.sweepAngle != sweepAngle;
}
}