概要
今日は FutureBuilderについて解説したいと思います。
アプリを作ってると非同期処理が頻出するけど、どうやったら綺麗に書けるか分からない!
そんなあなたに届けたい記事となっています。
先日、非同期処理をasync/awaitを用いて綺麗に書くと言う趣旨のブログを書きました。
こちらも合わせて読んでいただく事で、だいぶ非同期処理周りがすっきりするのではないかと思います。
取得した本の内容を描画する処理を書いてみよう
本一覧の取得処理
import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:http/http.dart'; class Book { const Book( this.id, this.title, this.price, ); final String id; final String title; final double 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) { return; } List<Book> responseBooks = []; // パース _books = responseBooks; notifyListeners(); } }
本一覧の描画処理
import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../providers/books.dart'; class BookListScreen extends StatefulWidget { @Override _BookListScreen createState() => _BookListScreenState(); } class _BookListScreenState extends State<BookListScreen> { @override void initState() { Future<void>.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: const AppBar( title: Text('Books'), ), body: ListView.builder( itemCount: books.items.length, itemBuilder: (ctx, index) => BookItem( // あるものとして books.items[index], ), ), ); } }
解説
こんな感じでしょうか。
(直書きしてるのでコンパイル通らなかったらすみません。)
これだと、確かに取得し終わったら描画はされると思いますが、取得してる最中は
真っ白い画面を見せられることになると思います。
UX的に良くないので、ローディングを表示しましょう。
ローディングの表示
import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; class BookListScreen extends StatefulWidget { @Override _BookListScreen createState() => _BookListScreenState(); } class _BookListScreenState extends State<BookListScreen> { bool _isLoading = false; @override void initState() { Future<void>.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: const AppBar( title: Text('Books'), ), body: _isLoading ? Center( child: CircularProgressIndicator.adaptive(), ) : ListView.builder( itemCount: books.items.length, itemBuilder: (ctx, index) => BookItem( // あるものとして books.items[index], ), ), ); } }
解説
さて、これで通信が返ってくるまではローディングを中心に表示する事ができました。
が、状態変化で再ビルドしたいためStatefulWidget
に変更しなければいけなくなりました。
async/await
が分からない人は前回の記事を
見てから戻ってきていただけると嬉しいです。
これを少し改善したい。
そもそも何をしたいか?
このWidget
がビルドされたら、
1. 通信が終わるまではローディングを表示したい
2. 正常に終わったらListView
を表示したい
この2つが主ですね。
これを踏まえた上で、 今回の主役FutureBuilder
を紹介します。
FutureBuilderを使って書き換える
import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../providers/books.dart'; class BookListScreen extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar: const 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.adaptive(), ); } 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
を再ビルドする必要がなくなりました。
昨日の記事と合わせると非同期処理が非常に綺麗に書ける様になっているのではないでしょうか?誰かのお役に立てば。
Twitterフォローお願いします
「次回以降も記事を読んでみたい!」「この辺分からなかったから質問したい!」
そんな時は、是非Twitter (@daiki1003)やInstagram (@ashdik_flutter)のフォローお願いします♪
Twitterコミュニティ参加お願いします
Twitterコミュニティ「Flutter lovers」を開設しました!参加お待ちしております😁
☕️ Buy me a coffee
また、記事がとても役に立ったと思う人はコーヒーを奢っていただけると非常に嬉しいです!
コメント