Skip to content

Instantly share code, notes, and snippets.

@temoki
Last active December 13, 2024 02:07
Show Gist options
  • Select an option

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

Select an option

Save temoki/0e9acedbfa2b6ebe424f7e856dc2f5f3 to your computer and use it in GitHub Desktop.
//================================================================================
// Flutter State Management Guide (1)
//
// f(State) = UI
//
// It means the UI is a function of the current state.
// When the state changes, the UI automatically updates to reflect it.
//================================================================================
import 'package:flutter/material.dart';
import 'package:provider/provider.dart' as provider;
import 'package:hooks_riverpod/hooks_riverpod.dart' as riverpod;
void main() => runApp(const App());
final class App extends StatelessWidget {
const App({super.key});
@override
Widget build(BuildContext context) {
return riverpod.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: [
_ListItem(
1,
'Stateless',
(_) => Case1Page(),
),
_ListItem(
2,
'Stateful / Single Widget',
(_) => Case2Page(),
),
_ListItem(
3,
'Stateful / Multiple Widgets / State Lift Up',
(_) => Case3Page(),
),
_ListItem(
4,
'Stateful / Multiple Widgets / ChangeNotifier',
(_) => Case4Page(),
),
_ListItem(
5,
'Stateful / Multiple Widgets / ChangeNotifier + InheritedWidget',
(_) => Case5Page(),
),
_ListItem(
6,
'Stateful / Multiple Widgets / ChangeNotifier + Provider',
(_) => Case6Page(),
),
_ListItem(
7,
'Stateful / Multiple Widgets / ChangeNotifier + Riverpod',
(_) => Case7Page(),
),
_ListItem(
8,
'Stateful / Multiple Widgets / Notifier + Riverpod',
(_) => Case8Page(),
),
],
),
),
),
),
);
}
}
final class _ListItem extends StatelessWidget {
_ListItem(this.no, this.description, this.builder);
final int no;
final String description;
final WidgetBuilder builder;
@override
Widget build(BuildContext context) {
final title = 'Case $no';
return ListTile(
title: Text(title),
subtitle: Text(description),
onTap: () => Navigator.of(context).push(
MaterialPageRoute(
builder: (c) => Scaffold(
appBar: AppBar(
title: Text(title),
bottom: _Description(description),
),
body: builder(c),
),
),
),
);
}
}
final class _Description extends StatelessWidget
implements PreferredSizeWidget {
_Description(this.description);
final String description;
@override
Widget build(BuildContext context) {
return SizedBox(
height: 40,
child: Text(description, style: TextStyle(fontSize: 10)),
);
}
@override
Size get preferredSize => Size.fromHeight(40);
}
//================================================================================
// CASE 1 : Stateless
//
// * `StatelessWidget` は状態をもたない Widget
//
// ┌-----------------┐
// │ │
// │ StatelessWidget │
// │ │
// └-----------------┘
//================================================================================
final class Case1Page extends StatelessWidget {
const Case1Page({super.key});
@override
Widget build(BuildContext context) {
debugPrint('Case1Page build');
return Center(
child: Text("I'm static"),
);
}
}
//================================================================================
// CASE 2 : Stateful / Single Widget
//
// * `StatefulWidget` は状態をもつ Widget
// * `State` クラスのフィールドが状態となる
// * `initState()` メソッドをオーバーライドすることで状態の初期化ができる(初回ビルド時のみ実行される)
// * `setState()` メソッドの中でフィールドを更新することでリビルドを促せる
// * この Widget 内のみで参照される状態なので local state や ephemeral state と呼ばれる
//
// ┌-----------------┐
// │ │
// │ StatefulWidget │ ┌-------┐
// │ ┼--------┼ State │
// └-----------------┘ └-------┘
//================================================================================
final class Case2Page extends StatefulWidget {
const Case2Page({super.key});
State<Case2Page> createState() => Case2State();
}
final class Case2State extends State<Case2Page> {
Case2State();
int _count = -1;
@override
void initState() {
_count = 0;
super.initState();
}
@override
Widget build(BuildContext context) {
debugPrint('Case2Page build : $_count');
return Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text("Count = $_count"),
const SizedBox(height: 8),
FilledButton(
onPressed: () {
setState(() => _count++);
},
child: const Text('+'),
),
],
),
);
}
}
//================================================================================
// CASE 3 : Stateful / Multiple Widgets / State Lift Up
//
// * 複数の Widget で共通の状態がある場合、それらの共通の祖先の Widget まで状態を移動させる
// * 祖先の Widget から、状態を必要とする子孫の Widge まで状態を伝搬させる(バケツリレー)
//
// ┌---------------┐
// │StatelessWidget│
// │ (A) │
// └-------┬-------┘
// │
// ┌------------┴-------------┐
// ┌-------┴-------┐ ┌-------┴-------┐
// │StatefulWidget │ ┌-----┐ │StatelessWidget│
// │ (B) ┼-┤State│ │ (C) │
// └---------------┘ └-----┘ └---------------┘
//
// ↓ State Lift Up! ↓
//
// ┌---------------┐
// │StatefulWidget │ ┌-----┐
// │ (A) ├-┤State│
// └-------┬-------┘ └-----┘
// │
// ┌------------┴-------------┐
// ┌-------┴-------┐ ┌-------┴-------┐
// │StatelessWidget│ │StatelessWidget│
// │ (B) │ │ (C) │
// └---------------┘ └---------------┘
//
// Widget (has state)
// ├ Child Widget (use state)
// └ Button (update state)
//================================================================================
final class Case3Page extends StatefulWidget {
const Case3Page({super.key});
State<Case3Page> createState() => Case3State();
}
final class Case3State extends State<Case3Page> {
Case3State();
int _count = 0;
@override
Widget build(BuildContext context) {
debugPrint('Case3State build');
return Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
CountWidget(count: _count),
CountDoubleWidget(count: _count),
EvenOddWidget(count: _count),
const SizedBox(height: 8),
FilledButton(
onPressed: () => setState(() => _count++),
child: const Text('+'),
),
],
),
);
}
}
final class CountWidget extends StatelessWidget {
const CountWidget({super.key, required this.count});
final int count;
@override
Widget build(BuildContext context) {
return Container(
color: Colors.cyan,
padding: EdgeInsets.all(8),
child: Text('CountWidget\nCount = $count'),
);
}
}
final class CountDoubleWidget extends StatelessWidget {
const CountDoubleWidget({super.key, required this.count});
final int count;
@override
Widget build(BuildContext context) {
return Container(
color: Colors.lime,
padding: EdgeInsets.all(8),
child: Text('CountDoubleWidget\nCount x 2 = ${count * 2}'),
);
}
}
final class EvenOddWidget extends StatelessWidget {
const EvenOddWidget({super.key, required this.count});
final int count;
@override
Widget build(BuildContext context) {
return Container(
color: Colors.yellow,
padding: EdgeInsets.all(8),
child: Text('EvenOddWidget\nCount is ${count % 2 == 0 ? 'even' : 'odd'}'),
);
}
}
//================================================================================
// CASE 4 : Stateful / Multiple Widgets / ChangeNotifier
//
// * 状態とその状態更新ロジックを同居させられるのが `ChangeNotifier`
// * https://api.flutter.dev/flutter/foundation/ChangeNotifier-class.html
// * `notifyListeners()` により状態の更新を通知することができる
// * `ListenableBuilder()` に `ChangeNotifier` を指定すると、その配下の Widget は
// `ChangeNotifier` の状態が更新されるたびにリビルドされる
//
// ┌-----------------┐
// │ │ ┌--------------┐
// │ StatelessWidget │ │ChangeNotifier│
// │ ┼--┼ (State) │
// └--------┬--------┘ └--------------┘
// │
// ┌--------┴--------┐
// │ListenableBuilder│
// └--------┬--------┘
// │
// ┌----------┴-----------┐
// │Widget that uses state│
// └----------------------┘
//
// Widget (has ChangeNotifier)
// └ ListenableBuilder (Rebuild on ChangeNotifier change)
// ├ Child Widget (use state via ChangeNotifier)
// └ Button (update state via ChangeNotifier)
//================================================================================
final class CounterChangeNotifier with ChangeNotifier {
int _count = 0;
int get count => _count;
void increment() {
_count++;
notifyListeners();
}
}
final class Case4Page extends StatelessWidget {
Case4Page();
final _counter = CounterChangeNotifier();
@override
Widget build(BuildContext context) {
debugPrint('Case4Page build');
return ListenableBuilder(
listenable: _counter,
builder: (_, __) {
debugPrint('Case4Page/ListenableBuilder child build');
return Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
CountWidget(count: _counter.count),
CountDoubleWidget(count: _counter.count),
EvenOddWidget(count: _counter.count),
const SizedBox(height: 8),
FilledButton(
onPressed: () => _counter.increment(),
child: const Text('+'),
),
],
),
);
},
);
}
}
//================================================================================
// CASE 5 : Stateful / Multiple Widgets / ChangeNotifier + InheritedWidget
//
// * `InheritedWidget` の子孫の Widget は `{InheritedWidget}.of(context)` にて
// 祖先にいる最初の `InheritedWidget` を取得することができる
// * https://api.flutter.dev/flutter/widgets/InheritedWidget-class.html
// * `InheritedWidget` の実装が面倒、わかりにくい
//
// ┌-----------------┐
// │ │ ┌--------------┐
// │ InheritedWidget │ │ChangeNotifier│
// ┌---►│ ┼--┼ (State) │
// │ └--------┬--------┘ └--------------┘
// │ │
// │ ┌--------┴--------┐
// │ │ (Child) │
// │ └--------┬--------┘
// │ │
// │ ...
// │ │
// │of ┌--------┴--------┐
// └----┼ (Descendant) │
// └--------┬--------┘
// │
// ┌--------┴--------┐
// │ListenableBuilder│
// └--------┬--------┘
// │
// ┌----------┴-----------┐
// │Widget that uses state│
// └----------------------┘
//
// InheritedWidget (has ChangeNotifier)
// └ Child Widget
// └ ...
// └ ListenableBuilder (Rebuild on ChangeNotifier change)
// ├ Descendant Widget 1 (use state via ChangeNotifier)
// └ Button (update state via ChangeNotifier)
//================================================================================
class CounterInheritedWidget extends InheritedWidget {
const CounterInheritedWidget({
super.key,
required this.counter,
required super.child,
});
final CounterChangeNotifier counter;
@override
bool updateShouldNotify(CounterInheritedWidget oldWidget) => false;
static CounterInheritedWidget of(BuildContext context) {
final widget = context
.getElementForInheritedWidgetOfExactType<CounterInheritedWidget>()
?.widget as CounterInheritedWidget?;
if (widget != null) {
return widget;
}
throw Exception('No Case3CounterInheritedWidget found in context');
}
}
final class Case5Page extends StatelessWidget {
Case5Page();
@override
Widget build(BuildContext context) {
debugPrint('Case5Page build');
return CounterInheritedWidget(
counter: CounterChangeNotifier(),
child: Case5ChildWidget(),
);
}
}
final class Case5ChildWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
debugPrint('Case5ChildWidget build');
final counter = CounterInheritedWidget.of(context).counter;
return ListenableBuilder(
listenable: counter,
builder: (_, __) {
debugPrint('Case5ChildWidget/ListenableBuilder child build');
return Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
CountWidget(count: counter.count),
CountDoubleWidget(count: counter.count),
EvenOddWidget(count: counter.count),
const SizedBox(height: 8),
FilledButton(
onPressed: () => counter.increment(),
child: const Text('+'),
),
],
),
);
},
);
}
}
//================================================================================
// CASE 6 : Stateful / Multiple Widgets / ChangeNotifier + Provider
//
// * `InheritedWidget` と同等のことを簡単にしてくれるのが 3rd Party の Provider パッケージ
// * https://pub.dev/packages/provider
// * Flutter の公式ドキュメントにおいても状態管理手法の1つとして紹介されている
// * https://docs.flutter.dev/data-and-backend/state-mgmt/simple
//
// ┌-----------------┐ ┌--------------┐
// │ ChangeNotifier │ │ChangeNotifier│
// ┌---►│ Provider ┼--┼ (State) │
// │ └--------┬--------┘ └--------------┘
// │ │
// │ ┌--------┴--------┐
// │ │ (Child) │
// │ └--------┬--------┘
// │ │
// │ ...
// │ │
// │of ┌--------┴--------┐
// └----┼ (Descendant) │
// └--------┬--------┘
// │
// ┌----------┴-----------┐
// │Widget that uses state│
// └----------------------┘
//
// Widget
// └ ChangeNotifierProvider (has ChangeNotifier)
// └ ...
// └ Consumer<ChangeNotifier> (Rebuild on ChangeNotifier change)
// ├ Descendant Widget 1 (use state via ChangeNotifier)
// └ Button (update state via ChangeNotifier)
//================================================================================
final class Case6Page extends StatelessWidget {
Case6Page();
@override
Widget build(BuildContext context) {
debugPrint('Case6Page build');
return provider.ChangeNotifierProvider(
create: (context) => CounterChangeNotifier(),
child: Case6ChildWidget(),
);
}
}
final class Case6ChildWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
debugPrint('Case6ChildWidget build');
return provider.Consumer<CounterChangeNotifier>(
builder: (_, counter, __) {
debugPrint('Case5ChildWidget/Consumer child build');
return Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
CountWidget(count: counter.count),
CountDoubleWidget(count: counter.count),
EvenOddWidget(count: counter.count),
const SizedBox(height: 8),
FilledButton(
onPressed: () => counter.increment(),
child: const Text('+'),
),
],
),
);
},
);
}
}
//================================================================================
// CASE 7 : Stateful / Multiple Widgets / ChangeNotifier + Riverpod
//
// * Provider パッケージの作者(Remiさん)が、Provider の問題点を解消すべく開発したのが Riverpod
// * https://riverpod.dev
// * 名称は Provider のアナグラム
// * Provider パッケージでは同じクラスのオブジェクトを複数提供できない
// * こちらも Flutter の公式ドキュメントにおいても状態管理手法の1つとして紹介されている
// * https://docs.flutter.dev/data-and-backend/state-mgmt/simple
// * Riverpod の Provider(同じ名前でややこしい)は、提供したいオブジェクトの生成方法と
// そのオブジェクトにアクセスするためのキーを担う
// * Riverpod の Provider にアクセスできるのは `ConsumerWidget` を継承した Widget のみで、
// ビルドメソッドに追加された `WidgetRef` を介してアクセスする
// * `WidgetRef` の `watch` を使えば、そのオブジェクトの状態が変わった時にリビルドが走る
//
// ProviderScope (has ChangeNotifier)
// └ ...
// └ ConsumerWidget (watch ChangeNotifier via Provider, Rebuild on ChangeNotifier change)
// ├ Descendant Widget 1 (use state via ChangeNotifier)
// └ Button (update state via ChangeNotifier)
//================================================================================
final counterChangeNotifierProvider =
riverpod.ChangeNotifierProvider<CounterChangeNotifier>((ref) {
return CounterChangeNotifier();
});
final class Case7Page extends riverpod.ConsumerWidget {
@override
Widget build(BuildContext context, riverpod.WidgetRef ref) {
debugPrint('Case7Page build');
final counter = ref.watch(counterChangeNotifierProvider);
return Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
CountWidget(count: counter.count),
CountDoubleWidget(count: counter.count),
EvenOddWidget(count: counter.count),
const SizedBox(height: 8),
FilledButton(
onPressed: () => counter.increment(),
child: const Text('+'),
),
],
),
);
}
}
//================================================================================
// CASE 8 : Stateful / Multiple Widgets / Notifier + Riverpod
//
// * Riverpod が `ChangeNotifier` の代替として提供するのが `Notifier`
// * `Notifier` がもつ `ref` により他の Provider が提供するオブジェクトにアクセスできる
// * `Notifier` の Provider となるのが `NotifierProvider` で、こちらも `ConsumerWidget`
// を継承した Widget から `WidgetRef` を介してアクセスする
// * `ref.watch(someNotifierProvider)` で `Notifier` のもつ状態変化によりリビルドできる
// * Provider の `select` で、状態のフィルタリングができる
// * `ref.read(someNotifierProvider.notifier)` で `Notifier` そのものにアクセスできる
// * `ref.listen(someNotifierProvider, ...)` で状態変化を検知できる
// * `ref.invalidate(someNotifierProvider)` で `Notifier` のリビルドができる
//
// ProviderScope (has ChangeNotifier)
// └ ...
// └ ConsumerWidget (watch ChangeNotifier via Provider, Rebuild on ChangeNotifier change)
// ├ Descendant Widget 1 (use state via ChangeNotifier)
// └ Button (update state via ChangeNotifier)
//================================================================================
final class CounterNotifier extends riverpod.Notifier<int> {
@override
int build() {
// You can watch the other provider
// final hoge = ref.watch(otherStateProvider);
return 0;
}
void increment() => state++;
}
final counterNotifierProvider =
riverpod.NotifierProvider<CounterNotifier, int>(() {
return CounterNotifier();
});
final class Case8Page extends riverpod.ConsumerWidget {
const Case8Page({super.key});
@override
Widget build(BuildContext context, riverpod.WidgetRef ref) {
debugPrint('Case8Page build');
final counter = ref.read(counterNotifierProvider.notifier);
ref.listen(counterNotifierProvider.select((count) => count ~/ 10),
(_, next) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Count is a multiple of 10 : $next')),
);
});
return Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
CountConsumerWidget(),
CountDoubleConsumerWidget(),
EvenOddConsumerWidget(),
const SizedBox(height: 8),
FilledButton(
onPressed: () => counter.increment(),
child: const Text('+'),
),
const SizedBox(height: 4),
FilledButton(
onPressed: () => ref.invalidate(counterNotifierProvider),
child: const Text('Reset'),
),
],
),
);
}
}
final class CountConsumerWidget extends riverpod.ConsumerWidget {
const CountConsumerWidget({super.key});
@override
Widget build(BuildContext context, riverpod.WidgetRef ref) {
final count = ref.watch(counterNotifierProvider);
return Container(
color: Colors.cyan,
padding: EdgeInsets.all(8),
child: Text('CountConsumerWidget\nCount = $count'),
);
}
}
final class CountDoubleConsumerWidget extends riverpod.ConsumerWidget {
const CountDoubleConsumerWidget({super.key});
@override
Widget build(BuildContext context, riverpod.WidgetRef ref) {
final countDouble =
ref.watch(counterNotifierProvider.select((count) => count * 2));
return Container(
color: Colors.lime,
padding: EdgeInsets.all(8),
child: Text('CountDoubleConsumerWidget\nCount × 2 = $countDouble'),
);
}
}
final class EvenOddConsumerWidget extends riverpod.ConsumerWidget {
const EvenOddConsumerWidget({super.key});
@override
Widget build(BuildContext context, riverpod.WidgetRef ref) {
final isEven =
ref.watch(counterNotifierProvider.select((count) => count % 2 == 0));
return Container(
color: Colors.yellow,
padding: EdgeInsets.all(8),
child: Text('EvenOddConsumerWidget\nCount is ${isEven ? 'even' : 'odd'}'),
);
}
}
//================================================================================
// Others : https://github.com/temoki/flutter_state_management
//================================================================================
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment