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に分けるだけでかなり扱いやすくなります。個人開発でも、最初から小さく分けておくと、後から機能追加するときに迷いにくくなります。