概要
今日は FutureBuilderについて解説したいと思います。
アプリを作ってると非同期処理が頻出するけど、どうやったら綺麗に書けるか分からない!
 そんなあなたに届けたい記事となっています。
先日、下記ブログを書きました。


こちらも合わせて読んでいただく事で、だいぶ非同期処理周りがすっきりするのではないかと思います。
取得した本の内容を描画する処理を書いてみよう
本一覧の取得処理
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart';
class Book {
  const Book({
    required this.id,
    required this.title,
    required 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>;
    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() {
    super.initState();
    Future<void>.delayed(Duration.zero).then((_) {
      Provider.of<Books>(context, listen: false).fetchBooks();
    });
  }
  @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: (context, 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() {
    super.initState();
    Future<void>.delayed(Duration.zero).then((_) async {
      setState(() {
        _isLoading = true;
      });
      await Provider.of<Books>(context, listen: false).fetchBooks();
      setState(() {
        _isLoading = false;
      });
    });
  }
  @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: (context, index) => BookItem( // あるものとして
          books.items[index],
        ),
      ),
    );
  }
}
解説
さて、これで通信が返ってくるまではローディングを中心に表示する事ができました。
 が、状態変化で再ビルドしたいためStatefulWidgetに変更しなければいけなくなりました。
(async/awaitが分からない人は前回の記事を見てから戻ってきていただけると嬉しいです。)
これを少し改善したい。
そもそも何をしたいか?
 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: (context, snapshot) {
          if (snapshot.connectionState == ConnectionState.waiting) {
            // 非同期処理未完了 = 通信中
            return Center(
              child: CircularProgressIndicator.adaptive(),
            );
          }
          if (snapshot.error != null) {
            // エラー
            return Center(
              child: Text('エラーがおきました');
            };
          }
          // 成功処理
          return Consumer<Books>(
            builder: (context, books, child) => ListView.builder(
              itemCount: books.items.length,
              itemBuilder: (context, 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 (snapshot.connectionState == ConnectionState.waiting) {
  // 非同期処理未完了 = 通信中
  return Center(
    child: CircularProgressIndicator(),
  );
}
まず、ローディング中かどうか(非同期処理が終わったかどうか)はAsyncSnapshotが持つconnectionStateで判定します。
そして、ローディング中に表示したいWidgetをreturnします。
エラーか否か
if (snapshot.error != null) {
  // エラー
  return Center(
    child: Text('エラーがおきました');
  };
}
エラーは、AsyncSnapshotが持つerrorプロパティで確認できます。
 最初の例ではエラー時の表示はなかったですが、中央にエラー文を表示しています。
通信成功時の処理
// 成功処理
return Consumer<Books>(
  builder: (context, books, child) => ListView.builder(
    itemCount: books.items.length,
    itemBuilder: (context, 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
また、記事がとても役に立ったと思う人はコーヒーを奢っていただけると非常に嬉しいです!



コメント