前書き
Flutterで初期データを非同期処理で取ってくる場合にどう書けばいいか悩んだのでまとめておきます。登場するのは主にinitState
とFutureBuilder
です。追加情報をいただければ随時書き足しますので、コメントよろしくお願いします。
サンプルコード
今回の検証コードのベースには、よくあるカウンターアプリの簡易版を使います。
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
void _incrementCounter() {
setState(() {
_counter++;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Text(
'$_counter',
style: Theme.of(context).textTheme.headline4,
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: const Icon(Icons.add),
),
);
}
}
実行画面はこんな感じ。「+ボタン」を押すと数字が一つずつ上がっていく。
以下では、このコードにデータ初期化処理を追加して、検証していきます。
検証: 非同期なデータ取得処理をinitStateに書いてみる
先にいいますがこれは失敗パターンです。データ取得はビルド前に済ませてしまいたいのでinitState()に非同期なデータ取得処理を書いたとしましょう。
コード
-
_MyHomePageState
にinitStateMethodと、その中で呼ぶための非同期処理を追加。
class _MyHomePageState extends State<MyHomePage> {
//略
@override
void initState() {
super.initState();
asyncSampleMethod();
}
// 非同期でデータを取ってくるサンプルメソッド
Future<int> asyncSampleMethod() async{
await Future.delayed(const Duration(seconds: 5), (){
_counter = 99;
});
return _counter;
}
//略
非同期処理は5秒待ったあと_counter
の値を99にするというものです。データベースとの通信に5秒かかったというのを想定しています。
結果
予想できる通りasyncSampleMethod
の完了を待たずにビルドされてしまうので実行結果は0から始まって5秒後に99が代入されるという形になる。
asyncSampleMethod
の完了を待たないので、_counter
は0のままビルドされ、5秒後asyncSampleMethod
が完了した際に99が代入されるという仕組みです。ちなみにinitStateにasyncを付けようとするとエラーが出ます。
_MyHomePageState.initState() returned a Future.
State.initState() must be a void method without an `async` keyword.
検証: FutureBuilderを使う
こちらも失敗パターンです。FutureBuilderはfuture引数に書いた非同期な処理の完了前と後に分けてウィジェットを書ける便利な関数です。データ取得処理にはFutureBuilderを使うといいよと書いてあったので、何も考えずとりあえず使ってみましょう。
コード
class _MyHomePageState extends State<MyHomePage> {
//略
// さっきのinitStateは一旦コメントアウト
// @override
// void initState() {
// super.initState();
// asyncSampleMethod();
// }
//略
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
// FutureBuilderを使って書き換える
child: FutureBuilder(
future: asyncSampleMethod(),
builder: (BuildContext context, AsyncSnapshot<int> snapshot) {
Widget childWidget;
if (snapshot.hasData) {
childWidget = Text(
'$_counter',
style: Theme.of(context).textTheme.headline4,
);
} else {
childWidget = const CircularProgressIndicator();
}
return childWidget;
}),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: const Icon(Icons.add),
),
);
}
}
今回はFutureBuilderのfuture
に渡したasyncSampleMethod()
が完了する前は CircularProgressIndicator()
(グルグル)を表示して、完了後はTextウィジェットで数字を表示するようにしています。
futureに渡した非同期処理が完了して99が表示され、「+ボタン」を押すと1ずつ上がっていくので一見大丈夫そうに見えます。しかし、setStateで状態が変わる(「+ボタン」を押す)度にFutureBuilderのfutureに渡した非同期な初期化処理が行われ、5秒後に値が99に戻ってしまいます。
setStateとFutureBuilderを両方使う
これなら正しく動作します。
コード
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int _counter;
Future<int> _future;
void _incrementCounter() {
setState(() {
_counter++;
});
}
@override
void initState() {
_future = asyncSampleMethod();
super.initState();
}
Future<int> asyncSampleMethod() async {
await Future.delayed(const Duration(seconds: 5), () {
_counter = 99;
});
return _counter;
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: FutureBuilder(
future: _future,
builder: (BuildContext context, AsyncSnapshot<int> snapshot) {
Widget childWidget;
if (snapshot.hasData) {
childWidget = Text(
'$_counter',
style: Theme.of(context).textTheme.headline4,
);
} else {
childWidget = const CircularProgressIndicator();
}
return childWidget;
}),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: const Icon(Icons.add),
),
);
}
}
ポイントは、initStateで一度だけ実行する非同期なデータ取得処理の返り値を格納するための変数(ここでは_future
)を用意して、FutureBuilderからはその変数を参照するようにします。こうすることでFutureBuilderがリビルドされても、データ取得処理が再度呼ばれることはなくなります。
実行結果
うまく動作しました!
まとめ
データ初期化処理について悩んだところをまとめました。少しでも誰かの参考になれば幸いです。今回は、FutureBuilderとsetStateを使いましたが他にも実装方法はあると思います。他の書き方があればコメントに書いていただけると助かります!