【Flutter】画面全体の Loading 表示を、パッケージも Navigator も使わない、一番シンプルな実装の提案
環境
# Flutter
3.22.0
# Dart
3.4
ゴール
Riverpod などのパッケージに頼らず、これだけで実装します。
Future<void> onPressed() async {
await LoadingIndicator.show(() async {
// some Future implements
});
}
showDialog
) を使う課題感
Navigator(or - どっかで間違えて 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()),
),
),
);
}
}
MaterialApp#builder
にStack
を設置
2. MaterialApp#builder
にStack
を設置し、LoadingIndicator
を上に被せるようにします。
return MatrialApp(
builder: (context, child) {
return Stack(
children: [
Positioned.fill(child: child!),
const Positioned.fill(child: LoadingIndicator()),
]
);
}
)
LoadingIndicator.show
関数を作成
3. 外部から利用するための関数を 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;
+ });
+ }
LoadingIndicator._state
が null の時のエラーを実装
6. 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!