概要
flutterで、ビュー、ステート、ロジックを整理・分割してコーディングでき、外部からもステートを変更できるStatefulWidgetの構築方法を整理、メモした。
背景と目的
flutterのコーディングをしていて思うのは、
簡単なことをやりたいのに、意外と複雑なコードを書かなければならないことが多い
ことだ。例えば、深い階層にあるウィジェットにデータを伝達するのを簡単にするためにInheritedWidget、Provider、Riverpodを導入したりするのだが、ライブラリ固有の書き方や仕組みをいちいち覚えなければいけなくて、それらを導入したことで楽になったという実感が薄い。
そこで、
標準の仕組みだけを使い、コード量が少なく、ビュー、ステート、ロジックを整理・分割して記述でき、なおかつ外部(上位階層)から柔軟にステートを変更できるような方法が無いか
模索していた。一応基本形が整理できたので、メモしておく。なお、参考としてこちらのサイトが役立った。
詳細
0. できるようにしたいこと
- 外部ライブラリ等を用いない
- ビュー、ステート、ロジックを整理・分割してコーディングできる
- 外部(自身が紐づく上位階層)から任意にステートを参照して変更できる
1. コード全体
いきなりだが、以下に簡単なサンプルとしてWidgetAというものを実装してみた。これは、ボタンとテキストを持つだけのシンプルなもの。
import 'package:flutter/material.dart'; // ステート担当部分 class WidgetAState { String text = "WidgetAテキスト"; String buttonText = "WidgetAボタン"; } // ロジック担当部分 // ignore: must_be_immutable class WidgetA extends StatefulWidget { WidgetA({super.key}); // 状態データ WidgetAState state = WidgetAState(); // 外部からも再描画させる仕組み ------------------ // _WidgetAのsetStateへの参照 void Function(void Function())? _setState; void setState({WidgetAState? newState}) { if (_setState != null) { _setState!(() { if (newState != null) { state = newState; } }); } } // --------------------------------------------- // ボタン押下時処理 void onPressd() { state.text = "onPressd"; setState(); } @override State<WidgetA> createState() => _WidgetA(); } // ビュー担当部分 class _WidgetA extends State<WidgetA> { @override void initState() { super.initState(); // setStateへの参照をwidget側に渡しておく widget._setState = setState; } @override void dispose() { super.dispose(); // widget側でsetStateが呼ばれないようにnullを設定しておく widget._setState = null; } @override Widget build(BuildContext context) { return Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text(widget.state.text), ElevatedButton( onPressed: () => widget.onPressd(), child: Text(widget.state.buttonText), ), ], ); } }
1.1 WidgetAState
これは、WidgetAに関係するステートの役割である。ここでは、ボタンのテキストbuttonTextとボタンを押した際に書き換えられる対象のテキストtextを保持するためのメンバを持つ。
1.2 WidgetA
StatefulWidgetで、WidgetAState型のメンバstateと、setStateというメソッドを持つ。ロジックを記述する役割を持たせる。
メンバ変数stateは、パブリックアクセスが可能なので、外部から操作できるようになっている。
メソッドsetStateは、再描画をするためのメソッド。通常、StatefulWidgetは、それに紐づくState側でしかsetStateが使えないのだが、後述の_WidgetAクラスにおいて、setStateメソッドをWidgetAクラス側に渡すことで、WidgetA側で実行できるようにしてある。
1.3 _WidgetA
WidgetAに紐づくStateクラスであり、Stateと言いながらbuildメソッドが実装されているのでビューを担当する。
- initStateメソッドにおいて、setStateメソッドをWidgetAクラス側に渡している。
- disposeメソッドにおいて、widget側の_setStateをnullにする。これにより、widgetがツリーに存在しないときにsetStateが呼ばれてエラーになるのを防止する。
2. 利用方法
上の図のように、WidgetAの下にボタンが追加された非常に簡単なアプリを構築した。
- ElevatedButton "外部のボタン"は、押下するとWidgetAのボタンテキストを書き換えられる。WidgetAのstateというプロパティとsetStateにアクセスできるおかげでこれが実現できている。
- MyAppStateクラスは、MyAppが使用する(操作したい)データをまとめておくためのものとして定義した。WidgetAクラスが操作したい対象なので、そのインスタンスwaを保持させる。
- MyAppStateクラスのインスタンスをMyAppに持たせることで、WidgetAのステートに、MyAppから何時でもアクセスできる。
つまり、
WidgetAのような構造であれば、上位のWidgetは下位の任意の階層にあるWidgetにいつでもアクセスでき、ステートを変更したり部分的に再描画したりできる。そして、階層が深くなっても、上位から順次データを送り込むためのコーディングは必要ない。
import 'package:flutter/material.dart'; import 'package:flutter_routing/widget_a.dart'; // MyAppのステート class MyAppState { WidgetA? wa; MyAppState() { wa = WidgetA(); } } void main() { runApp( MaterialApp( title: 'Flutter Demo', theme: ThemeData( colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple), useMaterial3: true, ), home: MyApp(), ), ); } // ignore: must_be_immutable class MyApp extends StatefulWidget { MyApp({super.key}); MyAppState state = MyAppState(); void onPressd() { state.wa!.state.buttonText = "ボタン名変更"; state.wa!.setState(); } @override State<MyApp> createState() => _MyApp(); } class _MyApp extends State<MyApp> { @override Widget build(BuildContext context) { return Scaffold( body: Container( padding: const EdgeInsets.all(20), child: Column( children: [ widget.state.wa!, ElevatedButton( onPressed: () => widget.onPressd(), child: const Text("外部のボタン"), ), ], ), ), ); } }
参考
まとめと今後の課題
特別なライブラリを用いずに、効率的なWidgetのコーディング方法が整理できた。次のアプリ作成の際に活用していきたい。