処理の進捗状況を表示するための実装について。
ユーザー認証とか、ファイルからのデータ読み込みなど、待機時間が若干かかる場合に別途ダイアログ画面を表示し、その中に進捗状況を表示させる実装について記述している。
処理途中で他の画面に遷移させたくない、処理のキャンセルができるようにさせたいといったときに、ダイアログにするのかな。
進捗状況表示用ウィジェット
何らか処理に時間がかかる場合に画面によく表示させるのがLinearProgressIndicatorもしくはCircularProgressIndicator。
それぞれ、以下の様な表示を行うウィジェットになる。
- LinearProgressIndicator
- CircularProgressIndicator
コンストラクタのvalueを値を与えることで0~1の間での進捗を表すことができる。
(LinearProgressIndicatorの方はvalueを与えた表示にしている)
意味合い的に0が開始で1が終了といった感じだろうか。
これらインジケーターは基本的にFuture(FutureBuilder)やStream(StreamBuilder)と合わせて使われる場合が多いと思う。
FutureやStreamは非同期処理で、その処理が完了するまで画面にこのインジケーターを表示し、処理中ということをユーザーに示すための物になると思う。
実装例
Futureの場合
Futureで処理中の間インジケーターを表示。完了したらダイアログを閉じるといった感じの実装。
/// Futureを受け取り、それが完了したらダイアログを自動的に閉じる。
void showFutureLoader(BuildContext context, Future future) {
final dialog = AlertDialog(
content: FutureBuilder(
future: future,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
Navigator.pop(context);
}
return Row(
children: [
const CircularProgressIndicator(),
Container(
margin: const EdgeInsets.only(left: 7),
child: const Text("Loading...")),
],
);
}),
);
showDialog(
barrierDismissible: false,
context: context,
builder: (BuildContext context) {
return dialog;
},
);
}
もしFutureが完了したことを呼び出し側で検知したいのであれば、引数で渡すFutureにthenやwhenCompleteを付けて検知させる。
またダイアログの呼び出し部分をシリアルで処理したいのであれば、上記関数をFuture<void> asyncにしshowDialogをawaitで待つ実装を入れたうえで、関数をawaitで待つようにするといった手法を選択できる。
処理キャンセルが可能か
Streamの例では、処理途中キャンセルボタンを押すことで処理の中断をさせるというのは案外簡単に実装できる。
Future側の実装ではどうだろうか。
引数で渡されるFutureの中にforやwhile等のループを作り、そこで停止フラグが真となっているか判断する形で実現はできるだろう。
しかし実際の実装では、Futureから呼び出される処理も単独のFutureが多いと思われるので、forやwhileなどを組み込み処理を抜けるというのは難しいのではと思う。
Streamの例
Stream側から現在の進捗状況を受け取り、それを元にインジケーターの値を更新するような実装。
/// [stream]はdataが0~1のStream<double>で、受け取ったデータで進捗インジケーター
/// を表示する。[stream]は1になったら処理を完了させる。
/// [stop]はインジケーターを停止させるための関数。
void showStreamLoader(BuildContext context, Stream<double> stream,
[void Function()? stop]) {
final dialog = AlertDialog(
content: StreamBuilder<double>(
stream: stream,
builder: (context, snapshot) {
double data = 0.0;
switch (snapshot.connectionState) {
case ConnectionState.none:
break;
case ConnectionState.waiting:
data = snapshot.data ?? data;
break;
case ConnectionState.active:
data = snapshot.data ?? data;
break;
case ConnectionState.done:
data = snapshot.data ?? data;
Navigator.pop(context);
break;
}
return Column(
mainAxisSize: MainAxisSize.min,
children: [
LinearProgressIndicator(
value: data,
minHeight: 30,
),
Text("Loading... ${(data * 100.0).toInt()}"),
],
);
}),
actions: stop == null
? null
: [TextButton(onPressed: stop, child: const Text("Cancel"))],
);
showDialog(
barrierDismissible: false,
context: context,
builder: (BuildContext context) {
return dialog;
},
);
}
Stream側の実装の例は次のような感じ。
/// ロード処理を中止するフラグ
bool _stop = false;
/// 0~99でローダーの進捗状況を表示するためのロード処理部分
Stream<double> countupLoader() async* {
_stop = false;
for (int i = 0; i < 100 && !_stop; i++) {
yield i / 100;
await Future.delayed(const Duration(milliseconds: 50));
}
}
/// ロード処理を中止するための関数
void countupLoaderStop() {
_stop = true;
}
Async*で作成しyieldで現在の進捗状況を返すようにしている。
このような処理を行うことで、処理途中で処理の中断をする処理も実装できる。
補足
StreamBuilderのconnectionStateは、以下の順番で呼び出される。
- waiting(dataはnull)
- active(dataはnot null)
- done(dataはnot null、たぶん最後のyieldの値だと思う)
noneに来ることはなかった。
コメントを見るとinitialDataが指定されていると呼び出されると書かれていたが、試してみたけど呼び出されなかった。
実装上は、initialDataが設定されていてstreamがnullの場合noneになるようだ。
コメント