Skip to content

Instantly share code, notes, and snippets.

@temoki
Last active December 16, 2024 11:59
Show Gist options
  • Select an option

  • Save temoki/aedf93fcc19ea97fc6b94622d53dc87a to your computer and use it in GitHub Desktop.

Select an option

Save temoki/aedf93fcc19ea97fc6b94622d53dc87a to your computer and use it in GitHub Desktop.
//================================================================================
// Flutter State Management Guide (2)
//
// 前回の続き
// https://gist.github.com/temoki/0e9acedbfa2b6ebe424f7e856dc2f5f3
//================================================================================
import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
final items = <Widget>[
Item((_) => PageA(), '[A] Widgetがリビルドされるタイミング'),
Item((_) => PageB(), '[B] Widgetのライフサイクル'),
Item((_) => PageC(), '[C] Flutter Hooks の基本 (useState)'),
Item((_) => PageD(), '[D] Flutter Hooks の基本 (useEffect)'),
Item((_) => PageE(), '[E] Flutter Hooks の基本 (use~Controller)'),
Item((_) => PageF(), '[F] Riverpod の各機能'),
];
//================================================================================
// [A] Widget がリビルドされるタイミングについて
//================================================================================
final class PageA extends StatelessWidget {
const PageA({super.key});
@override
Widget build(BuildContext context) => CounterWidget();
}
// (1) Count値を状態に持つよくある StatefulWidget を例に解説していきます。
final class CounterWidget extends StatefulWidget {
const CounterWidget({super.key});
@override
State<CounterWidget> createState() => CounterState();
}
final class CounterState extends State<CounterWidget> {
CounterState();
int _count = 0;
@override
void initState() {
debugPrint('[CounterState] initState');
super.initState();
}
@override
Widget build(BuildContext context) {
debugPrint('[CounterState] build');
return Center(
// (5) この Rainbow は自身がリビルトされると枠の色が変わるようになっています。
// +ボタンを押すたびに CounterWidget がリビルドされ、その 子Widget にも伝搬します。
// Rainbow で囲まれた 各Widget の色が変わることで 子Widget のリビルドがわかります。
child: Rainbow(
child: Container(
width: 200,
padding: const EdgeInsets.all(8),
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// (6) ただし、この Rainbow は枠の色が変わりません。
// const(=不変なもの)として定義されているためです。
// 不要なリビルドを抑えるために可能なものは const にするのが良いです。
const Rainbow(
child: Text('Count', textAlign: TextAlign.center),
),
// (7) この Widget は Count という変数に依存するので const にできません。
Rainbow(
// (4) リビルド時に更新された Count値 を使うので UI 上の Count値 も更新されます。
child: Text('$_count', textAlign: TextAlign.center),
),
const SizedBox(height: 24),
Rainbow(
// (2) この+ボタンを押すと Count値 がインクリメントされます。
child: FilledButton(
// (3) ボタン押下時に setState() で Count値 を更新しています。
// StatefulWidget は setState() をトリガーに自身をリビルドします。
onPressed: () => setState(() => _count++),
child: const Text('+'),
),
),
],
),
),
),
);
}
}
// (8) const にできる Widget を定義するための条件
// スーパークラスが const である(非constな NonConstClass に変更したら...)
class SomeClass extends ConstClass {
// コンストラクタが const である
const SomeClass({required this.value1, required this.value2}) : super();
// すべてのフィールドが final である(一部のフィールドの final を外すと...)
final int value1;
final int value2;
}
class ConstClass {
const ConstClass();
}
class NonConstClass {
NonConstClass();
}
//================================================================================
// [B] Widgetのライフサイクル
//================================================================================
// (1) そもそも Widget はビルドのたびに新しいインスタンスが再生成されます(constを除く)。
// リビルドで StatefulWidget が再生成されても、その State は生き続けている。
// では、誰が Widget のライフサイクルを管理しているのでしょうか? 👉 Element というもの。
// 内部的には Widgetツリー と対となる Elementツリー が構築されています。
// (参考) https://docs.flutter.dev/resources/architectural-overview
// (2) Element の役割は?
// ・Widget とその状態のライフサイクル管理
// ・ビルド,リビルドの管理
// ・Widget の親子関係の管理
// build メソッドの BuildContext は実は Element そのものだったりします。
// (参考) https://zenn.dev/chooyan/books/934f823764db62/viewer/3d3f8e
// (3) Widget ツリーからある Widget がいなくなると、その対となる Element も破棄されるデモ
final class PageB extends StatefulWidget {
const PageB({super.key});
@override
State<PageB> createState() => PageBState();
}
final class PageBState extends State<PageB> {
PageBState();
bool _flag = true;
@override
Widget build(BuildContext context) {
return Stack(
children: [
// (4) Switch で _flag が true/false と切り替わるようになっています。
Switch(
value: _flag,
onChanged: (newValue) => setState(
() => _flag = newValue,
),
),
// (5) これは[A]の Counter と同じもの。ボタン押下で Count値 がインクリメント。
// ただ、_flag が false になると Widget ツリーから消えるようにしてあります。
// 👉 消えるとともに Element も破棄される、つまり State も破棄される。
if (_flag) const CounterWidget(),
// (6) 状態を保持したまま非表示にしたい場合は Visibility Widget を使いましょう。
// Visibility(
// visible: _flag,
// maintainState: true, // このフラグを true にすることで状態が保持されます
// child: const CounterWidget(),
// ),
],
);
}
}
//================================================================================
// [C] Flutter Hooks の基本 (useState)
//================================================================================
// (1) Flutter Hooks は、一言で言うと StatefulWidget の代替となるものです。
// つまり、Widget ローカルな状態の管理を行うためのものです。
// StatefulWidget よりもかなりシンプルに実装することができます。
// 例として StatefulWidget で実装された Counter を Flutter Hooks に置き換えてみます。
final class PageC extends StatelessWidget {
const PageC();
@override
Widget build(BuildContext context) {
return const Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text('StatefulWidget'),
CounterStatefulWidget(),
SizedBox(height: 32),
Text('Flutter Hooks'),
CounterHookWidget(),
],
),
);
}
}
// (2) StatefulWidget の例
final class CounterStatefulWidget extends StatefulWidget {
const CounterStatefulWidget({super.key});
@override
State<CounterStatefulWidget> createState() => CounterStatefulWidgetState();
}
final class CounterStatefulWidgetState extends State<CounterStatefulWidget> {
CounterStatefulWidgetState();
int _count = 0;
@override
void initState() => super.initState();
@override
Widget build(BuildContext context) {
return Column(children: [
Text('$_count'),
FilledButton(
onPressed: () => setState(() => _count++),
child: const Text('+'),
)
]);
}
}
// (3) Flutter Hooks の例。
// HookWidget を継承して Widget を定義するだけです。State の定義が不要。
final class CounterHookWidget extends HookWidget {
const CounterHookWidget({super.key});
@override
Widget build(BuildContext context) {
// (4) build メソッド内で useState を使って状態を定義します。
// 初回ビルド時は引数で渡された初期値となります。
final count = useState<int>(0);
return Column(children: [
// (5) 状態の値を参照するには value フィールドを参照します。
Text('${count.value}'),
FilledButton(
// (6) 状態の値を更新するには value フィールドを更新するだけです。
// setState しなくてもリビルドが発生します。
onPressed: () => count.value++,
child: const Text('+'),
)
]);
}
}
// (7) なぜ Hooks と言うのか?
// UI(Widget)に状態をひっかける(フックする)という意味らしいです。
// 宣言的UI の祖である React フレームワークが公式に提供する機能を Flutter にもってきた。
// (参考) https://ja.react.dev/reference/react/useState
// (8) useState 以外にも use~ という名前で便利なフックが用意されているし、
// 自分で独自のカスタムフックを作ることもできます。
// (参考) https://github.com/rrousselGit/flutter_hooks#existing-hooks
//================================================================================
// [D] Flutter Hooks の基本 (useEffect)
//================================================================================
// (1) useEffect は Widget の特定のビルドのタイミングで処理(= Effect)を実行するためのものです。
// 指定する keys が前回のビルド時から変化した時だけ Effect が実行されます。
// 初回ビルド時だけ処理を実行したい時などでよく利用されます(StatefulWidgetのinitStateの代替)
final class PageD extends HookWidget {
const PageD({super.key});
@override
Widget build(BuildContext context) {
final count1 = useState<int>(0);
final count2 = useState<int>(0);
// (2) build メソッドの中で useEffect を呼び出します。
// 特定のビルドのタイミングで第1引数に指定した Function が実行されます。
// ここでは第2引数 keys が空なので初回ビルド時だけ実行されます。
useEffect(() {
debugPrint('[PageD] useEffect');
return null;
}, const []);
// (3) keys に指定された count1 が変化した時だけ実行されます。
useEffect(() {
debugPrint('[PageD] useEffect for count1 => ${count1.value}');
return null;
}, [count1.value]);
// (4) keys に指定された count2 が変化した時だけ実行されます。
useEffect(() {
debugPrint('[PageD] useEffect for count2 => ${count2.value}');
return null;
}, [count2.value]);
return Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text("Count 1 = ${count1.value}"),
FilledButton(
onPressed: () => count1.value++,
child: const Text('+'),
),
const SizedBox(height: 8),
Text("Count 2 = ${count2.value}"),
FilledButton(
onPressed: () => count2.value++,
child: const Text('+'),
),
const SizedBox(height: 8)
],
),
);
}
}
//================================================================================
// [E] Flutter Hooks の基本 (use~Controller)
//================================================================================
// (1) use~Controller は Flutter の TextEditingController や AnimationController など、
// UI をコントロールするために提供されている Controller を扱いやすくしてくれます。
final class PageE extends HookWidget {
const PageE();
@override
Widget build(BuildContext context) {
return Center(
child: Padding(
padding: EdgeInsets.all(32),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text('StatefulWidget'),
TextFieldStatefulWidget(),
SizedBox(height: 32),
Text('Flutter Hooks'),
TextFieldHookWidget(),
],
),
),
);
}
}
// (2) StatefulWidget の例
final class TextFieldStatefulWidget extends StatefulWidget {
const TextFieldStatefulWidget({super.key});
@override
State<TextFieldStatefulWidget> createState() =>
TextFieldStatefulWidgetState();
}
final class TextFieldStatefulWidgetState
extends State<TextFieldStatefulWidget> {
TextFieldStatefulWidgetState();
late final TextEditingController _controller;
@override
void initState() {
// (3) Controller の生成は initState の中で
_controller = TextEditingController(text: 'initial text');
super.initState();
}
@override
void dispose() {
// (4) 不要になった時点で dispose メソッドを呼んであげなければなりません
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return TextField(
controller: _controller,
decoration: InputDecoration(
suffixIcon: IconButton(
icon: Icon(Icons.clear),
onPressed: () => _controller.text = '',
),
),
);
}
}
// (3) HookWidget + useTextEditingController の例
final class TextFieldHookWidget extends HookWidget {
const TextFieldHookWidget({super.key});
@override
Widget build(BuildContext context) {
// (4) build メソッド内で useTextEditingController を使って Controller を取得します。
// dispose といったライフサイクルの管理は勝手にやってくれるので不要です。
final controller = useTextEditingController(text: 'initial text');
return TextField(
controller: controller,
decoration: InputDecoration(
suffixIcon: IconButton(
icon: Icon(Icons.clear),
onPressed: () => controller.text = '',
),
),
);
}
}
//================================================================================
// [F] Riverpod の各機能
//================================================================================
// 公式ドキュメント
// https://dartpad.dev/?id=aedf93fcc19ea97fc6b94622d53dc87a
//
// 仕組みから理解する Riverpod / Riverpod チートシート
// https://zenn.dev/chooyan/books/92a0a489f68233/viewer/overview
final class PageF extends StatelessWidget {
const PageF();
@override
Widget build(BuildContext context) {
return const Center(
child: Text('No contents'),
);
}
}
//================================================================================
// Common
//================================================================================
void main() => runApp(const App());
final class App extends StatelessWidget {
const App({super.key});
@override
Widget build(BuildContext context) {
return ProviderScope(
child: MediaQuery(
data: MediaQuery.of(context).copyWith(
textScaler: TextScaler.linear(2.0),
),
child: MaterialApp(
title: 'Flutter State Management Guide',
debugShowCheckedModeBanner: false,
theme: ThemeData(colorSchemeSeed: Colors.blue),
home: Scaffold(
appBar: AppBar(title: Text('Flutter State Management')),
body: ListView(children: items),
),
),
),
);
}
}
final class Item extends StatelessWidget {
Item(this.builder, this.title, {super.key});
final String title;
final WidgetBuilder builder;
@override
Widget build(BuildContext context) {
return ListTile(
title: Text(title),
onTap: () => Navigator.of(context).push(
MaterialPageRoute(
builder: (c) => Scaffold(
appBar: AppBar(
title: Text(title),
),
body: builder(c),
),
),
),
);
}
}
final class Rainbow extends StatelessWidget {
const Rainbow({required this.child, super.key});
final Widget child;
@override
Widget build(BuildContext context) => RepaintBoundary(
key: super.key,
child: CustomPaint(
painter: _RepaintPainter(),
child: child,
),
);
}
final class _RepaintPainter extends CustomPainter {
_RepaintPainter();
final _num = math.Random().nextInt((1 << 31) - 1);
@override
void paint(Canvas canvas, Size size) {
int k = 0;
int r, g, b = 0;
double lum = 0.0;
do {
k += 1;
r = (_num * k * 64) % 255;
g = (_num * k * 128) % 255;
b = (_num * k * 192) % 255;
lum = 0.2126 * (r / 255.0) + 0.7152 * (g / 255.0) + 0.0722 * (b / 255.0);
} while (lum > 0.8);
canvas.drawRect(
Offset(1, 1) & Size(size.width - 2, size.height - 2),
Paint()
..color = Color.fromARGB(255, r, g, b)
..style = PaintingStyle.stroke
..strokeWidth = 2,
);
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => true;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment