【Flutter】FutureBuilderの正しい使い方
背景
Flutter公式のウィジェット紹介である、Widget of the WeekにFutureBuilder(Take 2)が投稿された。(2022/11/18)
こちらの動画では、過去に投稿されたFutureBuilderに関する実装方法が訂正されている。
具体的には、FutureBuilder内でFutureを生成する実装方法に関して、FutureBuilderの外に出すように訂正されている。
本記事では、この実装方法の違いが処理にどう影響するのかを簡単なサンプルアプリを作成し確認する。
間違った実装方法
FutureBuilder内でFutureを生成する。
FutureBuilder(
future : http.get('http://awesome.data'),
・・・
)
問題点
FutureBuilder内でFutureを生成すると、親Widgetがリビルドされるたびに非同期処理が走ってしまう。
正しい実装方法
initStateやライフサイクルメソッド内などFutureBuilderの外でFutureをあらかじめ保持しておき、それをFutureBuilderに渡すように実装する。
Future<MyData> _data;
initState(){
_data = http.get('http://awesome.data');
}
FutureBuilder(
future : _data,
・・・
)
動作確認
サンプルアプリを作成し、実際にそれぞれの動作の違いを確認する。
サンプルアプリの概要
Flutterのデモアプリ(カウンターアプリ)に非同期処理を追加したサンプルアプリを作成。(コード全文はこちら)
非同期処理では、1秒後に文字列を取得する。
Future.delayed(const Duration(seconds: 1), () => '完了')
FutureBuilderを利用して非同期で取得した文字列を画面に表示する。
非同期処理の間はインジケータを表示する。
間違った実装方法の場合
重要なコード以外は省略。
body: FutureBuilder(
// ①FutureBuilder内でFutureを生成
future: Future.delayed(const Duration(seconds: 1), () => '完了'),
builder: ---省略---
実行
カウントアップし、リビルドが走るたびに非同期処理が実行されている。
正しい実装方法の場合
重要なコード以外は省略。
class _MyHomePageState extends State<MyHomePage> {
// ①Futureを定義
Future<dynamic>? _data;
initState() {
super.initState();
// ②Futureをあらかじめ_dataに保持しておく
_data = Future.delayed(const Duration(seconds: 1), () => '完了');
}
Widget build(BuildContext context) {
return Scaffold(
appBar : ---省略---
body: FutureBuilder(
// ③FutureBuilderに_dataを渡す
future: _data,
builder: ---省略---
}
}
実行
初回ビルド時のみ非同期処理が実行される。カウントアップし、リビルドされても非同期処理は実行されない。
実装コード
今回実装したサンプルサプリのコード全文
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
final String title;
const MyHomePage({
Key? key,
required this.title,
}) : super(key: key);
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
// ①Futureを定義
Future<dynamic>? _data;
int _counter = 0;
void _incrementCounter() {
setState(() {
_counter++;
});
}
initState() {
super.initState();
// ②Futureをあらかじめ_dataに保持しておく
_data = Future.delayed(const Duration(seconds: 1), () => '完了');
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: FutureBuilder(
// ③FutureBuilderに_dataを渡す
future: _data,
builder: (BuildContext context, AsyncSnapshot<dynamic> snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
if (snapshot.hasData) {
String data = snapshot.data;
return contents(data);
} else {
String data = 'エラー';
return contents(data);
}
} else if (snapshot.connectionState == ConnectionState.waiting) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const CircularProgressIndicator(),
Text('$_counter', style: const TextStyle(fontSize: 24))
],
),
);
} else {
String data = 'エラー';
return contents(data);
}
},
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: const Icon(Icons.add),
),
);
}
Center contents(String data) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(data, style: const TextStyle(fontSize: 24)),
Text('$_counter', style: const TextStyle(fontSize: 24))
],
),
);
}
}
まとめ
FutureBuilderの正しい実装方法に関してサンプルアプリを用いて確認した。実装の違いにより、意図しないタイミングで非同期処理が走る可能性があることを確認できた。
参考
- FutureBuilder(Widget of the Week)
https://www.youtube.com/watch?v=zEdw_1B7JHY&list=PLjxrf2q8roU23XGwz3Km7sQZFTdB996iG&index=1
Discussion