Flutter FutureBuilderでリロードしたい
無性にリロードがしたい。人間にはそういう時期だってある。
FutureBuilderは完了するまで別のウィジェットを表示できる
FutureBuilderは、HTTPリクエストの結果を表示する時に、リクエストが完了するまでインジケーターを表示したいケースに使える。 基本的な使い方は以下の通り。
FutureBuilder<String>( future: _futureObj, builder: (BuildContext context, AsyncSnapshot<String> snapshot) { if(snapshot.hasData) { return Text(snapshot.data); } else if (snapshot.hasError) { return Text("Something wrong"); } else { return CircularProgressIndicator(); } }, );
future
プロパティにFutureオブジェクトを配置し、builder
プロパティにはFutureオブジェクトがSuccess/Errorを返すまでどんなウィジェットを使うべきかの処理を記述する。
Futureオブジェクトは非同期処理をするための機能を提供してくれる。
Future<int> future = getFuture(); future.then((value) => handleValue(value)) .catchError((error) => handleError(error));
時間のかかる処理をFutureオブジェクトでラップして、処理が完了したタイミングで何かしたい時に使う。 JavaScriptのPromiseみたいなもの。
一回きりなんて、そんな冷たいこと言わないで
前述の通り、FutureBuilderは何らかの処理が完了するまでインジケーターを表示して、処理が完了したら別のウィジェットを表示する時に使える。 しかし、このウィジェットをRefreshIndicatorと組み合わせて使いたくなった時に問題が発生する。
RefreshIndicatorはいわゆる引っ張って更新する機能を提供してくれるウィジェットなんだけど、単純にFutureBuilderを小要素として配置しただけではうまくリロードができない。
今回は、RefreshIndicatorとFutureBuilderを組み合わせて、リロードするたびにインジケーターが表示→ロードが完了したらウィジェットを表示させるコードを書いてみる。
precondition
StatefulWidgetでfutureを更新する
結論から書くと、FutureBuilderを小要素として持つWidgetをStatefulWidgetで定義して、Futureオブジェクトを更新できるようにしてやれば良い。
import 'package:flutter/material.dart'; void main() => runApp(MyApp()); class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( title: 'Flutter Demo', theme: ThemeData( primarySwatch: Colors.blue, ), home: MyHomePage(title: 'Flutter Demo Home Page'), ); } } // FutureBuilderを小要素として持つStatefulWidgetを定義する class MyHomePage extends StatefulWidget { MyHomePage({Key key, this.title}) : super(key: key); final String title; @override _MyHomePageState createState() => _MyHomePageState(); } class _MyHomePageState extends State<MyHomePage> { // FutureBuilderに渡すFutureオブジェクトを更新可能なプロパティとして持つ Future<String> _imageUrl; // 初期化時にFutureオブジェクトも初期化しておく @override void initState() { super.initState(); _imageUrl = _fetchImageUrl(); } // asyncをつけていると、返り値をFutureオブジェクトでラップしてくれる Future<String> _fetchImageUrl() async { var result = "https://via.placeholder.com/150"; await Future.delayed(Duration(seconds: 3)); return result; } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(widget.title), ), body: Container( child: RefreshIndicator( // onRefreshは Future<void>を返す関数を引数に取る onRefresh: () async { _imageUrl= _fetchImageUrl(); setState(() { }); }, child: GridView.builder( gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 2, crossAxisSpacing: 8, mainAxisSpacing: 8, childAspectRatio: 1.0, ), padding: EdgeInsets.all(8.0), itemCount: 2, itemBuilder: (context, index) { return FutureBuilder( future: _imageUrl, // FutureがSuccessを返すまで、builderに渡した無名関数が呼ばれる builder: (BuildContext context, AsyncSnapshot<String> snapshot) { switch(snapshot.connectionState) { case ConnectionState.waiting: return CircularProgressIndicator(); break; default: break; } if (snapshot.hasError) { print(snapshot.error); } return snapshot.hasData ? Image.network(snapshot.data) : CircularProgressIndicator(); }, ); }, ), ), ), ); } }
FutureBuilderの部分を少し詳しく解説する。
return FutureBuilder( future: _imageUrl, builder: (BuildContext context, AsyncSnapshot<String> snapshot) { switch(snapshot.connectionState) { case ConnectionState.waiting: return CircularProgressIndicator(); break; default: break; } if (snapshot.hasError) { return Text(snapshot.error); } return snapshot.hasData ? Image.network(snapshot.data) : CircularProgressIndicator(); }, );
画面を引っ張って更新するとRefreshIndicatorのonRefresh
が走る。このとき、_imageUrl
が更新され、setState()
が呼ばれる。
このsetState()
が呼ばれると、このメソッドを持つウィジェット自身が再描画され、小要素であるFutureBuilderも再描画される。
_imageUrl
が新しいFutureオブジェクトに更新されているので、Successが発生するまで再びCircularProgressIndicatorが表示される仕組みになっている。
connectionState
はAsyncSnapshot_imageUrl
がSuccessを返すまでConnectionState.waiting
が格納されている。
これを判定することで、処理が完了したかが分かる。
できあがったものはコチラ。引っ張って更新するたびにFutureBuilderが動いていることがわかる。
FlutterのFutureBuilderでリロードっぽいやつできた! pic.twitter.com/MJoruoGLP6
— osamtimizer (@osamtimizer) 2020年2月25日
今回使用したサンプルコードはこちらに公開してある。
ちなみに
身も蓋もない話だけど、画像のロードをいい感じに見せるだけだったらFadeInImage.memoryNetwork
を使うのが一番楽。
おわりに
こういうクロスプラットフォームのフレームワークってちょっと凝ったことしようとすると派手に自作しないといけなかったりするんだけど、 Flutterの場合はMaterial Designっぽいウィジェットが一通り揃っているので、その文脈の中であればそこそこ複雑なことも実現しやすそうだった。*1
*1:でもインデントがすごい深くなるのはちょっと微妙