本記事は CBcloud Advent Calendar 2020の20日目の記事です.
自分はCBcloudに,toCサービスであるPickGo 買い物のアプリを主に担当するモバイルアプリエンジニアとして参画しています.
PickGo 買い物は,プロのドライバーさんに買い物を代行してもらえ,欲しい物が当日すぐに届くアプリです.
現在送料無料キャンペーンをやっているので良かったらこの機会に是非試してみてください!
この記事では,自分が業務中に編み出したテクニックの一つを紹介します.
ダイアログを出す時に非同期処理をしたい
ユーザーの操作からダイアログを出すとき,その内容をAPIから取得したいシチュエーションを考えます.
何かしら非同期で通信している場合は,ロード中であることをこのようにインジケーターを出すことで親切になります.これは別に元のページでやっても良いのですが,どうせダイアログを出すのであればダイアログ上でやったほうがなんか見た目も良い気がします.
これを実現するために,まずダイアログのWidget上で普通のページ用Widgetと同じようにStatefulWidget
でいうinitState
(ChangeNotifier
でいうコンストラクタ)で非同期処理をすることを考えました.
…しかしただやるだけではうまく行かず.ちょっと前の話なので記憶が曖昧ですが,確かProviderでうまくChangeNotifier
を呼び出せなかったことが原因だったと思います.おとなしくStatefukWidget
を使ってたらできていたかもしれませんが,StatefulWidget
をなるべく使いたくない気持ちが勝って更に探っていました.
実装方法
色々考えたところ,以下の方法で落ち着きました.
-
showDialog
のラッパー関数を用意する - 内部で
Provider.value
を使い,ViewModel的な役割を果たすControllerを利用する - ラッパーの引数に実際に行う非同期処理の内容と,その結果を含んだ
WidgetBuilder
的なものを定義しておく
実装はこのようになります.
typedef ResultBuilder<R> = Widget Function(BuildContext, R);
typedef PreProcess<R> = Future<R> Function();
Future<T> showAsyncDialog<T, R>({
BuildContext context,
ResultBuilder<R> resultDialogBuilder,
PreProcess<R> process,
}) async {
final controller = _AsyncDialogController(process: process);
final dialogResult = await showDialog(
context: context,
builder: (context) {
final dialog = Builder(
builder: (context) {
final isLoading = context.select(
(_AsyncDialogController<R> controller) => controller.isLoading);
final result = context.select(
(_AsyncDialogController<R> controller) => controller.result);
return isLoading
? const PlaceholderDialog()
: resultDialogBuilder(context, result);
},
);
return ChangeNotifierProvider.value(
value: controller,
child: dialog,
);
});
return dialogResult;
}
class _AsyncDialogController<T> extends ChangeNotifier {
_AsyncDialogController({this.process}) {
Future(() async {
result = await process();
isLoading = false;
notifyListeners();
});
}
bool isLoading = true;
final Future<T> Function() process;
T result;
}
ダイアログ中のProviderアクセスは https://stackoverflow.com/questions/57968453/how-to-access-provider-providers-in-dialogs-in-flutter を参考にしました.
PlaceholderDialog
はロード中に出すダイアログで,結果のダイアログとは別物にしてあります.
ここの実装を工夫すれば処理完了した時にアニメーション付きで結果のダイアログがスムーズに出せそうな気がします.
利用側はこんな感じです.
TextButton(
onPressed: () => showAsyncDialog<void, String>(
context: context,
resultDialogBuilder: (context, result) => AlertDialog(
content: Text(result),
),
process: () async {
await Future.delayed(const Duration(seconds: 2));
return 'hoge';
},
),
child: Text('Tap me'),
),
実装してみればなんてことのないものでしたが,個人的に作っておくと結構便利だなと思いました.