アプリ開発をしている時、リストのセルをタップして動作確認のダイアログでOKかキャンセルかを表示した後、処理を走らせて終わったら次の画面に遷移させるのってどうやるのかなって思って色々調べたのでそのまとめとして書きました。
完成動画
Dialog
のボタンから処理をして、その後別画面に遷移させようと思った時、ローディング画面がダイアログの背面に移ってしまい、さらに遷移先から戻ってきた時にも、ダイアログが表示されたままになってしまっている。
ダイアログが戻ってきた時にも表示されたままの問題は遷移の方法をpush
からpushReplace
に変えればいい。
けど、ローディング画面がダイアログの背面に回ってしまうのはどうにかしたい。
これの何が問題かと言うと、ローディング中に何度もダイアログのボタンを押せてしまい、バグの元になってしまう。
こっちがそれらの問題を解決した方。
ダイアログのOKを押した後にダイアログを閉じて、その後ローディング。
それが終わると次の画面に遷移する。
これなら、先ほどの問題を解決できる
全容
まずこれがサンプルの全体のコード。
やっていることは、リストを表示させてそのセルをタップしたらダイアログを表示。そこで「OK」を押せば_refresh_
処理が走り、終わったら次のページに遷移。もし、「キャンセル」が押されたら何もせずにダイアログを閉じると言う簡単なもの。
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
@override
State<StatefulWidget> createState() => MyHomePageState();
}
class MyHomePageState extends State<MyHomePage> {
List<int> listItem = [1, 2, 3];
bool isLoading = false;
Future<void> _refresh() async {
// ローディング
setState(() {
isLoading = true;
});
// 1秒待つ
await Future.delayed(Duration(seconds: 1));
// 新しいリストを代入
setState(() {
listItem = listItem.map((f) => f + 3).toList();
});
// 1秒待つ
await Future.delayed(Duration(seconds: 1));
// ロード終わり
setState(() {
isLoading = false;
});
}
@override
Widget build(BuildContext context) {
return Stack(
children: <Widget>[
Scaffold(
appBar: AppBar(title: Text('Dialog'),),
body: ListView.builder(
itemBuilder: (context, index) {
return GestureDetector(
onTap: () async {
final _isRefresh = await showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: Text('${listItem[index]}'),
actions: <Widget>[
FlatButton(
child: Text('ok'),
onPressed: () => Navigator.of(context).pop(true),
),
FlatButton(
child: Text('キャンセル'),
onPressed: () => Navigator.of(context).pop(false),
),
],
);
}
);
if(_isRefresh) {
await _refresh();
Navigator.of(context).push(MaterialPageRoute(builder: (context) => MySecondScreen()));
}
},
child: Card(
child: Padding(
padding: const EdgeInsets.all(15.0),
child: Center(child: Text('INDEX = ${listItem[index]}')),
),
),
);
},
itemCount: listItem.length,
),
),
Visibility(
visible: isLoading,
child: DecoratedBox(
decoration: BoxDecoration(
color: Color(0x44000000),
),
child: Center(child: const CircularProgressIndicator()),
),
),
],
);
}
}
class MySecondScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Dialog'),),
body: Center(
child: Text('Second Screen')
),
);
}
}
解説
上から一つずつみていくと
Future<void> _refresh() async {
// ローディング
setState(() {
isLoading = true;
});
// 1秒待つ
await Future.delayed(Duration(seconds: 1));
// 新しいリストを代入
setState(() {
listItem = listItem.map((f) => f + 3).toList();
});
// 1秒待つ
await Future.delayed(Duration(seconds: 1));
// ロード終わり
setState(() {
isLoading = false;
});
}
これは1秒待った後にリストを更新して、さらに1秒待つという関数で、実際の通信とかを再現して作った関数。
ローディングの変数とリストの変数のsetState()
以外特に何もやってない。
次は、GestureDetector
内のonTap
の処理部分
final _isRefresh = await showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: Text('${listItem[index]}'),
actions: <Widget>[
FlatButton(
child: Text('ok'),
onPressed: () => Navigator.of(context).pop(true),
),
FlatButton(
child: Text('キャンセル'),
onPressed: () => Navigator.of(context).pop(false),
),
],
);
}
);
if(_isRefresh != null && _isRefresh) {
await _refresh();
Navigator.of(context).push(MaterialPageRoute(builder: (context) => MySecondScreen()));
}
まず最初にshowDialog
の結果を_isRefresh
という変数に代入しています。
showDialog
はNavigator.of(context).push(...)
と同じようにawait
で待つことで遷移先から戻ってきた時の結果などを返してくれる。
この場合だと
actions: <Widget>[
FlatButton(
child: Text('ok'),
onPressed: () => Navigator.of(context).pop(true),
),
FlatButton(
child: Text('キャンセル'),
onPressed: () => Navigator.of(context).pop(false),
),
],
Navigator.of(context).pop(true)
とNavigator.of(context).pop(false)
の部分でbool
型の値が返され、その結果がawait
して待っている_isRefresh
変数に代入されるという感じになっています。
※ ダイアログの背景を押して、閉じた場合には何も返さないので注意
こうすることでダイアログでOKが押されたのかキャンセルが押されたのかを判別できるようにして、_refresh
関数を走らせるかの判定をできるようにしています。
if(_isRefresh != null && _isRefresh) {
await _refresh();
Navigator.of(context).push(MaterialPageRoute(builder: (context) => MySecondScreen()));
}
ここは前のshowDialog
の部分でtrue
が返ってきた(OKが押された)時だけ_refresh
関数を走らせて、その後次の画面に遷移させるようにしています。
null
チェックをしているのは、ダイアログの背景を押されたら何も返ってこないため
最後に
これは直接は関係していないですが、ローディングする部分になっています
Visibility(
visible: isLoading,
child: DecoratedBox(
decoration: BoxDecoration(
color: Color(0x44000000),
),
child: Center(child: const CircularProgressIndicator()),
),
),
Visibility
のvisible
の部分で表示、非表示を切り替えています。
本来の通信ではローディングがいるだろうと思って追加しました。
終わりに
Flutterのダイアログの表示方法など最初は慣れるのに苦労したけど、今回の件を通してちょっと分かった気がする。
もっといい方法があるなどがあったらコメントで教えていただけると、すごく助かります