📜  Flutter和区块链——人口 Dapp

📅  最后修改于: 2021-09-23 06:32:24             🧑  作者: Mango

在查看本文之前,请先看看Flutter和区块链 – Hello World Dapp。本教程将带您完成构建移动 dapp 的过程——区块链上的人口!

本教程适用于具有以太坊和智能合约基础知识、对Flutter框架有一定了解但对移动 dapp 不熟悉的人。

在本教程中,我们将介绍:

  1. 设置开发环境
  2. 创建松露项目
  3. 编写智能合约
  4. 编译和迁移智能合约
  5. 测试智能合约
  6. 与Flutter 的合约链接
  7. 创建一个 UI 来与智能合约交互
  8. 与完整的 Dapp 交互

描述

区块链上的人口是一个简单的去中心化应用程序,它允许您在区块链上存储特定国家的人口,您可以根据该国家的当前状况增加和减少人口。

输出:

设置开发环境

Truffle 是以太坊最受欢迎的开发框架,其使命是让您的生活更轻松。但是在我们安装 truffle 之前,请确保安装 node 。

一旦我们安装了节点,我们只需要一个命令来安装 Truffle:

npm install -g truffle

我们还将使用 Ganache,这是一个用于以太坊开发的个人区块链,可用于部署智能合约、开发应用程序和运行测试。您可以通过导航到 https://truffleframework.com/ganache 并单击“下载”按钮来下载 Ganache。

创建松露项目

  1. 在你喜欢的 IDE 中创建一个基本的Flutter项目
  2. 通过运行在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 ;
  • countryNamecurrentPopulation将保存国家的名称和人口。
  • 它被定义为 public 修饰符,因为 Solidity 会自动为所有公共状态变量创建 getter 函数。

构造函数

坚固性

constructor() public{
  countryName = "Unknown" ;
  currentPopulation = 0 ;
}

Solidity 中的构造函数仅在创建合约时执行一次,并用于初始化合约状态。这里我们只是设置变量的初始值。

函数

  1. 在我们上面设置的构造函数声明之后,将以下函数添加到智能合约中。

坚固性

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函数用于设置用户指定的countryNamecurrentPopulation
  • decrement函数将使用指定的计数减少currentPopulation
  • increment函数将使用指定的计数增加currentPopulation

编译和迁移

汇编

  1. 在终端中,确保您位于包含flutter和 truffle 项目的目录的根目录中,运行以下命令:
truffle compile

您应该会看到类似于以下内容的输出:

松露编译

移民

您会在migrations/目录中看到一个 JavaScript 文件: 1_initial_migration.js 。这处理部署Migrations.sol合约以观察后续的智能合约迁移,并确保我们将来不会重复迁移未更改的合约。

让我们创建自己的迁移脚本:

  1. migrations/目录中创建一个名为2_deploy_contracts.js的新文件。
  2. 将以下内容添加到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 中编写测试。

  1. test/目录中创建一个名为population.js的新文件。
  2. 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 智能合约的setdecrementincrement函数。
  • 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
  1. 创建一个名为contract_linking的新文件。 dartlib/目录中。
  2. 将以下内容添加到文件中:

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 – RPC 网址

  • 从 ganache 获取私钥:

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;
  1. _client变量将用于在 WebSocket 的帮助下建立到以太坊 rpc 节点的连接。
  2. isLoading变量将用于检查合约的状态。
  3. _abiCode变量将用于读取合约abi
  4. _contractAddress变量将用于存储已部署智能合约的合约地址。
  5. _credentials变量将存储智能合约部署者的凭据。
  6. _contract变量将用于告诉 Web3dart 我们的智能合约在哪里声明。
  7. _countryName_currentPopulation_set_decrement_increment变量将用于保存在我们的 Population.sol 智能合约中声明的函数。
  8. countryNamecurrentPopulation将保存智能合约中的确切名称和人口。

功能

  • 声明上述变量后,在其下声明以下函数:

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 来与智能合约交互

  1. 创建一个名为display_population的新文件。 dartlib/目录中。
  2. 将以下内容添加到文件中:

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();
                      },
                    ),
                  ],
                )
              ],
            ));
  }
}
  1. 创建另一个名为set_population 的新文件。 dartlib/目录中。
  2. 将以下内容添加到文件中:

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 交互

  1. 现在我们准备好使用我们的 dapp 了!
  2. 只需运行Flutter项目。

如您所见, countryName ( Unknown ) 和currentPopulation ( 0 ) 实际上来自智能合约,我们在智能合约的构造函数中设置了它。

当您单击floatingActionButton 时,它会将当前路由推送到SetPopulation()。

  1. 您可以在此处定义countryNamecurrentPopulation
  2. 当您单击保存时,指定的名称和人口将实际添加到区块链中。

  • 您可以通过单击指定的ElevatedButton来增加或减少currentPopulation

恭喜!您在成为成熟的移动 dapp 开发者方面迈出了一大步。对于本地开发,您拥有开始制作更高级 dapp 所需的所有工具。

如果您卡在某个地方,请查看 GitHub 存储库以获取完整代码。