CounterAppを実装してProviderライフサイクルを学習しよう
経緯
実務で Riverpod を使用するようになり、公式ドキュメントで Provider の学習を始めた。
学習が進み、Provider のライフサイクルについて理解したいと考え、今回の記事を書くに至った。
対象読者
- Riverpod の Provider ライフサイクルについて学習したい方。
Provider ライフサイクルについて
以下は公式ドキュメントの該当箇所である。
When does my Provider get created and disposed?
Provider はいつ作成され、いつ破棄されるのか?
- Uninitialized
- Alive
- Paused
- Disposed
上記 4 つが Provider ライフサイクルの状態。
Uninitialized
An Uninitialized or Disposed provider does not take up any memory since its state is not initialized.
Essentially it is just a definition of how to create the provider's state when you need it.
It will stay that way until an Alive provider or a WidgetRef from the UI reads,
watches, or listens to it.
初期化されていない、または破棄された Provider は、状態が初期化されていないためメモリを消費しない。
Provider の状態は、Alive な Provider もしくは UI からの WidgetRef がそれを read / watch / listen するまで維持される。
Alive
When your provider is Alive,
changes to its state will cause dependent providers
and/or the dependent UI to rebuild.
Provider の状態が Alive の場合、その状態が変更されると依存する Provider や依存する UI が再構築される。
Paused
When an Alive provider is no longer listened to
by other providers or the UI, it goes into a Paused state.
This means that it no longer will react to changes on providers it is listening to.
This is an optimization, as if you are not listening to the provider,
there is no need to keep it alive.
Every provider not being used will be returned to a Paused state,
reducing the computational burden of your app.
Alive 状態の Provider が他の Provider や UI から listen されなくなると、Paused 状態になる。
これは、listen している Provider の変更に反応しなくなることを意味する。
これは最適化であり、Provider を listen していない場合はそれを維持する必要はない。
使用されていないすべての Provider は Paused 状態に戻り、アプリの計算負荷を軽減する。
Disposed
There are a few reasons for a provider to be disposed.
- When defined using the .autoDispose modifier and no longer being watched by the UI or another provider
- When the provider is being manually refreshed or invalidated
- When the provider is being recreated due to one of its watched dependencies changing
Refreshing causes the provider to immediately go through the creation process again,
whereas invalidating causes the next read / watch of the provider to cause the provider to be rebuilt.
Provider が破棄される理由はいくつかある。
-
.autoDispose修飾子を使用して定義され、UI または別の Provider によって watch されなくなった場合。 - Provider が手動で refresh / invalidate された場合。
- watch 対象の依存関係の 1 つが変更され、Provider が再作成される場合。
Counter App のコードを実装して Provider のライフサイクルについて理解しよう
アプリ概要
- Increment ボタンを押すと ( +1 ) された数字が表示されるアプリ。
- 5 の倍数になったら Dialog で「5 の倍数です」を表示する。
フォルダ構成
lib
|
|--logic
| |
| |-counter_notifier.dart
|
|--view
| |
| |-counter_page.dart
|
|
|--main.dart
|
|--provider_observer.dart
View 実装
import 'package:counter_app_riverpod/logic/counter_notifier.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class CounterPage extends ConsumerWidget {
const CounterPage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final counter = ref.watch(counterProvider);
ref.listen(counterProvider, (_, next) {
// 5 の倍数の時、アラートを表示させる。
if (next % 5 == 0) {
showDialog(
context: context,
builder: (context) =>
const AlertDialog(title: Text('5 の倍数'), content: Text('5 の倍数です')),
);
}
});
return Scaffold(
appBar: AppBar(title: const Text('Counter Page')),
body: Column(
children: [
Text('Counter: $counter'),
SizedBox(height: 20),
ElevatedButton(
onPressed: () {
ref.read(counterProvider.notifier).increment();
},
child: const Text('Increment'),
),
],
),
);
}
}
Logic 実装
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
final counterProvider = NotifierProvider.autoDispose<CounterNotifier, int>(
() => CounterNotifier(),
);
class CounterNotifier extends AutoDisposeNotifier<int> {
@override
int build() {
debugPrint('counterProvider: build() が実行された。');
ref.onCancel(() {
debugPrint('counterProvider: onCancel された(listener が 0)。');
});
ref.onResume(() {
debugPrint('counterProvider: onResume された(listener が復活)。');
});
ref.onDispose(() {
debugPrint('counterProvider: onDispose された。');
});
return 0;
}
void increment() {
state++;
}
void decrement() {
state--;
}
}
Uninitialized について
まだ値を生成していない状態。
counterProvider: build() が実行された。
Alive について
Provider のインスタンスが ProviderContainer 内に存在していて、
破棄されていない状態を示す。
counterProvider: build() が実行された。← Alive の結果である。
Disposed について理解する
counterProvider が ProviderContainer から破棄されるタイミングで、保持していたキャッシュも消える。
View に SecondPage を追加して CounterPage から画面遷移して確認する。
import 'package:counter_app_riverpod/logic/counter_notifier.dart';
import 'package:counter_app_riverpod/view/second_page.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class CounterPage extends ConsumerWidget {
const CounterPage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final counter = ref.watch(counterProvider);
ref.listen(counterProvider, (_, next) {
// 5 の倍数の時、アラートを表示させる。
if (next % 5 == 0) {
showDialog(
context: context,
builder: (context) =>
const AlertDialog(title: Text('5 の倍数'), content: Text('5 の倍数です')),
);
}
});
return Scaffold(
appBar: AppBar(title: const Text('Counter Page')),
body: Column(
children: [
Text('Counter: $counter'),
SizedBox(height: 20),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
onPressed: () {
ref.read(counterProvider.notifier).increment();
},
child: const Text('Increment'),
),
SizedBox(width: 30),
ElevatedButton(
onPressed: () {
ref.read(counterProvider.notifier).decrement();
},
child: const Text('Decrement'),
),
],
),
SizedBox(height: 20),
/// これを追加
ElevatedButton(
onPressed: () {
// Navigator.pushReplacement を使用する。
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (context) => SecondPage()),
);
},
child: const Text('Second Page'),
),
],
),
);
}
}
Navigator.pushReplacement を用いることで画面遷移した際に CounterPage の履歴が消えるため、onDispose が実行される。
import 'package:counter_app_riverpod/view/counter_page.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class SecondPage extends ConsumerWidget {
const SecondPage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
return Scaffold(
appBar: AppBar(title: const Text('Second Page')),
body: Column(
children: [
Text('Second Page'),
ElevatedButton(
onPressed: () {
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (context) => CounterPage()),
);
},
child: const Text('Counter Page'),
),
],
),
);
}
}
flutter: counterProvider: build() が実行された。
↓
SecondPage へ画面遷移
↓
flutter: counterProvider: onDispose された。
↓
CounterPage へ画面遷移
↓
flutter: counterProvider: build() が実行された。
上記のような流れになる。

Discussion