メインコンテンツまでスキップ

lesson-1_Flutterでフロントを開発しよう(ロジック編)

✨ Flutter でフロントを開発する(ロジック編)

※ 今回のプロジェクトではAndroid Emulatorを用いて開発します。

※ Flutterはメインではないので1行1行解説はしていきません。詳しく知りたい方は各自での学習をお願い致します。

UIとデータコントローラを構築するのに、libディレクトリに3つのファイルを作成します。

  1. TodoListModel.dart : データモデル、コントラクト関数、UIを更新するためのノーティファイクラスが含まれます。

  2. TodoList.dart : アプリのメインUIが含まれます。

  3. TodoBottomSheet.dart : ToDoの作成・更新フォームが含まれます。

それではまず、Flutterのロジック部分を構築するために、1. TodoListModel.dartファイルから中身を作成していきます。

TodoListModel.dartファイルに、下記を追加してください。

//TodoListModel.dart
//パッケージをインポートする。
import 'dart:convert';
import 'dart:core';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:http/http.dart';
import 'package:web3dart/web3dart.dart';
import 'package:web_socket_channel/io.dart';

class TodoListModel extends ChangeNotifier {
List<Task> todos = [];
bool isLoading = true;
int? taskCount;
final String _rpcUrl = "http://127.0.0.1:7545";
final String _wsUrl = "ws://127.0.0.1:7545/";

//自分のPRIVATE_KEYを追加してください。
final String _privateKey =
"YOUR_PRIVATE_KEY";

Web3Client? _client;
String? _abiCode;

Credentials? _credentials;
EthereumAddress? _contractAddress;
EthereumAddress? _ownAddress;
DeployedContract? _contract;

ContractFunction? _taskCount;
ContractFunction? _todos;
ContractFunction? _createTask;
ContractFunction? _updateTask;
ContractFunction? _deleteTask;
ContractFunction? _toggleComplete;
}

List<Task> todos = [];Taskに赤波線が引かれていますが、後で定義していくので今は無視してください。

それでは、詳しく見ていきましょう。

//TodoListModel.dart
import 'dart:convert';
import 'dart:core';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:http/http.dart';
import 'package:web3dart/web3dart.dart';
import 'package:web_socket_channel/io.dart';

ファイルの冒頭で、必要なパッケージをインポートします。

//TodoListModel.dart
class TodoListModel extends ChangeNotifier {
...
}

ProviderパッケージのChangeNotifierクラスを継承したTodoListModelクラスを作成しています。

Providerパッケージの詳細についてはこちらをご覧ください。

  final String _rpcUrl = "http://127.0.0.1:7545";
final String _wsUrl = "ws://127.0.0.1:7545/";

Ganacheの_rpcUrl_wsUrlをローカル環境用に設定しています。

  • Android向けにEmulatorを使ってDebugビルドをする場合は_rpcUrlをhttp://10.0.2.2:7545に変更する必要があります

    また、開発マシンのアドレス 127.0.0.1 は、エミュレータ固有のループバック インターフェースと一致することになります。開発マシンのループバック インターフェース(マシン上の別名 127.0.0.1)で実行されているサービスにアクセスする場合は、代わりに特殊アドレス 10.0.2.2 を使用する必要があります。 (参照: https://developer.android.com/studio/run/emulator-networking)

  final String _privateKey =
"YOUR_PRIVATE_KEY";

Ganacheから任意のアカウントの秘密鍵を設定する(Ganache UIで鍵アイコンをクリックすると取得できますので貼り付けてください)。下記参考。

次に、TodoListModelクラス内のContractFunction? _toggleComplete;の直下に下記を追加してください。

//TodoListModel.dart
TodoListModel() {
init();
}

Future<void> init() async {
_client = Web3Client(_rpcUrl, Client(), socketConnector: () {
return IOWebSocketChannel.connect(_wsUrl).cast<String>();
});

await getAbi();
await getCredentials();
await getDeployedContract();
}

//スマートコントラクトの`ABI`を取得し、デプロイされたコントラクトのアドレスを取り出す。
Future<void> getAbi() async {
String abiStringFile = await rootBundle
.loadString("smartcontract/TodoContract.json");
var jsonAbi = jsonDecode(abiStringFile);
_abiCode = jsonEncode(jsonAbi["abi"]);
_contractAddress =
EthereumAddress.fromHex(jsonAbi["networks"]["5777"]["address"]);
}

//秘密鍵を渡して`Credentials`クラスのインスタンスを生成する。
Future<void> getCredentials() async {
_credentials = await _client!.credentialsFromPrivateKey(_privateKey);
_ownAddress = await _credentials!.extractAddress();
}

//`_abiCode`と`_contractAddress`を使用して、スマートコントラクトのインスタンスを作成する。
Future<void> getDeployedContract() async {
_contract = DeployedContract(
ContractAbi.fromJson(_abiCode!, "TodoList"), _contractAddress!);
_taskCount = _contract!.function("taskCount");
_updateTask = _contract!.function("updateTask");
_createTask = _contract!.function("createTask");
_deleteTask = _contract!.function("deleteTask");
_toggleComplete = _contract!.function("toggleComplete");
_todos = _contract!.function("todos");
await getTodos();
}
}

await getTodos();getTodosに赤波線が引かれていますが、後で定義していくので今は無視してください。

それでは、詳しく見ていきましょう。

//TodoListModel.dart
TodoListModel() {
init();
}

Future<void> init() async {
_client = Web3Client(_rpcUrl, Client(), socketConnector: () {
return IOWebSocketChannel.connect(_wsUrl).cast<String>();
});

await getAbi();
await getCredentials();
await getDeployedContract();
}

TodoListModelクラスのコンストラクタでは、非同期のinit関数を呼び出しています。

init関数には、_clientオブジェクトの初期化に続いて、getAbigetCredentialsgetDeployedContract関数を呼び出しています。

//TodoListModel.dart
Future<void> getAbi() async {
String abiStringFile = await rootBundle
.loadString("smartcontract/TodoContract.json");
var jsonAbi = jsonDecode(abiStringFile);
_abiCode = jsonEncode(jsonAbi["abi"]);
_contractAddress =
EthereumAddress.fromHex(jsonAbi["networks"]["5777"]["address"]);
}

getAbi関数では、スマートコントラクトのABIを取得し、デプロイされたコントラクトのアドレスを取り出しています。

//TodoListModel.dart
Future<void> getCredentials() async {
_credentials = await _client!.credentialsFromPrivateKey(_privateKey);
_ownAddress = await _credentials!.extractAddress();
}

getCredentials関数では、秘密鍵を渡してCredentialsクラスのインスタンスを生成しています。

※秘密鍵は暗号化して保存することが推奨されています。今回の学習では重要ではないので、簡略化するためにも、秘密鍵は文字列形式で保存しています。

//TodoListModel.dart
Future<void> getDeployedContract() async {
_contract = DeployedContract(
ContractAbi.fromJson(_abiCode!, "TodoList"), _contractAddress!);
_taskCount = _contract!.function("taskCount");
_updateTask = _contract!.function("updateTask");
_createTask = _contract!.function("createTask");
_toggleComplete = _contract!.function("toggleComplete");
_deleteTask = _contract!.function("deleteTask");
_todos = _contract!.function("todos");
await getTodos();
}

getDeployedContract関数では、_abiCode_contractAddressを使用して、スマートコントラクトのインスタンスを作成しています。

コントラクトのインスタンスができれば、上記のコードのように、スマートコントラクトにあるすべての関数のインスタンスを作成することができます。

これですべての変数を初期化できたので、次はCRUD操作を実装します。以下の4つを思い出してください。

  1. to-doを作成する機能

  2. to-doを更新する機能

  3. to-doの完了・未完了を切り替える機能

  4. to-doを削除する機能

TodoListModelクラス内のgetDeployedContract関数の下に下記を追加してください。

//TodoListModel.dart
//すべてのto-do項目を取得してリストに追加する。
getTodos() async {
List totalTaskList = await _client!
.call(contract: _contract!, function: _taskCount!, params: []);

BigInt totalTask = totalTaskList[0];
taskCount = totalTask.toInt();
todos.clear();
for (var i = 0; i < totalTask.toInt(); i++) {
var temp = await _client!.call(
contract: _contract!, function: _todos!, params: [BigInt.from(i)]);
if (temp[1] != "")
todos.add(
Task(
id: (temp[0] as BigInt).toInt(),
taskName: temp[1],
isCompleted: temp[2],
),
);
}
isLoading = false;
todos = todos.reversed.toList();

notifyListeners();
}

//1.to-doを作成する機能
addTask(String taskNameData) async {
isLoading = true;
notifyListeners();
await _client!.sendTransaction(
_credentials!,
Transaction.callContract(
contract: _contract!,
function: _createTask!,
parameters: [taskNameData],
),
);
await getTodos();
}

//2.to-doを更新する機能
updateTask(int id, String taskNameData) async {
isLoading = true;
notifyListeners();
await _client!.sendTransaction(
_credentials!,
Transaction.callContract(
contract: _contract!,
function: _updateTask!,
parameters: [BigInt.from(id), taskNameData],
),
);
await getTodos();
}

//3.to-doの完了・未完了を切り替える機能
toggleComplete(int id) async {
isLoading = true;
notifyListeners();
await _client!.sendTransaction(
_credentials!,
Transaction.callContract(
contract: _contract!,
function: _toggleComplete!,
parameters: [BigInt.from(id)],
),
);
await getTodos();
}

//4.to-doを削除する機能
deleteTask(int id) async {
isLoading = true;
notifyListeners();
await _client!.sendTransaction(
_credentials!,
Transaction.callContract(
contract: _contract!,
function: _deleteTask!,
parameters: [BigInt.from(id)],
),
);
await getTodos();
}

詳しく見ていきましょう。

//TodoListModel.dart
getTodos() async {
List totalTaskList = await _client!
.call(contract: _contract!, function: _taskCount!, params: []);

BigInt totalTask = totalTaskList[0];
taskCount = totalTask.toInt();
todos.clear();
for (var i = 0; i < totalTask.toInt(); i++) {
var temp = await _client!.call(
contract: _contract!, function: _todos!, params: [BigInt.from(i)]);
if (temp[1] != "")
todos.add(
Task(
id: (temp[0] as BigInt).toInt(),
taskName: temp[1],
isCompleted: temp[2],
),
);
}
isLoading = false;
todos = todos.reversed.toList();

notifyListeners();
}

getTodosでは、_taskCount関数を呼び出してto-doの総数を取得し、ループを使ってすべてのto-do項目を取得してリストに追加しています。

すべてのto-doを取得したら、isLoadingfalseに設定し、ProviderパッケージからnotifyListenersを呼び出してUIを更新しています。

//TodoListModel.dart
addTask(String taskNameData) async {
isLoading = true;
notifyListeners();
await _client!.sendTransaction(
_credentials!,
Transaction.callContract(
contract: _contract!,
function: _createTask!,
parameters: [taskNameData],
),
);
await getTodos();
}

addTaskでは、タスク名をパラメータとして受け取り、isLoadingtrueに設定し、 _contractオブジェクトを使用して 、taskNameDataをパラメータとして渡し、_createTask関数を呼び出すようにしています。

//TodoListModel.dart
updateTask(int id, String taskNameData) async {
isLoading = true;
notifyListeners();
await _client!.sendTransaction(
_credentials!,
Transaction.callContract(
contract: _contract!,
function: _updateTask!,
parameters: [BigInt.from(id), taskNameData],
),
);
await getTodos();
}

updateTaskはタスクのidと更新されたtaskNameDataを受け取り、スマートコントラクトにクレデンシャルを使用してトランザクションを作成しています。

web3dartパッケージはBigInt形式のすべての数値を必要とするため、idBigIntのインスタンスとして送信していることに注意してください。

//TodoListModel.dart
toggleComplete(int id) async {
isLoading = true;
notifyListeners();
await _client!.sendTransaction(
_credentials!,
Transaction.callContract(
contract: _contract!,
function: _toggleComplete!,
parameters: [BigInt.from(id)],
),
);
await getTodos();
}

toggleComplete関数はタスクのidを受け取り、スマートコントラクトのisCompleteブール値の値を切り替えています。

//TodoListModel.dart
deleteTask(int id) async {
isLoading = true;
notifyListeners();
await _client!.sendTransaction(
_credentials!,
Transaction.callContract(
contract: _contract!,
function: _deleteTask!,
parameters: [BigInt.from(id)],
),
);
await getTodos();
}

deleteTask関数は、削除するタスクのidを受け取り、スマートコントラクトのtodosマッピングからその特定のタスクを削除するために、コントラクトの_deleteTask関数を呼び出します。

addTaskupdateTasktoggleCompletedeleteTask関数の最後で、getTodosを呼び出して、更新されたto-doリストを取得し、UIを更新しています。

また、スマートコントラクト関数を呼び出す前にローカルのto-doリストを更新して、結果を即座に表示し、バックグラウンドでリストを更新することも可能です。

ここまでで、Taskに赤波線がいかれているかと思いますが、そのTaskを定義していきます。

Taskは、TodoListModelでto-doのリストを格納するモデルクラスでです。

それでは、以下のコードをTodoListModel.dartファイルの一番下に追加してください。

//TodoListModel.dart
//to-doのリストを格納するモデルクラス
class Task {
final int? id;
final String? taskName;
final bool? isCompleted;
Task({this.id, this.taskName, this.isCompleted});
}

以上で、Flutterのロジック部分が終わったので、次にアプリのUIを作成していきます。

🙋‍♂️ 質問する

ここまでの作業で何かわからないことがある場合は、Discordの#polygonで質問をしてください。

ヘルプをするときのフローが円滑になるので、エラーレポートには下記の3点を記載してください ✨

1. 質問が関連しているセクション番号とレッスン番号
2. 何をしようとしていたか
3. エラー文をコピー&ペースト
4. エラー画面のスクリーンショット

次のレッスンに進んで、フロント側の実装(UI編)を開始しましょう 🎉