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

Dart

概要

今日は FutureBuilderについて解説したいと思います。

アプリを作ってると非同期処理が頻出するけど、どうやったら綺麗に書けるか分からない!
そんなあなたに届けたい記事となっています。

先日、下記ブログを書きました。

【Flutter】httpなどの非同期処理をasync/awaitで見やすくしてみる
🌈 概要 httpを始めとする処理を行う場合、多くは非同期処理になります。 通信を行う処理と通信が成功/失敗した場合の処理をそれぞれ記述します。 割と多く出て来る記述だと思うのでなるべく分かりやすく綺麗に書きたいですよね。 ...

こちらも合わせて読んでいただく事で、だいぶ非同期処理周りがすっきりするのではないかと思います。

取得した本の内容を描画する処理を書いてみよう

本一覧の取得処理

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を使う

FutureBuilderWidgetなので、bodyに直接渡す事ができます。

さて、FutureBuilderをインスタンス化するために、まずfutureと言うパラメタにFuture<void>を返す処理を渡します。

  return FutureBuilder(
    future: Provider.of<Books>(context, listen: false).fetchBooks(),
    builder: ...
  );

この処理結果を見て、FutureBuilderbuilder内の処理を呼び出してくれます。

builderWidget Function(BuildContext, AsyncSnapshot<void>)型です。

ローディング処理
if (snapshot.connectionState == ConnectionState.waiting) {
  // 非同期処理未完了 = 通信中
  return Center(
    child: CircularProgressIndicator(),
  );
}

まず、ローディング中かどうか(非同期処理が終わったかどうか)はAsyncSnapshotが持つconnectionStateで判定します。

ConnectionState enum - widgets library - Dart API
API docs for the ConnectionState enum from the widgets library, for the Dart programming language.

そして、ローディング中に表示したいWidgetreturnします。

エラーか否か
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へ

さて、お気づきの方もいらっしゃると思いますが、StatefulWidgetStatelessWidgetになっています。
FutureBuilderが状況に応じて再ビルドをしてくれるのでScaffoldAppBarを再ビルドする必要がなくなりました。

昨日の記事と合わせると非同期処理が非常に綺麗に書ける様になっているのではないでしょうか?

誰かのお役に立てば。

Twitterフォローお願いします

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

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

Twitterコミュニティ参加お願いします

Twitterコミュニティ「Flutter lovers」を開設しました!
参加お待ちしております😁

☕️ Buy me a coffee

また、記事がとても役に立ったと思う人は
コーヒーを奢っていただけると非常に嬉しいです!

コメント

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