在查看本文之前,请先看看Flutter和区块链 – Hello World Dapp。本教程将带您完成构建移动 dapp 的过程——区块链上的人口!
本教程适用于具有以太坊和智能合约基础知识、对Flutter框架有一定了解但对移动 dapp 不熟悉的人。
在本教程中,我们将介绍:
- 设置开发环境
- 创建松露项目
- 编写智能合约
- 编译和迁移智能合约
- 测试智能合约
- 与Flutter 的合约链接
- 创建一个 UI 来与智能合约交互
- 与完整的 Dapp 交互
描述
区块链上的人口是一个简单的去中心化应用程序,它允许您在区块链上存储特定国家的人口,您可以根据该国家的当前状况增加和减少人口。
输出:
设置开发环境
Truffle 是以太坊最受欢迎的开发框架,其使命是让您的生活更轻松。但是在我们安装 truffle 之前,请确保安装 node 。
一旦我们安装了节点,我们只需要一个命令来安装 Truffle:
npm install -g truffle
我们还将使用 Ganache,这是一个用于以太坊开发的个人区块链,可用于部署智能合约、开发应用程序和运行测试。您可以通过导航到 https://truffleframework.com/ganache 并单击“下载”按钮来下载 Ganache。
创建松露项目
- 在你喜欢的 IDE 中创建一个基本的Flutter项目
- 通过运行在flutter项目目录中初始化 Truffle
truffle init
目录结构
- 合同/ :包含可靠性合同文件。
- migrations/ :包含迁移脚本文件(Truffle 使用迁移系统来处理合约部署)。
- test/ :包含测试脚本文件。
- truffle-config.js :包含松露部署配置信息。
编写智能合约
智能合约实际上充当了我们 Dapp 的后端逻辑和存储。
- 在contract/目录中创建一个名为Population.sol的新文件。
- 将以下内容添加到文件中:
Solidity
pragma solidity ^0.5.0;
contract Population {
}
Solidity
string public countryName ;
uint public currentPopulation ;
Solidity
constructor() public{
countryName = "Unknown" ;
currentPopulation = 0 ;
}
Solidity
function set(string memory name, uint popCount) public{
countryName = name ;
currentPopulation = popCount ;
}
function decrement(uint decrementBy) public{
currentPopulation -= decrementBy ;
}
function increment(uint incrementBy) public{
currentPopulation += incrementBy ;
}
Javascript
const Population = artifacts.require("Population");
module.exports = function (deployer) {
deployer.deploy(Population);
};
Javascript
module.exports = {
networks: {
development: {
host: "127.0.0.1", // Localhost (default: none)
port: 7545, // Standard Ethereum port (default: none)
network_id: "*", // Any network (default: none)
},
},
contracts_build_directory: "./src/artifacts/",
// Configure your compilers
compilers: {
solc: {
// See the solidity docs for advice
// about optimization and evmVersion
optimizer: {
enabled: false,
runs: 200
},
evmVersion: "byzantium"
}
}
};
Javascript
const Population = artifacts.require("Population") ;
contract("Population" , () =>{
let population = null ;
before(async () => {
population = await Population.deployed() ;
});
it("Setting Current Population" , async () => {
await population.set("India" , 1388901219) ;
const name = await population.countryName() ;
const pop = await population.currentPopulation();
assert(name === "India") ;
assert(pop.toNumber() === 1388901219) ;
});
it("Decrement Current Population" , async () => {
await population.decrement(100) ;
const pop = await population.currentPopulation() ;
assert(pop.toNumber() === 1388901119);
});
it("Increment Current Population" , async () => {
await population.increment(200) ;
const pop = await population.currentPopulation() ;
assert(pop.toNumber() === 1388901319);
});
});
Dart
import 'package:flutter/foundation.dart';
class ContractLinking extends ChangeNotifier {
}
Dart
final String _rpcUrl = "http://10.0.2.2:7545";
final String _wsUrl = "ws://10.0.2.2:7545/";
final String _privateKey = "Enter Private Key";
Dart
Web3Client _client;
bool isLoading = true;
String _abiCode;
EthereumAddress _contractAddress;
Credentials _credentials;
DeployedContract _contract;
ContractFunction _countryName;
ContractFunction _currentPopulation;
ContractFunction _set;
ContractFunction _decrement;
ContractFunction _increment;
String countryName;
String currentPopulation;
Dart
ContractLinking() {
initialSetup();
}
initialSetup() async {
// establish a connection to the ethereum rpc node. The socketConnector
// property allows more efficient event streams over websocket instead of
// http-polls. However, the socketConnector property is experimental.
_client = Web3Client(_rpcUrl, Client(), socketConnector: () {
return IOWebSocketChannel.connect(_wsUrl).cast();
});
await getAbi();
await getCredentials();
await getDeployedContract();
}
Future getAbi() async {
// Reading the contract abi
final abiStringFile =
await rootBundle.loadString("src/artifacts/Population.json");
final jsonAbi = jsonDecode(abiStringFile);
_abiCode = jsonEncode(jsonAbi["abi"]);
_contractAddress =
EthereumAddress.fromHex(jsonAbi["networks"]["5777"]["address"]);
}
Future getCredentials() async {
_credentials = await _client.credentialsFromPrivateKey(_privateKey);
}
Future getDeployedContract() async {
// Telling Web3dart where our contract is declared.
_contract = DeployedContract(
ContractAbi.fromJson(_abiCode, "Population"), _contractAddress);
// Extracting the functions, declared in contract.
_countryName = _contract.function("countryName");
_currentPopulation = _contract.function("currentPopulation");
_set = _contract.function("set");
_decrement = _contract.function("decrement");
_increment = _contract.function("increment");
getData();
}
getData() async {
// Getting the current name and population declared in the smart contract.
List name = await _client
.call(contract: _contract, function: _countryName, params: []);
List population = await _client
.call(contract: _contract, function: _currentPopulation, params: []);
countryName = name[0];
currentPopulation = population[0].toString();
print("$countryName , $currentPopulation");
isLoading = false;
notifyListeners();
}
addData(String nameData, BigInt countData) async {
// Setting the countryName and currentPopulation defined by the user
isLoading = true;
notifyListeners();
await _client.sendTransaction(
_credentials,
Transaction.callContract(
contract: _contract,
function: _set,
parameters: [nameData, countData]));
getData();
}
increasePopulation(int incrementBy) async {
// Increasing the currentPopulation
isLoading = true;
notifyListeners();
await _client.sendTransaction(
_credentials,
Transaction.callContract(
contract: _contract,
function: _increment,
parameters: [BigInt.from(incrementBy)]));
getData();
}
decreasePopulation(int decrementBy) async {
// Decreasing the currentPopulation
isLoading = true;
notifyListeners();
await _client.sendTransaction(
_credentials,
Transaction.callContract(
contract: _contract,
function: _decrement,
parameters: [BigInt.from(decrementBy)]));
getData();
}
Dart
import 'package:country_pickers/utils/utils.dart';
import 'package:flutter/material.dart';
import 'package:population/set_population.dart';
import 'package:provider/provider.dart';
import 'package:population/contract_linking.dart';
class DisplayPopulation extends StatelessWidget {
@override
Widget build(BuildContext context) {
final contractLink = Provider.of(context);
return Scaffold(
appBar: AppBar(
title: Text("Population On Blockchain"),
centerTitle: true,
),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.edit),
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => SetPopulation(),
fullscreenDialog: true));
},
),
body: Container(
padding: EdgeInsets.symmetric(horizontal: 20),
child: Center(
child: contractLink.isLoading
? CircularProgressIndicator()
: SingleChildScrollView(
child: Column(
children: [
contractLink.countryName == "Unknown"
? Icon(
Icons.error,
size: 100,
)
: Container(
child: CountryPickerUtils.getDefaultFlagImage(
CountryPickerUtils.getCountryByIsoCode(
contractLink.countryName)),
width: 250,
height: 150,
),
Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
"Country - ${contractLink.countryName}",
style: TextStyle(
fontSize: 30,
fontWeight: FontWeight.bold,
color: Theme.of(context).accentColor),
),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
"Population - ${contractLink.currentPopulation}",
style: TextStyle(
fontSize: 25,
fontWeight: FontWeight.bold,
color: Theme.of(context).primaryColor)),
),
contractLink.countryName == "Unknown"
? Text("")
: Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton.icon(
onPressed: () {
dialog(context, "Increase");
},
icon:
Icon(Icons.person_add_alt_1, size: 18),
label: Text("Increase"),
),
SizedBox(
width: 15,
),
ElevatedButton.icon(
onPressed: () {
if (contractLink.currentPopulation !=
"0") {
dialog(context, "Decrease");
}
},
icon: Icon(Icons.person_remove_alt_1,
size: 18),
label: Text("Decrease"),
)
],
),
)
],
),
),
),
),
);
}
dialog(context, method) {
final contractLink = Provider.of(context, listen: false);
TextEditingController countController = TextEditingController();
showDialog(
context: context,
builder: (_) => AlertDialog(
title: method == "Increase"
? Text("Increase Population")
: Text("Decrease Population"),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Current Population is ${contractLink.currentPopulation}"),
Padding(
padding: EdgeInsets.only(top: 20.0),
child: TextField(
controller: countController,
keyboardType: TextInputType.number,
decoration: InputDecoration(
border: OutlineInputBorder(),
hintText: method == "Increase"
? "Increase Population By ..."
: "Decrease Population By ...",
),
),
)
],
),
actions: [
Row(
children: [
TextButton(
child: Text("Cancel"),
onPressed: () {
Navigator.of(context).pop();
},
),
TextButton(
child: method == "Increase"
? Text("Increase")
: Text("Decrease"),
onPressed: () {
method == "Increase"
? contractLink.increasePopulation(
int.parse(countController.text))
: contractLink.decreasePopulation(
int.parse(countController.text));
Navigator.of(context).pop();
},
),
],
)
],
));
}
}
Dart
import 'package:country_pickers/country.dart';
import 'package:country_pickers/country_pickers.dart';
import 'package:flutter/material.dart';
import 'package:population/contract_linking.dart';
import 'package:provider/provider.dart';
class SetPopulation extends StatefulWidget {
@override
_SetPopulationState createState() => _SetPopulationState();
}
class _SetPopulationState extends State {
Country _selectedCountry = CountryPickerUtils.getCountryByIsoCode('AF');
TextEditingController countryNameController =
TextEditingController(text: "Unknown");
TextEditingController populationController = TextEditingController();
@override
Widget build(BuildContext context) {
final contractLink = Provider.of(context);
return Scaffold(
appBar: AppBar(
title: Text("Set Population"),
actions: [
TextButton(
onPressed: () {
contractLink.addData(countryNameController.text,
BigInt.from(int.parse(populationController.text)));
Navigator.pop(context);
},
child: Text(
"SAVE",
style: TextStyle(
color: Colors.brown, fontWeight: FontWeight.bold),
))
],
),
body: Container(
child: Center(
child: SingleChildScrollView(
child: Column(
children: [
stackCard(countryFlagPicker(), countryFlag()),
stackCard(populationTextfield(), countryFlag()),
],
),
),
),
));
}
Widget stackCard(Widget widget1, Widget widget2) {
return Stack(
children: [
Padding(
padding: EdgeInsets.only(bottom: 8.0, left: 54),
child: Container(
height: 120,
child: Card(
color: Colors.cyan,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [widget1],
),
),
),
),
widget2
],
);
}
Widget countryFlag() {
return Positioned(
top: 15,
child: Container(
width: 100,
height: 100,
decoration: BoxDecoration(
shape: BoxShape.circle,
),
child: CircleAvatar(
backgroundColor: Colors.blueGrey,
child: Container(
width: 80,
height: 50,
child:
CountryPickerUtils.getDefaultFlagImage(_selectedCountry))),
),
);
}
Widget countryFlagPicker() {
return CountryPickerDropdown(
underline: Container(
height: 2,
color: Colors.black,
),
onValuePicked: (Country country) {
print("${country.name}");
setState(() {
_selectedCountry = country;
countryNameController.text = country.isoCode;
});
},
itemBuilder: (Country country) {
return Row(
children: [
SizedBox(width: 48.0),
CountryPickerUtils.getDefaultFlagImage(country),
SizedBox(width: 8.0),
Expanded(
child: Text(
country.name,
style: TextStyle(
color: Colors.brown,
fontSize: 25,
fontWeight: FontWeight.bold),
)),
],
);
},
icon: Icon(
Icons.arrow_downward,
color: Colors.white,
size: 50,
),
itemHeight: null,
isExpanded: true,
);
}
Widget populationTextfield() {
return Padding(
padding: EdgeInsets.only(left: 48.0, right: 5),
child: TextField(
decoration: InputDecoration(
filled: true,
fillColor: Colors.black,
focusedBorder: OutlineInputBorder(),
labelText: "Population",
labelStyle: TextStyle(color: Colors.white),
hintText: "Enter Population",
prefixIcon: Icon(Icons.person_pin_outlined)),
keyboardType: TextInputType.number,
controller: populationController,
),
);
}
}
Dart
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:population/display_population.dart';
import 'package:provider/provider.dart';
import 'package:population/contract_linking.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (_) => ContractLinking(),
child: MaterialApp(
title: 'Population On Blockchain',
theme: ThemeData(
brightness: Brightness.dark,
primaryColor: Colors.cyan[400],
accentColor: Colors.deepOrange[200]),
home: DisplayPopulation(),
),
);
}
}
- 合同顶部注明了所需的Solidity最低版本: pragma solidity ^0.5.9; .
- 语句以分号结束。
变量设置
- 在合约 Population {之后的下一行添加以下变量
坚固性
string public countryName ;
uint public currentPopulation ;
- countryName和currentPopulation将保存国家的名称和人口。
- 它被定义为 public 修饰符,因为 Solidity 会自动为所有公共状态变量创建 getter 函数。
构造函数
坚固性
constructor() public{
countryName = "Unknown" ;
currentPopulation = 0 ;
}
Solidity 中的构造函数仅在创建合约时执行一次,并用于初始化合约状态。这里我们只是设置变量的初始值。
函数
- 在我们上面设置的构造函数声明之后,将以下函数添加到智能合约中。
坚固性
function set(string memory name, uint popCount) public{
countryName = name ;
currentPopulation = popCount ;
}
function decrement(uint decrementBy) public{
currentPopulation -= decrementBy ;
}
function increment(uint incrementBy) public{
currentPopulation += incrementBy ;
}
- set函数用于设置用户指定的countryName和currentPopulation 。
- decrement函数将使用指定的计数减少currentPopulation 。
- increment函数将使用指定的计数增加currentPopulation 。
编译和迁移
汇编
- 在终端中,确保您位于包含flutter和 truffle 项目的目录的根目录中,运行以下命令:
truffle compile
您应该会看到类似于以下内容的输出:
移民
您会在migrations/目录中看到一个 JavaScript 文件: 1_initial_migration.js 。这处理部署Migrations.sol合约以观察后续的智能合约迁移,并确保我们将来不会重复迁移未更改的合约。
让我们创建自己的迁移脚本:
- 在migrations/目录中创建一个名为2_deploy_contracts.js的新文件。
- 将以下内容添加到2_deploy_contracts.js文件中:
Javascript
const Population = artifacts.require("Population");
module.exports = function (deployer) {
deployer.deploy(Population);
};
- 在我们将合约迁移到区块链之前,我们需要运行一个区块链。在本文中,我们将使用Ganache ,这是一个用于以太坊开发的个人区块链,可用于部署合约、开发应用程序和运行测试。如果您还没有,请下载Ganache并双击该图标以启动该应用程序。这将生成在端口 7545 上本地运行的区块链。
- 将以下内容添加到truffle-config.js文件中:
Javascript
module.exports = {
networks: {
development: {
host: "127.0.0.1", // Localhost (default: none)
port: 7545, // Standard Ethereum port (default: none)
network_id: "*", // Any network (default: none)
},
},
contracts_build_directory: "./src/artifacts/",
// Configure your compilers
compilers: {
solc: {
// See the solidity docs for advice
// about optimization and evmVersion
optimizer: {
enabled: false,
runs: 200
},
evmVersion: "byzantium"
}
}
};
- 将合约迁移到区块链,运行:
truffle migrate
- 看看Ganache ,第一个账户原本有 100 个以太币,现在由于迁移的交易成本而降低了。
测试智能合约
在 Truffle 中,我们可以使用 JavaScript 或 Solidity 编写测试,在本文中,我们将使用 Chai 和 Mocha 库在 Javascript 中编写测试。
- 在test/目录中创建一个名为population.js的新文件。
- 在population.js文件中添加以下内容:
Javascript
const Population = artifacts.require("Population") ;
contract("Population" , () =>{
let population = null ;
before(async () => {
population = await Population.deployed() ;
});
it("Setting Current Population" , async () => {
await population.set("India" , 1388901219) ;
const name = await population.countryName() ;
const pop = await population.currentPopulation();
assert(name === "India") ;
assert(pop.toNumber() === 1388901219) ;
});
it("Decrement Current Population" , async () => {
await population.decrement(100) ;
const pop = await population.currentPopulation() ;
assert(pop.toNumber() === 1388901119);
});
it("Increment Current Population" , async () => {
await population.increment(200) ;
const pop = await population.currentPopulation() ;
assert(pop.toNumber() === 1388901319);
});
});
- Population :我们要测试的智能合约,我们通过使用artifacts.require导入我们的Population合约来开始我们的测试。
- 通过提供一些定义的值来测试 Population.sol 智能合约的set 、 decrement和increment函数。
- Truffle 导入 Chai,因此我们可以使用 assert函数。我们传递实际值和期望值,要检查名称设置是否正确, assert(name === “India”) ; .
- 对于 currentPopulation,它返回 BigNum 对象但 Javascript 不处理它,因此将 BigNum 对象转换为常规 Javascript 对象进行检查 as assert( pop.toNumber() === 1388901219) ;
运行测试
- 运行测试如下:
truffle test
- 如果所有测试都通过,您将看到类似于以下内容的控制台输出:
与Flutter 的合约链接
- 在pubspec.yaml文件中导入以下包:
provider: ^4.3.3
web3dart: ^1.2.3
http: ^0.12.2
web_socket_channel: ^1.2.0
country_pickers: ^1.3.0
- 此外,将资产src/artifacts/Population.json 添加到pubspec.yaml文件中,该文件在我们迁移合约时由truffle -config.js生成。
assets:
- src/artifacts/Population.json
- 创建一个名为contract_linking的新文件。 dart在lib/目录中。
- 将以下内容添加到文件中:
Dart
import 'package:flutter/foundation.dart';
class ContractLinking extends ChangeNotifier {
}
变量
- 在class ContractLinking extends ChangeNotifier {之后的下一行添加以下变量
Dart
final String _rpcUrl = "http://10.0.2.2:7545";
final String _wsUrl = "ws://10.0.2.2:7545/";
final String _privateKey = "Enter Private Key";
库web3dart不会将签名的交易发送给矿工本身。相反,它依赖于 RPC 客户端来执行此操作。对于 WebSocket url,只需修改 RPC url。您可以从 ganache 获取 RPC url:
- 从 ganache 获取私钥:
- 在下面声明以下变量:
Dart
Web3Client _client;
bool isLoading = true;
String _abiCode;
EthereumAddress _contractAddress;
Credentials _credentials;
DeployedContract _contract;
ContractFunction _countryName;
ContractFunction _currentPopulation;
ContractFunction _set;
ContractFunction _decrement;
ContractFunction _increment;
String countryName;
String currentPopulation;
- _client变量将用于在 WebSocket 的帮助下建立到以太坊 rpc 节点的连接。
- isLoading变量将用于检查合约的状态。
- _abiCode变量将用于读取合约abi 。
- _contractAddress变量将用于存储已部署智能合约的合约地址。
- _credentials变量将存储智能合约部署者的凭据。
- _contract变量将用于告诉 Web3dart 我们的智能合约在哪里声明。
- _countryName 、 _currentPopulation 、 _set 、 _decrement和_increment变量将用于保存在我们的 Population.sol 智能合约中声明的函数。
- countryName和currentPopulation将保存智能合约中的确切名称和人口。
功能
- 声明上述变量后,在其下声明以下函数:
Dart
ContractLinking() {
initialSetup();
}
initialSetup() async {
// establish a connection to the ethereum rpc node. The socketConnector
// property allows more efficient event streams over websocket instead of
// http-polls. However, the socketConnector property is experimental.
_client = Web3Client(_rpcUrl, Client(), socketConnector: () {
return IOWebSocketChannel.connect(_wsUrl).cast();
});
await getAbi();
await getCredentials();
await getDeployedContract();
}
Future getAbi() async {
// Reading the contract abi
final abiStringFile =
await rootBundle.loadString("src/artifacts/Population.json");
final jsonAbi = jsonDecode(abiStringFile);
_abiCode = jsonEncode(jsonAbi["abi"]);
_contractAddress =
EthereumAddress.fromHex(jsonAbi["networks"]["5777"]["address"]);
}
Future getCredentials() async {
_credentials = await _client.credentialsFromPrivateKey(_privateKey);
}
Future getDeployedContract() async {
// Telling Web3dart where our contract is declared.
_contract = DeployedContract(
ContractAbi.fromJson(_abiCode, "Population"), _contractAddress);
// Extracting the functions, declared in contract.
_countryName = _contract.function("countryName");
_currentPopulation = _contract.function("currentPopulation");
_set = _contract.function("set");
_decrement = _contract.function("decrement");
_increment = _contract.function("increment");
getData();
}
getData() async {
// Getting the current name and population declared in the smart contract.
List name = await _client
.call(contract: _contract, function: _countryName, params: []);
List population = await _client
.call(contract: _contract, function: _currentPopulation, params: []);
countryName = name[0];
currentPopulation = population[0].toString();
print("$countryName , $currentPopulation");
isLoading = false;
notifyListeners();
}
addData(String nameData, BigInt countData) async {
// Setting the countryName and currentPopulation defined by the user
isLoading = true;
notifyListeners();
await _client.sendTransaction(
_credentials,
Transaction.callContract(
contract: _contract,
function: _set,
parameters: [nameData, countData]));
getData();
}
increasePopulation(int incrementBy) async {
// Increasing the currentPopulation
isLoading = true;
notifyListeners();
await _client.sendTransaction(
_credentials,
Transaction.callContract(
contract: _contract,
function: _increment,
parameters: [BigInt.from(incrementBy)]));
getData();
}
decreasePopulation(int decrementBy) async {
// Decreasing the currentPopulation
isLoading = true;
notifyListeners();
await _client.sendTransaction(
_credentials,
Transaction.callContract(
contract: _contract,
function: _decrement,
parameters: [BigInt.from(decrementBy)]));
getData();
}
创建一个 UI 来与智能合约交互
- 创建一个名为display_population的新文件。 dart在lib/目录中。
- 将以下内容添加到文件中:
Dart
import 'package:country_pickers/utils/utils.dart';
import 'package:flutter/material.dart';
import 'package:population/set_population.dart';
import 'package:provider/provider.dart';
import 'package:population/contract_linking.dart';
class DisplayPopulation extends StatelessWidget {
@override
Widget build(BuildContext context) {
final contractLink = Provider.of(context);
return Scaffold(
appBar: AppBar(
title: Text("Population On Blockchain"),
centerTitle: true,
),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.edit),
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => SetPopulation(),
fullscreenDialog: true));
},
),
body: Container(
padding: EdgeInsets.symmetric(horizontal: 20),
child: Center(
child: contractLink.isLoading
? CircularProgressIndicator()
: SingleChildScrollView(
child: Column(
children: [
contractLink.countryName == "Unknown"
? Icon(
Icons.error,
size: 100,
)
: Container(
child: CountryPickerUtils.getDefaultFlagImage(
CountryPickerUtils.getCountryByIsoCode(
contractLink.countryName)),
width: 250,
height: 150,
),
Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
"Country - ${contractLink.countryName}",
style: TextStyle(
fontSize: 30,
fontWeight: FontWeight.bold,
color: Theme.of(context).accentColor),
),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
"Population - ${contractLink.currentPopulation}",
style: TextStyle(
fontSize: 25,
fontWeight: FontWeight.bold,
color: Theme.of(context).primaryColor)),
),
contractLink.countryName == "Unknown"
? Text("")
: Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton.icon(
onPressed: () {
dialog(context, "Increase");
},
icon:
Icon(Icons.person_add_alt_1, size: 18),
label: Text("Increase"),
),
SizedBox(
width: 15,
),
ElevatedButton.icon(
onPressed: () {
if (contractLink.currentPopulation !=
"0") {
dialog(context, "Decrease");
}
},
icon: Icon(Icons.person_remove_alt_1,
size: 18),
label: Text("Decrease"),
)
],
),
)
],
),
),
),
),
);
}
dialog(context, method) {
final contractLink = Provider.of(context, listen: false);
TextEditingController countController = TextEditingController();
showDialog(
context: context,
builder: (_) => AlertDialog(
title: method == "Increase"
? Text("Increase Population")
: Text("Decrease Population"),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Current Population is ${contractLink.currentPopulation}"),
Padding(
padding: EdgeInsets.only(top: 20.0),
child: TextField(
controller: countController,
keyboardType: TextInputType.number,
decoration: InputDecoration(
border: OutlineInputBorder(),
hintText: method == "Increase"
? "Increase Population By ..."
: "Decrease Population By ...",
),
),
)
],
),
actions: [
Row(
children: [
TextButton(
child: Text("Cancel"),
onPressed: () {
Navigator.of(context).pop();
},
),
TextButton(
child: method == "Increase"
? Text("Increase")
: Text("Decrease"),
onPressed: () {
method == "Increase"
? contractLink.increasePopulation(
int.parse(countController.text))
: contractLink.decreasePopulation(
int.parse(countController.text));
Navigator.of(context).pop();
},
),
],
)
],
));
}
}
- 创建另一个名为set_population 的新文件。 dart在lib/目录中。
- 将以下内容添加到文件中:
Dart
import 'package:country_pickers/country.dart';
import 'package:country_pickers/country_pickers.dart';
import 'package:flutter/material.dart';
import 'package:population/contract_linking.dart';
import 'package:provider/provider.dart';
class SetPopulation extends StatefulWidget {
@override
_SetPopulationState createState() => _SetPopulationState();
}
class _SetPopulationState extends State {
Country _selectedCountry = CountryPickerUtils.getCountryByIsoCode('AF');
TextEditingController countryNameController =
TextEditingController(text: "Unknown");
TextEditingController populationController = TextEditingController();
@override
Widget build(BuildContext context) {
final contractLink = Provider.of(context);
return Scaffold(
appBar: AppBar(
title: Text("Set Population"),
actions: [
TextButton(
onPressed: () {
contractLink.addData(countryNameController.text,
BigInt.from(int.parse(populationController.text)));
Navigator.pop(context);
},
child: Text(
"SAVE",
style: TextStyle(
color: Colors.brown, fontWeight: FontWeight.bold),
))
],
),
body: Container(
child: Center(
child: SingleChildScrollView(
child: Column(
children: [
stackCard(countryFlagPicker(), countryFlag()),
stackCard(populationTextfield(), countryFlag()),
],
),
),
),
));
}
Widget stackCard(Widget widget1, Widget widget2) {
return Stack(
children: [
Padding(
padding: EdgeInsets.only(bottom: 8.0, left: 54),
child: Container(
height: 120,
child: Card(
color: Colors.cyan,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [widget1],
),
),
),
),
widget2
],
);
}
Widget countryFlag() {
return Positioned(
top: 15,
child: Container(
width: 100,
height: 100,
decoration: BoxDecoration(
shape: BoxShape.circle,
),
child: CircleAvatar(
backgroundColor: Colors.blueGrey,
child: Container(
width: 80,
height: 50,
child:
CountryPickerUtils.getDefaultFlagImage(_selectedCountry))),
),
);
}
Widget countryFlagPicker() {
return CountryPickerDropdown(
underline: Container(
height: 2,
color: Colors.black,
),
onValuePicked: (Country country) {
print("${country.name}");
setState(() {
_selectedCountry = country;
countryNameController.text = country.isoCode;
});
},
itemBuilder: (Country country) {
return Row(
children: [
SizedBox(width: 48.0),
CountryPickerUtils.getDefaultFlagImage(country),
SizedBox(width: 8.0),
Expanded(
child: Text(
country.name,
style: TextStyle(
color: Colors.brown,
fontSize: 25,
fontWeight: FontWeight.bold),
)),
],
);
},
icon: Icon(
Icons.arrow_downward,
color: Colors.white,
size: 50,
),
itemHeight: null,
isExpanded: true,
);
}
Widget populationTextfield() {
return Padding(
padding: EdgeInsets.only(left: 48.0, right: 5),
child: TextField(
decoration: InputDecoration(
filled: true,
fillColor: Colors.black,
focusedBorder: OutlineInputBorder(),
labelText: "Population",
labelStyle: TextStyle(color: Colors.white),
hintText: "Enter Population",
prefixIcon: Icon(Icons.person_pin_outlined)),
keyboardType: TextInputType.number,
controller: populationController,
),
);
}
}
- 更新主.dart为:
Dart
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:population/display_population.dart';
import 'package:provider/provider.dart';
import 'package:population/contract_linking.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (_) => ContractLinking(),
child: MaterialApp(
title: 'Population On Blockchain',
theme: ThemeData(
brightness: Brightness.dark,
primaryColor: Colors.cyan[400],
accentColor: Colors.deepOrange[200]),
home: DisplayPopulation(),
),
);
}
}
与完整的 Dapp 交互
- 现在我们准备好使用我们的 dapp 了!
- 只需运行Flutter项目。
如您所见, countryName ( Unknown ) 和currentPopulation ( 0 ) 实际上来自智能合约,我们在智能合约的构造函数中设置了它。
当您单击floatingActionButton 时,它会将当前路由推送到SetPopulation()。
- 您可以在此处定义countryName和currentPopulation 。
- 当您单击保存时,指定的名称和人口将实际添加到区块链中。
- 您可以通过单击指定的ElevatedButton来增加或减少currentPopulation 。
恭喜!您在成为成熟的移动 dapp 开发者方面迈出了一大步。对于本地开发,您拥有开始制作更高级 dapp 所需的所有工具。
如果您卡在某个地方,请查看 GitHub 存储库以获取完整代码。