【Flutter】画面全体の Loading 表示を、パッケージも Navigator も使わない、一番シンプルな実装の提案
環境
# Flutter
3.22.0
# Dart
3.4
ゴール
Riverpod などのパッケージに頼らず、これだけで実装します。
Future<void> onPressed() async {
await LoadingIndicator.show(() async {
// some Future implements
});
}

Navigator(or showDialog) を使う課題感
- どっかで間違えて Navigator.pop をしてしまうと、消えてしまう。
- そもそも、色んなダイアログが同じライフサイクルで使われていることに違和感がある。
-
RouteAwareが無駄にトリガーしてしまう。 -
BuildContextが必要になってしまう。 - Android でのバックボタンをケアする必要がある。
- Web では大体 boolean で管理されているので、そっちで使いたくなる。
- Router API(go_router など)を使っている場合、こちらの命令形を導入するかどうかの兼ね合いを考えるのがめんどくさい。
実装
1. LoadingIndicator の作成
ValueNotifier を利用して表示切り替えをします。
一番パフォーマンスよく作りやすいからそうしてるだけなので、setState なり hooks なりここはなんでも大丈夫です。
class LoadingIndicator extends StatefulWidget {
const LoadingIndicator({super.key});
State<LoadingIndicator> createState() => _LoadingIndicatorState();
}
class _LoadingIndicatorState extends State<LoadingIndicator> {
final _isLoadingNotifier = ValueNotifier(false);
Widget build(BuildContext context) {
return ValueListenableBuilder(
valueListenable: _isLoadingNotifier,
builder: (context, value, child) {
return Visibility(
visible: value,
child: child!,
);
},
child: const SizedBox.expand(
child: ColoredBox(
color: Colors.black45,
child: Center(child: CircularProgressIndicator()),
),
),
);
}
}
2. MaterialApp#builder にStack を設置
MaterialApp#builder にStack を設置し、LoadingIndicator を上に被せるようにします。
return MatrialApp(
builder: (context, child) {
return Stack(
children: [
Positioned.fill(child: child!),
const Positioned.fill(child: LoadingIndicator()),
]
);
}
)
3. LoadingIndicator.show 関数を作成
外部から利用するための関数を static で定義(完全なグローバルな関数でも問題ないのでお好みで)。
class LoadingIndicator extends StatefulWidget {
const LoadingIndicator({super.key});
+ static Future<void> show(Future<void> Function() cb) async {}
}
4. _LoadingIndicatorState をグローバルな変数に格納する
static な関数から、_LoadingIndicatorState の状態を変えないといけないので、_LoadingIndicatorState に static な関数からアクセスする必要があります。
そのために、まず、再代入可能な変数として、 LoadingIndicator._state に _LoadingIndicatorState を持たせます。
class LoadingIndicator extends StatefulWidget {
const LoadingIndicator({super.key});
static Future<void> show(Future<void> Function() cb) async {}
+ static late _LoadingIndicatorState? _state;
State<LoadingIndicator> createState() => _LoadingIndicatorState();
}
そして、LoadingIndicator._state に _LoadingIndicatorState を格納する処理を書いていきます。
- 代入 ...
didChangeDependencies- State が変更されると毎回実行される関数(
activateやinitStateでも問題なさそうだが、一応)
- State が変更されると毎回実行される関数(
-
nullの再代入 ...dispose- ウィジェットがツリーから永久に外れる時に実行される関数
class _LoadingIndicatorState extends State<LoadingIndicator> {
// 中略
+
+ void didChangeDependencies() {
+ LoadingIndicator._state = this;
+ super.didChangeDependencies();
+ }
+
+ void dispose() {
+ LoadingIndicator._state = null;
+ super.dispose();
+ }
余談ですが、static に State を持たせる方法は、flutter_hooks を参考にしました。
mixin HookElement on ComponentElement {
static HookElement? _currentHookElement;
5. ローディング表示切り替え処理
「ローディングの表示 → Future が完了したら、ローディング非表示」の関数を _LoadingIndicatorState に実装して、LoadingIndicator.show でそれを呼び出す処理を追加します。
Future がエラーで終了してもちゃんと非表示になるように、whenComplete でローディングを非表示にします。
class LoadingIndicator extends StatefulWidget {
const LoadingIndicator({super.key});
static Future<void> show(Future<void> Function() cb) async {
+ await _state!._show(cb);
}
static late _LoadingIndicatorState? _state;
State<LoadingIndicator> createState() => _LoadingIndicatorState();
}
class _LoadingIndicatorState extends State<LoadingIndicator> {
final _isLoadingNotifier = ValueNotifier(false);
+ Future<void> _show(Future<void> Function() cb) async {
+ _isLoadingNotifier.value = true;
+ await cb().whenComplete(() {
+ _isLoadingNotifier.value = false;
+ });
+ }
6. LoadingIndicator._state が null の時のエラーを実装
LoadingIndicator.show() は、LoadingIndicator がビルドされている前提で実行されるものなので、そうなっていない場合はちゃんとエラーを出すようにしましょう。
(エラーメッセージは好きに変えてください)
class LoadingIndicator extends StatefulWidget {
const LoadingIndicator({super.key});
static Future<void> show(Future<void> Function() cb) async {
+ if (_state == null || !_state!.mounted) {
+ throw FlutterError(
+ 'LoadingIndicator.show() was called \n'
+ 'No State of LoadingIndicator widget has created.',
+ );
+ }
await _state!._show(cb);
}
完成 🎉
ボタンを設置して試してみましょう。
Future<void> _onPressed() async {
await LoadingIndicator.show(() async {
await Future.delayed(const Duration(seconds: 3));
});
}

改善案
今回は、極力シンプルに実装したので、以下のような改善案が考えられます。
-
LoadingIndicator._stateが気持ち悪い場合の改善 -
CompleterやAsyncCache.ephemeralで、LoadingIndicator.show()の複数実行対策を行う。 - indicator に value を渡せるようにする。
割とさっき思いついて(保険)、テストとかは何も書いてないので、改善案はなんでもウェルカムです。
Discussion
そう言う経験についての投稿を書いてくれてありがとうございます!便利ですよ!
ありがとうございます!
Thanks!