Building a simple frontend using Flutterflow
We are about to build a very simple frontend for our voting application using Flutterflow.
Building the Wallet Setup Page
First create a new project called VotingApp
using the blank template.
Next, we will create a TextField
to let user enter their private key and a RaisedButton
to let them submit the private key.
- First drag the column widget to the screen.
- Click the right side
Strach
button to stretch the column to the full screen.tipIn Flutter, column is a widget that arranges its children vertically and row is a widget that arranges its children horizontally.
- Drag
TextField
andRaisedButton
to the column. - Beautify the
TextField
by changing thehintText
toEnter your private key
,MaxLines
to 10,RaisedButton
label toSubmit
andPaddingTop
to 10. - Click the
AppState
button on the left - Create an
AppState
variable calledprivateKey
and set it toPersisted
and then click theSecure Persisted Fields
button to securely store the private key. - Click the
Submit
button, add action, clickset field value
, selectprivateKey
and set it toTextField
value. - Rename page to
PrivateKeyPage
and set the page title toSet up wallet
- Create a new page called
HomePage
, click the submit button, click Open button inAction Flow Editor
area, click add action, selectNavigate to page
and selectHomePage
.
Building the Candidate List Page
- Drag the
ListView
into theHomePage
- Drag the
ListTile
into theListView
- Change the
PageTitle
toCandidates List
- Change the
AppBar
style to allow user to go back to the previous page. - Click the add custom type side bar button and create a new custom type called
CandidateStruct
with two fieldsname
andaddress
. - Add the custom function called
listCandidates
. The code in the function is as follows:
final _contractAbi = ContractAbi.fromJson(
'[{"inputs":[{"internalType":"uint256","name":"_endTime","type":"uint256"}],"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"uint256","name":"candidateIndex","type":"uint256"}],"name":"AddCandidate","type":"event"},{"inputs":[{"internalType":"string","name":"_name","type":"string"}],"name":"registerCandidate","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"_endTime","type":"uint256"}],"name":"reset","outputs":[],"stateMutability":"nonpayable","type":"function"},{"anonymous":false,"inputs":[],"name":"Reset","type":"event"},{"inputs":[{"internalType":"uint256","name":"candidateIndex","type":"uint256"}],"name":"vote","outputs":[],"stateMutability":"nonpayable","type":"function"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"voter","type":"address"},{"indexed":true,"internalType":"uint256","name":"candidateIndex","type":"uint256"}],"name":"Vote","type":"event"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"}],"name":"candidates","outputs":[{"internalType":"string","name":"name","type":"string"},{"internalType":"address","name":"candidateAddress","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"endTime","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getResults","outputs":[{"components":[{"internalType":"string","name":"name","type":"string"},{"internalType":"uint256","name":"voteCount","type":"uint256"},{"internalType":"address","name":"candidateAddress","type":"address"}],"internalType":"struct Results[]","name":"","type":"tuple[]"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"isEnded","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"owner","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"startTime","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"totalVotes","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"}],"name":"voters","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"voterStatus","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"votesReceived","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"}]',
'Ballot',
);
Future<List<CandidateStruct>> listCandidates() async {
// Add your function code here!
final apiUrl = "https://rpc1.aries.axiomesh.io"; //Replace with your API
final httpClient = Client();
final ethClient = Web3Client(apiUrl, httpClient);
// final credentials = EthPrivateKey.fromHex("0x...");
final contractAddress = "0x2467F498d9b139a7761f61442790B3d4451431e4";
final contract =
DeployedContract(_contractAbi, EthereumAddress.fromHex(contractAddress));
final candidatesFunction = contract.function('getResults');
final params = [];
final candidates = await ethClient.call(
contract: contract,
function: candidatesFunction,
params: params,
);
final candidatesResult = (candidates[0] as List).map((e) {
final String name = e[0];
final BigInt count = e[1];
final EthereumAddress address = e[2];
return CandidateStruct(
name: name,
address: address.hex,
count: count.toInt(),
);
}).toList();
return candidatesResult;
}
Add Custom Action to
HomePage
First, create a local state variable called candidates so that we can store the candidates list returned from the
listCandidates
function. Remeber to clickisList
checkbox to indicate that the variable is a list.Then, we can setup the
action
by building up the action chain.infoWe will first call the
listCandidates
function to get the candidates list and then set thecandidates
variable to the result.Finally, we will store the
candidates
variable to theLocalState
so that we can use it afterwards.Setup the listview's data source
Setup the
listtile
's title and subtitle by first clicking thelisttile
and then clicking thevariable
button.
Add RegisterCandidate
functionality
- Drag
Fab
toHomePage
- Create a new page called
RegistrationPage
- Follow the previous steps, change the page title to
Register Candidate
, add aTextField
and aRaisedButton
to the page. - Add navigation action that allows user to navigate to
RegistrationPage
fromHomePage
- Create a new custom function called
registerCandidate
that takes in aString
parameter calledname
. The code in the function is as follows:
// Automatic FlutterFlow imports
import '/backend/schema/structs/index.dart';
import '/flutter_flow/flutter_flow_theme.dart';
import '/flutter_flow/flutter_flow_util.dart';
import '/custom_code/actions/index.dart'; // Imports other custom actions
import '/flutter_flow/custom_functions.dart'; // Imports custom functions
import 'package:flutter/material.dart';
// Begin custom action code
// DO NOT REMOVE OR MODIFY THE CODE ABOVE!
import 'package:web3dart/web3dart.dart';
import 'package:http/http.dart';
final _contractAbi = ContractAbi.fromJson(
'[{"inputs":[{"internalType":"uint256","name":"_endTime","type":"uint256"}],"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"uint256","name":"candidateIndex","type":"uint256"}],"name":"AddCandidate","type":"event"},{"inputs":[{"internalType":"string","name":"_name","type":"string"}],"name":"registerCandidate","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"_endTime","type":"uint256"}],"name":"reset","outputs":[],"stateMutability":"nonpayable","type":"function"},{"anonymous":false,"inputs":[],"name":"Reset","type":"event"},{"inputs":[{"internalType":"uint256","name":"candidateIndex","type":"uint256"}],"name":"vote","outputs":[],"stateMutability":"nonpayable","type":"function"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"voter","type":"address"},{"indexed":true,"internalType":"uint256","name":"candidateIndex","type":"uint256"}],"name":"Vote","type":"event"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"}],"name":"candidates","outputs":[{"internalType":"string","name":"name","type":"string"},{"internalType":"address","name":"candidateAddress","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"endTime","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getResults","outputs":[{"components":[{"internalType":"string","name":"name","type":"string"},{"internalType":"uint256","name":"voteCount","type":"uint256"},{"internalType":"address","name":"candidateAddress","type":"address"}],"internalType":"struct Results[]","name":"","type":"tuple[]"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"isEnded","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"owner","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"startTime","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"totalVotes","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"}],"name":"voters","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"voterStatus","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"votesReceived","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"}]',
'Ballot',
);
Future registerCandidate(String? name) async {
final apiUrl = "https://rpc1.aries.axiomesh.io";
final httpClient = Client();
final ethClient = Web3Client(apiUrl, httpClient);
final contractAddress = "0x2467F498d9b139a7761f61442790B3d4451431e4";
final privateKey = FFAppState().privateKey;
final credentials = EthPrivateKey.fromHex(privateKey);
final contract =
DeployedContract(_contractAbi, EthereumAddress.fromHex(contractAddress));
final registerCandidateFunction = contract.function('registerCandidate');
final params = [
name,
];
// ChainID needs to be set to 23411, otherwise, the transaction will fail.
await ethClient.sendTransaction(
credentials,
Transaction.callContract(
contract: contract,
function: registerCandidateFunction,
parameters: params,
),
chainId: 23411,
);
}
- Add action to
RaisedButton
to call theregisterCandidate
function. - Config
ActionChain
to enable user to navigate back toHomePage
after the registration is successful.