HOME / BLOG
BLOG

FlutterでAPI通信をRepositoryに分ける方法【http・FutureBuilder】

デフォルト画像

FlutterでAPI通信を書くとき、画面のbuild()メソッドに直接http.get()を書くと、再描画のたびに通信が走る原因になります。最初は動いても、アプリが育つほど不具合を追いにくくなります。

この記事では、API通信をRepositoryに分け、FutureBuilderで画面に表示する基本構成をまとめます。ArcNextのような個人開発アプリでも、そのまま使いやすい形です。

基本方針

  • JSONの形はModelで受ける
  • API通信はRepositoryに閉じ込める
  • 画面は読み込み中、成功、失敗の表示に集中する
  • 通信はinitState()で開始し、build()で毎回実行しない
  • タイムアウトとHTTPステータスのチェックを入れる

Modelを作る

class Article {
  const Article({
    required this.id,
    required this.title,
    required this.updatedAt,
  });

  final int id;
  final String title;
  final DateTime updatedAt;

  factory Article.fromJson(Map<String, dynamic> json) {
    return Article(
      id: json['id'] as int,
      title: json['title'] as String,
      updatedAt: DateTime.parse(json['updated_at'] as String),
    );
  }
}

RepositoryでAPI通信を分ける

import 'dart:convert';

import 'package:http/http.dart' as http;

class ArticleRepository {
  ArticleRepository({required this.client, required this.baseUrl});

  final http.Client client;
  final Uri baseUrl;

  Future<List<Article>> fetchArticles() async {
    final uri = baseUrl.replace(path: '/api/articles');
    final response = await client.get(uri).timeout(const Duration(seconds: 10));

    if (response.statusCode != 200) {
      throw Exception('Failed to load articles: ${response.statusCode}');
    }

    final decoded = jsonDecode(response.body) as List<dynamic>;
    return decoded
        .map((item) => Article.fromJson(item as Map<String, dynamic>))
        .toList();
  }
}

FutureBuilderで表示する

class ArticleListPage extends StatefulWidget {
  const ArticleListPage({super.key, required this.repository});

  final ArticleRepository repository;

  @override
  State<ArticleListPage> createState() => _ArticleListPageState();
}

class _ArticleListPageState extends State<ArticleListPage> {
  late Future<List<Article>> futureArticles;

  @override
  void initState() {
    super.initState();
    futureArticles = widget.repository.fetchArticles();
  }

  @override
  Widget build(BuildContext context) {
    return FutureBuilder<List<Article>>(
      future: futureArticles,
      builder: (context, snapshot) {
        if (snapshot.hasError) {
          return const Center(child: Text('読み込みに失敗しました'));
        }

        if (!snapshot.hasData) {
          return const Center(child: CircularProgressIndicator());
        }

        final articles = snapshot.data!;
        return ListView.builder(
          itemCount: articles.length,
          itemBuilder: (context, index) {
            final article = articles[index];
            return ListTile(
              title: Text(article.title),
              subtitle: Text(article.updatedAt.toLocal().toString()),
            );
          },
        );
      },
    );
  }
}

よくある失敗

  • build()内で毎回APIを呼んでしまう
  • HTTPステータスが200以外でも成功扱いにしてしまう
  • JSONの型変換を画面側に散らしてしまう
  • エラー時の表示を用意していない
  • タイムアウトを設定せず、通信が詰まったときの挙動が不明になる

ArcNextで考えるなら

シュート記録、練習メニュー、ローカル保存データを扱うアプリでは、画面とデータ取得を分けておく価値が高いです。最初から完璧なアーキテクチャにする必要はありませんが、Repositoryを1枚挟むだけで、あとからAPI連携やローカルDB保存へ拡張しやすくなります。

あわせて読みたい

参考資料

まとめ

FlutterのAPI通信は、画面に直接書かず、Model、Repository、Widgetに分けるだけでかなり扱いやすくなります。個人開発でも、最初から小さく分けておくと、後から機能追加するときに迷いにくくなります。