Flutter でアプリ開発(3. 状態の変化とウィジェットへの反映)
当たり前のことですが、何かしらの入力があったときは画面が変化するものです。画面を変化させるためには変数を定義して、変更に応じて反映させる必要があります。
今まで使っていた StatelessWidget ではそれができないので、今回は別の方法でやっていきたいと思います。
参考ページ
- Introduction to widgets | Flutter
- はじめに | Riverpod
- 【Flutter】StatefulWidget の 2 つの問題点。どうして実践ではあまり使われないか?その代わりにどうすればいいか?|石川 陽太| note
StatefulWidget を使う
Flutter には StatelessWidget の他に、StatefulWidget というクラスも用意されています。
以下のソースは公式のチュートリアルのものです。
画面にボタンが表示され、押した回数だけカウンターが上がっていきます。
import 'package:flutter/material.dart'; class Counter extends StatefulWidget { const Counter({Key? key}) : super(key: key); @override _CounterState createState() => _CounterState(); } class _CounterState extends State<Counter> { int _counter = 0; void _increment() { setState(() { _counter++; }); } @override Widget build(BuildContext context) { return Row( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ ElevatedButton( onPressed: _increment, child: const Text('Increment'), ), const SizedBox(width: 16), Text('Count: $_counter'), ], ); } } void main() { runApp( const MaterialApp( home: Scaffold( body: Center( child: Counter(), ), ), ), ); }
前回は自作のウィジェットを作成するために、 1 つのクラス(StatelessWidget を継承したクラス)しか定義しませんでしたが、今回は以下のクラスを定義しています。
- StatefulWidget をオーバーライドした Counter クラス
- 画面には表示されない
- State をオーバーライドした _CounterState クラス
- データを保持する(ここでは _counter 変数)
- ウィジェットを生成・再生成する
StatelessWidget の時とは違い、build 関数は StatefulWidget には定義されていません。build 関数は State で定義され、データが変更されるたびにウィジェットを生成して表示するようになっています。
アプリが動いている間、State は残り続ける一方、データが変更されるたびに作られたウィジェットは破棄されうることになります。
また、State クラス内で変数を変更するときは setState 関数にコールバックを渡して行う必要があります。そうすることで、値が変更されたことが Flutter に通知され、正しく UI が更新されます。
以下は実行画面です。
ボタンを追加する
先程の例をアレンジして、カウンターを減らすボタンを追加します。
build 関数を以下のように変更しました。
@override Widget build(BuildContext context) { return Row( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ // 追加:Columnを使ってボタンを縦に配置 Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ ElevatedButton( onPressed: _increment, child: const Text('+ Increment'), ), //追加:数を減らすボタン ElevatedButton( onPressed: _decrement, child: const Text('- Decrement'), ) ] ), const SizedBox(width: 16), Text('Count: $_counter'), ], ); }
カウンターを減らす関数は次の通りです。
void _decrement() { setState(() { _counter--; }); }
実行画面です
これでも良いのですが、2 つのボタンを 1 つのウィジェットとしてまとめ、それを子ウィジェットとして作成した方がわかりやすくなります。以下の例では、2 つのボタンを持つ CounterButtons という StatelessWidget を作りました。
以下は、公式チュートリアルをアレンジして作った例です。
import 'package:flutter/material.dart'; class CounterButtons extends StatelessWidget { const CounterButtons({ required this.onIncrement, required this.onDecrement, Key? key }) : super(key: key); final VoidCallback onIncrement; final VoidCallback onDecrement; @override Widget build(BuildContext context) { return Column( mainAxisAlignment: MainAxisAlignment.center, children: [ ElevatedButton( onPressed: onIncrement, child: const Text('+1 Increment'), ), ElevatedButton( onPressed: onDecrement, child: const Text('-1 Decrement'), ) ] ); } } class Counter extends StatefulWidget { const Counter({Key? key}) : super(key: key); @override _CounterState createState() => _CounterState(); } class _CounterState extends State<Counter> { int _counter = 0; void _increment() { setState(() { ++_counter; }); } void _decrement() { setState(() { _counter--; }); } @override Widget build(BuildContext context) { return Row( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ CounterButtons( onIncrement: _increment, onDecrement: _decrement, ), const SizedBox(width: 16), Text('Count: $_counter'), ], ); } } void main() { runApp( const MaterialApp( home: Scaffold( body: Center( child: Counter(), ), ), ), ); }
CounterButtons は 2 つのボタンを build 関数で作成します。ボタンを押した時に呼ばれる関数は、コンストラクタで渡されるようにしました。それらの関数を経由してカウンターの値を変更しています。
たった 2 つのボタンを表示したいというだけでは、ありがたみもそんなに感じないですが、もっと UI が複雑になってくると必須だと思います。また、ウィジェットを使い回すこともできます。
画面は前と同じです。
Reverpod を使う
ウィジェットがデータを持ってしまうと、めんどくさいことになりがちなので、別途、状態管理ライブラリを使用することができます。いろいろ選択肢はあるようですが、今回は Riverpod を使ってみます。
pubspec.yaml の dependencies に必要なパッケージ(flutter_hooks / hooks_riverpod)を追加します。今回は hooks_riverpod を使いませんでしたが、後で使いそうなので入れておきました。
dependencies: flutter: sdk: flutter flutter_hooks: ^0.18.2 hooks_riverpod: ^1.0.3
Android Studio の下端の Terminal ボタンを押して、flutter pub get と入力して実行すると、ライブラリがダウンロードされます。
先程のサンプルを、Riverpod を使って書き直しました。
import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; // 追加:状態を保持するオブジェクト final counterProvider = StateProvider((ref) => 0); // 親クラスを ConsumerWidget に変更 class CounterButtons extends ConsumerWidget { const CounterButtons({Key? key}) : super(key: key); // build 関数の引数に WidgetRef が追加される @override Widget build(BuildContext context, WidgetRef ref) { return Column( mainAxisAlignment: MainAxisAlignment.center, children: [ ElevatedButton( // 変更:counterProvider を経由して値を変更 onPressed: () => ref.watch(counterProvider.notifier).state++, child: const Text('+1 Increment'), ), ElevatedButton( // 変更:同上 onPressed: () => ref.watch(counterProvider.notifier).state--, child: const Text('-1 Decrement'), ) ] ); } } // 親クラスを ConsumerWidget に変更 class Counter extends ConsumerWidget { const Counter({Key? key}) : super(key: key); // build 関数の引数に WidgetRef が追加される @override Widget build(BuildContext context, WidgetRef ref) { // counterProvider を経由して値を取得 final counter = ref.watch(counterProvider); return Row( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ const CounterButtons(), const SizedBox(width: 16), // counter の値は自動で反映される Text('Count: $counter'), ], ); } } void main() { runApp( const ProviderScope( // 追加 child: MaterialApp( home: Scaffold( body: Center( child: Counter(), ), ), ), ) ); }
StatelessWidget と StatefulWidget のどちらも ConsumerWidget で置き換えています。
StatefulWidget を使ったやり方では、Counter に変数があり、コールバック関数を経由して操作を行っていました。
対して上記のやり方では、グローバル変数の counterProvider を経由して値の変更を行っています。なので Counter を意識せずとも、CounterButtons から直接カウンターの値を変更できます。
また、値が変わっても自動で反映されるのは変わりません。
特殊な書き方をする必要がありますが、プログラムの構造は分かりやすくなります。
Dart 言語で気になったこと
関数定義
以下のどのようにも書けるようです。
// 普通 int add(int a, int b){ return a + b; } // アロー演算子 int add_oneline(int a, int b) => a + b; // 匿名関数 int Function(int, int) add_lambda = (int a, int b) { return a + b; }; // 匿名関数 + アロー演算子 int Function(int, int) add_lambda_oneline = (int a, int b) => a + b;
型推論を使えば、以下のようにも書けます。
// 型推論 var add_lambda_oneline = (int a, int b) => a + b;
まとめ
ユーザーの入力を受け付けるようになり、ようやくアプリっぽくなってきました。
状態変化の処理は Riverpod のおかげで、相当かんたんに書けるようになりました。これじゃあ StatefulWidget なんていらないじゃないか。入れるのは必須と言って良いかもしれません。勉強量が増えますけど……
次回はここまでの応用で、もっとアプリっぽい見た目の物を作ってみようと思います。