[Flutter入門(6)] 公式チュートリアルのステップ2の後半:Managing stateを読んでみる
Flutterの公式サイトに チュートリアル として紹介されているコンテンツのうち、2つ目の Adding interactivity to your Flutter app のページの後半部分にある Managing state の部分を読んで、その内容をまとめてみました。
原文を補完するような形で活用していただければと思います。
はじめに
先にポイントをまとめると、以下のとおり。
- 状態の管理方法にはいくつかのアプローチがある
- ウィジェットをデザインする際、どのアプローチを使うかはあなた次第
- 迷う場合は、親ウィジェットで管理する方法から始めるのがおすすめ
Statefulウィジェットの状態を管理する方法にはいくつかの選択肢があり、どの方法が適切かは状況による。ウィジェットをデザインする際は、そのウィジェットがどのように使われることを期待するかによって適切に管理方法を選択するべし。
最も一般的な管理方法は以下の3つ。
どの方法を選択するかを決める際は、以下の基本原則を参考にするとよい。
- 状態がユーザーデータに関するものであれば、親ウィジェットに管理してもらうのがよい(例:チェックボックスの選択状態、スライダーの位置情報など)
- 状態が単純に見た目に関するものであれば、ウィジェット自身で管理するのがよい(例:アニメーションなど)
- 迷う場合は、親ウィジェットに管理してもらう方法から始めるとよい
以下、簡単な実例を用いて3つの管理方法を比較してみる。例とする仕様は以下のとおり。
- それぞれの管理方法ごとに、
TapboxA
TapboxB
TapboxC
というシンプルなウィジェットを用意する - いずれもタップすると
_active
という真偽値がトグルする機能を持ち、_active
の状態に応じて見た目が下図のようにグリーン(アクティブ)とグレー(非アクティブ)で変化する
なお、このサンプルでは Container
をタップ可能にするために GestureDetector
クラスを使用する。
著者注
実際にコードを動かしながら読み進めたい場合は、flutter create managing_state
などとして雛形アプリを作成しておいてください。managing_state/lib/main.dart
の中身を書き換えていくことで、実際にサンプルコードを動かすことができます。
ウィジェット自身が自分の状態を管理する方法
例えば ListView
ウィジェットは、コンテンツのサイズに対して表示領域が小さすぎる場合、自動的にスクロール可能な状態としてレンダリングされ、現在のスクロール位置などの状態も ListView
自身が管理する。
ListView
を使用する開発者は、 ListView
のスクロールに関する振る舞いをわざわざ自分で管理したいとは思わないので、この設計は正しいと言える。
ここで紹介するパターンはこれと同じで、以下に例示する _TapboxAState
クラスは、
-
TapboxA
ウィジェットの状態を管理する -
_active
という真偽値のプロパティを持ち、この値がボックスの色を決定する -
_handleTap()
メソッドを持ち、このメソッドが_active
プロパティの値の変更およびsetState()
をコールすることによるUIの再描画を行う - ウィジェットについてのすべてのインタラクティブな操作を実装している
コード例
// lib/main.dart
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
home: Scaffold(
appBar: AppBar(
title: Text('Flutter Demo'),
),
body: Center(
child: TapboxA(),
),
),
);
}
}
class TapboxA extends StatefulWidget {
TapboxA({Key key}) : super(key: key);
_TapboxAState createState() => _TapboxAState();
}
class _TapboxAState extends State<TapboxA> {
bool _active = false;
void _handleTap() {
setState(() {
_active = !_active;
});
}
Widget build(BuildContext context) {
return GestureDetector(
onTap: _handleTap,
child: Container(
child: Center(
child: Text(
_active ? 'Active' : 'Inactive',
style: TextStyle(fontSize: 32.0, color: Colors.white),
),
),
width: 200.0,
height: 200.0,
decoration: BoxDecoration(
color: _active ? Colors.lightGreen[700] : Colors.grey[600],
),
),
);
}
}
親ウィジェットが子ウィジェットの状態を管理する方法
親ウィジェットが子ウィジェットの状態を管理して、変更があれば子ウィジェットに伝えるという設計が合理性を持つ状況は多い。
例えば、 IconButton
はアイコンをタップ可能なボタンとして使用できるウィジェットだが、 IconButton
自体はStatelessウィジェットである。 IconButton
の親ウィジェットが「ボタンが押されたかどうか」を知り、適切なアクションを実行させることができるようになっている。
以下の例では、 TabboxB
は自身の状態を親ウィジェットに対して開示(コールバックを使ってアクセスできるように)しており、 TapboxB
自体は何ら状態を管理していない。(Statelessウィジェットである)
一方で ParentWidgetState
クラスは、
-
_active
によってTapboxB
の状態を管理している -
_handleTapboxChanged()
という、ボックスがタップされたときに実行されるメソッドを持っている - 状態が変化したら、
setState()
を呼び出してUIを更新する
また TapboxB
クラスは、
-
StatelessWidget
クラスを継承しており、自身は状態を持たない(親が管理してくれるから) - タップを検知したら、親にそのことを通知する
コード例
// lib/main.dart
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
home: Scaffold(
appBar: AppBar(
title: Text('Flutter Demo'),
),
body: Center(
child: ParentWidget(),
),
),
);
}
}
class ParentWidget extends StatefulWidget {
_ParentWidgetState createState() => _ParentWidgetState();
}
class _ParentWidgetState extends State<ParentWidget> {
bool _active = false;
void _handleTapboxChanged(bool newValue) {
setState(() {
_active = newValue;
});
}
Widget build(BuildContext context) {
return Container(
child: TapboxB(
active: _active,
onChanged: _handleTapboxChanged,
),
);
}
}
class TapboxB extends StatelessWidget {
TapboxB({Key key, this.active: false, this.onChanged})
: super(key: key);
final bool active;
final ValueChanged<bool> onChanged;
void _handleTap() {
onChanged(!active);
}
Widget build(BuildContext context) {
return GestureDetector(
onTap: _handleTap,
child: Container(
child: Center(
child: Text(
active ? 'Active' : 'Inactive',
style: TextStyle(fontSize: 32.0, color: Colors.white),
),
),
width: 200.0,
height: 200.0,
decoration: BoxDecoration(
color: active ? Colors.lightGreen[700] : Colors.grey[600],
),
),
);
}
}
mix-and-matchアプローチ
mix-and-matchアプローチと呼ばれる手法が最適な場合もありえる。例えば、あるStatefulウィジェットが
ここで紹介する TapboxC
の例では、下図のように、タップダウン(押し込み)中はボックスに濃い緑色の枠線を表示し、タップアップすると(押し込みを離すと)枠線を消去してボックスの色を変更するような仕様を考える。
TapboxC
は _active
という状態は親ウィジェットに対して開示するが、 _highlight
という別の状態(枠線の表示を司る状態)は自分自身で管理する。これがmix-and-matchアプローチである。
この例では、 _ParentWidgetState
クラスと _TapboxCState
クラスという2つのStateクラスが登場する。
_ParentWidgetState
クラスは、
-
_active
状態を管理する -
_handleTapboxChanged()
メソッドを持ち、ボックスがタップされたときにそれが呼ばれる - タップが発生して
_active
状態が変化すると、setState()
を読んでUIを更新する
_TapboxCState
クラスは、
-
_highlight
状態を管理す -
GestureDetector
クラスを使ってタップイベントを監視し、タップダウンされたら枠線を表示、タップアップされたら枠線を消去する - タップダウン、タップアップ、タップキャンセルイベントが発生するか、
_highlight
状態が変化するかしたタイミングで、setState()
を読んでUIを更新する - タップイベント発生時、
widget
プロパティを使って自ウィジェットの状態を親ウィジェットに伝える
コード例
// lib/main.dart
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
home: Scaffold(
appBar: AppBar(
title: Text('Flutter Demo'),
),
body: Center(
child: ParentWidget(),
),
),
);
}
}
class ParentWidget extends StatefulWidget {
_ParentWidgetState createState() => _ParentWidgetState();
}
class _ParentWidgetState extends State<ParentWidget> {
bool _active = false;
void _handleTapboxChanged(bool newValue) {
setState(() {
_active = newValue;
});
}
Widget build(BuildContext context) {
return Container(
child: TapboxC(
active: _active,
onChanged: _handleTapboxChanged,
),
);
}
}
class TapboxC extends StatefulWidget {
TapboxC({Key key, this.active: false, this.onChanged})
: super(key: key);
final bool active;
final ValueChanged<bool> onChanged;
_TapboxCState createState() => _TapboxCState();
}
class _TapboxCState extends State<TapboxC> {
bool _highlight = false;
void _handleTapDown(TapDownDetails details) {
setState(() {
_highlight = true;
});
}
void _handleTapUp(TapUpDetails details) {
setState(() {
_highlight = false;
});
}
void _handleTapCancel() {
setState(() {
_highlight = false;
});
}
void _handleTap() {
widget.onChanged(!widget.active);
}
Widget build(BuildContext context) {
// This example adds a green border on tap down.
// On tap up, the square changes to the opposite state.
return GestureDetector(
onTapDown: _handleTapDown, // Handle the tap events in the order that
onTapUp: _handleTapUp, // they occur: down, up, tap, cancel
onTap: _handleTap,
onTapCancel: _handleTapCancel,
child: Container(
child: Center(
child: Text(widget.active ? 'Active' : 'Inactive',
style: TextStyle(fontSize: 32.0, color: Colors.white)),
),
width: 200.0,
height: 200.0,
decoration: BoxDecoration(
color: widget.active ? Colors.lightGreen[700] : Colors.grey[600],
border: _highlight
? Border.all(
color: Colors.teal[700],
width: 10.0,
)
: null,
),
),
);
}
}
この実装例とは逆に、 _active
状態を自身で管理して、 _hightlight
状態を親ウィジェットに開示する方法も考えられるが、直感的ではない。タップボックスウィジェットを利用する開発者のことを考えれば、彼らはボックスがアクティブかどうかに関心はあるが、枠線がどのように表示されるかには関心がないはずだからだ。そうであれば、枠線の表示制御はタップボックス自身が管理してくるほうがよいだろう。
Discussion