アプリの開発をしていると、非同期の処理をしている間はローディングをしているようなViewを表示して、その他の入力を受け付けないようにしたいケースがあると思います。
androidのProgressDialogの非推奨の理由を見る限り、googleはそういったUXを排除したいようですが、欲しいものは欲しいのでRiverpodと組み合わせてFutureが完了するまで表示されるローディングViewを実装しました。
環境
Flutter : 3.3.10-stable
Riverpod : 2.1.1
ディレクトリ
lib
├── app.dart
├── main.dart <-- ProviderScopeとMyAppを読み込むだけ
└── loading
├── loading.dart
├── loading_container.dart
├── loading_notifier.dart
└── loading_screen.dart
loadingの状態を管理するnotifierを作る
bool型のStateNotifierを作成
Futureを受け取り、Future実行前にstateをtrue, 実行後にstateをfalseにするメソッドを定義
import 'package:flutter_riverpod/flutter_riverpod.dart';
final loadingProvider = StateNotifierProvider<LoadingNotifier, bool>((ref) => LoadingNotifier());
class LoadingNotifier extends StateNotifier<bool> {
LoadingNotifier() : super(false);
Future<T> whileLoading<T>(Future<T> Function() future) {
return Future.microtask(toLoading)
.then((_) => future())
.whenComplete(toComplete);
}
void toLoading() => state = true;
void toComplete() => state = false;
}
loadingのViewを作る
import 'package:flutter/material.dart';
class Loading extends StatelessWidget {
const Loading({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return DecoratedBox(
// 背景透過のため
decoration: const BoxDecoration(color: Color.fromRGBO(0, 0, 0, 0.4)),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// View本体
Container(
width: 100,
height: 100,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
),
child: const Center(
child: CircularProgressIndicator(),
),
),
],
),
);
}
}
Loading ViewをラップするContainerも作る
ここでloadingの状態を受け取り、状態によってLoading Viewの表示を出し分ける
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:progress_dialog_sample/progress/loading_notifier.dart';
import 'loading.dart';
class LoadingContainer extends ConsumerWidget {
const LoadingContainer({
Key? key,
required this.child,
}) : super(key: key);
final Widget? child;
@override
Widget build(BuildContext context, WidgetRef ref) {
final loading = ref.watch(loadingProvider);
return Stack(
fit: StackFit.expand,
children: [
child ?? const SizedBox(),
loading ? const Loading() : const SizedBox(),
],
);
}
}
builderに渡すためのTransitionBuilderを定義
作っておくと他のbuilderに渡すものと共存させやすいので
import 'package:flutter/material.dart';
import 'loading_container.dart';
mixin LoadingScreen {
static TransitionBuilder init({
TransitionBuilder? builder,
}) {
return (BuildContext context, Widget? child) {
if (builder != null) {
return builder(context, LoadingContainer(child: child));
} else {
return LoadingContainer(child: child);
}
};
}
}
MaterialAppのbuilderに↑を渡す
builderに渡すことで、どの画面でもloading_notifierの状態が変わればローディングのViewを表示させることができる
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
builder: LoadingScreen.init(),
home: const LoadingSample(title: 'loading progress sample'),
);
}
}
動かす
ボタンを押すと3秒delayするFutureが動く
Futureが止まるまでLoadingのViewが表示される