元ブログ - 技術は熱いうちに打て! | 【Flutter】それ、FutureBuilderだったら綺麗に書けるよ?
概要
今日は FutureBuilderについて解説したいと思います。
アプリを作ってると非同期処理が頻出するけど、どうやったら綺麗に書けるか分からない!
そんなあなたに届けたい記事となっています。
先日、非同期処理をasync/awaitを用いて綺麗に書くと言う趣旨のブログを書きました。
こちらも合わせて読んでいただく事で、だいぶ非同期処理周りがすっきりするのではないかと思います。
取得した本の内容を描画する処理を書いてみよう
本一覧の取得処理
import 'dart:convert';
import 'packages:flutter/material.dart';
import 'packages:http/http.dart';
class Book {
final String id;
final String title;
final double price;
const Book(this.id, this.title, this.price);
}
class Books with ChangeNotifier {
List<Book> _books = [];
List<Book> get books {
return [..._books];
}
Future<void> fetchBooks() async {
const url = '...';
final response = await http.get(url);
final decodedData = json.decode(response.body) as Map<String, dynamic>;
if (decodedData == null) {
return;
}
List<Book> responseBooks = [];
// パース
_books = responseBooks;
notifyListeners();
}
}
本一覧の描画処理
import 'packages:flutter/material.dart';
import 'packages:provider/provider.dart';
import '../providers/books.dart';
class BookListScreen extends StatefulWidget {
@Override
_BookListScreen createState() => _BookListScreenState();
}
class _BookListScreenState extends State<BookListScreen> {
@override
void initState() {
Future.delayed(Duration.zero).then((_) {
Provider.of<Books>(context, listen: false).fetchBooks();
});
super.initState();
}
@override
Widget build(BuildContext context) {
final books = Provider.of<Books>(context);
return Scaffold(
appBar: AppBar(
title: Text('Books'),
),
body: ListView.builder(
itemCount: books.items.length,
itemBuilder: (ctx, index) => BookItem( // あるものとして
books.items[index],
),
),
);
}
}
解説
こんな感じでしょうか。
(直書きしてるのでコンパイル通らなかったらすみません。)
これだと、確かに取得し終わったら描画はされると思いますが、取得してる最中は
真っ白い画面を見せられることになると思います。
UX的に良くないので、ローディングを表示しましょう。
ローディングの表示
import 'packages:flutter/material.dart';
import 'packages:provider/provider.dart';
class BookListScreen extends StatefulWidget {
@Override
_BookListScreen createState() => _BookListScreenState();
}
class _BookListScreenState extends State<BookListScreen> {
bool _isLoading = false;
@override
void initState() {
Future.delayed(Duration.zero).then((_) async {
setState(() {
_isLoading = true;
});
await Provider.of<Books>(context, listen: false).fetchBooks();
setState(() {
_isLoading = false;
});
});
super.initState();
}
@override
Widget build(BuildContext context) {
final books = Provider.of<Books>(context);
return Scaffold(
appBar: AppBar(
title: Text('Books'),
),
body: _isLoading
? Center(
child: CircularProgressIndicator(),
)
: ListView.builder(
itemCount: books.items.length,
itemBuilder: (ctx, index) => BookItem( // あるものとして
books.items[index],
),
),
);
}
}
解説
さて、これで通信が返ってくるまではローディングを中心に表示する事ができました。
が、状態変化で再ビルドしたいためStatefulWidgetに変更しなければいけなくなりました。
async/awaitが分からない人は前回の記事を
見てから戻ってきていただけると嬉しいです。
これを少し改善したい。
そもそも何をしたいか?
このWidgetがbuildされたら、
1. 通信が終わるまではローディングを表示したい
2. 正常に終わったらListViewを表示したい
この2つが主ですね。
これを踏まえた上で、 今回の主役FutureBuilder
を紹介します。
FutureBuilderを使って書き換える
import 'packages:flutter/material.dart';
import 'packages:provider/provider.dart';
import '../providers/books.dart';
class BookListScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Books'),
),
body: FutureBuilder(
future: Provider.of<Books>(context, listen: false).fetchBooks(),
builder: (ctx, dataSnapshot) {
if (dataSnapshot.connectionState == ConnectionState.waiting) {
// 非同期処理未完了 = 通信中
return Center(
child: CircularProgressIndicator(),
);
}
if (dataSnapshot.error != null) {
// エラー
return Center(
child: Text('エラーがおきました');
};
}
// 成功処理
return Consumer<Books>(
builder: (cctx, books, child) => ListView.builder(
itemCount: books.items.length,
itemBuilder: (lctx, index) => BookItem( // あるものとして
books.items[index],
),
),
);
},
),
);
}
}
解説
何が起こったかわかりませんね笑
一つずつ解説していきます。
FutureBuilderを使う
FutureBuilder
はWidgetの小クラスです。
なので、bodyに直接渡す事ができます。
さて、FutureBuilder
をインスタンス化するために、まずfuture
と言うパラメタにFuture<void>
を返す処理を渡します。
return FutureBuilder(
future: Provider.of<Books>(context, listen: false).fetchBooks(),
builder: ...
);
この処理結果を見て、FutureBuilderはbuilder内の処理を呼び出してくれます。
builderは Widget Function(BuildContext, AsyncSnapshot<void>)
型です。
ローディング処理
if (dataSnapshot.connectionState == ConnectionState.waiting) {
// 非同期処理未完了 = 通信中
return Center(
child: CircularProgressIndicator(),
);
}
まず、ローディング中かどうか(非同期処理が終わったかどうか)はAsyncSnapshot
が持つconnectionStateで判定します。
そして、ローディング中に表示したいWidgetをreturnします。
エラーか否か
if (dataSnapshot.error != null) {
// エラー
return Center(
child: Text('エラーがおきました');
};
}
エラーは、AsyncSnapshot
が持つerror
プロパティで確認できます。
最初の例ではエラー時の表示はなかったですが、中央にエラー文を表示するWidgetをreturnをしました。
通信成功時の処理
// 成功処理
return Consumer<Books>(
builder: (cctx, books, child) => ListView.builder(
itemCount: books.items.length,
itemBuilder: (lctx, index) => BookItem( // あるものとして
books.items[index],
),
),
);
通信成功時は、Consumerを使って無駄な再ビルドを防ぎつつリスト表示部分だけ描画します。
StatefulWidgetはStatelessWidgetへ
さて、お気づきの方もいらっしゃると思いますがなんとStatefulWidgetだったのがStatelessWidgetになっています。
FutureBuilder
が状況に応じて再ビルドをしてくれるのでScaffoldやAppBarを再ビルドする必要がなくなりました。
昨日の記事と合わせると非同期処理が非常に綺麗に書ける様になっているのではないでしょうか?
誰かのお役に立てば。