【Flutter】それ、FutureBuilderだったら綺麗に書けるよ?

Dart

概要

今日は 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で判定します。

ConnectionState enum

そして、ローディング中に表示したい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フォローお願いします

毎日技術ブログを更新しています。

「次回以降も記事を読んでみたい!」
「この辺分からなかったから質問したい!」

そんな時は、是非@daiki1003のフォローお願いします♪

コメント

タイトルとURLをコピーしました