FutureBuilder & StreamBuilder:Flutter と Dart におけるベストプラクティス

もしFlutterアプリを開発しているなら、きっとお馴染みの FutureBuilder と StreamBuilder に出会っているはずです。これらは非同期データを手軽に表示するのにとても便利ですが、使い方を誤るとすぐにフラストレーションの原因になります。
この記事では、クラッシュや奇妙なバグを避けるためのベストプラクティスを一通り見ていきます。ネタバレ:Dart 3 で導入されたレコードを使うと、コードがさらに簡潔になります!
FutureBuilder と StreamBuilder とは?
FutureBuilder
FutureBuilder は Future を通じて非同期処理を実行し、結果が利用可能になったときに UI を自動的に再構築するためのものです。例えば、API からユーザー名を取得する場合:
FutureBuilder<String>(
future: getUserName(), // getUserName() -> Future<String>
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return CircularProgressIndicator();
}
if (snapshot.hasError) {
return Text('Erreur : ${snapshot.error}');
}
return Text('Nom : ${snapshot.data}');
},
)
原理は簡単です:future を渡すと、レスポンスを受け取ったときにウィジェットが更新されます。
StreamBuilder
継続的に変化するデータには、StreamBuilder が頼りになります。例えば、1 秒ごとに増えるカウンターや、順次届くメッセージの系列を想像してください。
StreamBuilder<int>(
stream: counterStream(), // counterStream() -> Stream<int>
builder: (context, snapshot) {
// L'état active signifie qu'on a commencé à recevoir des données du Stream.
if (snapshot.connectionState == ConnectionState.active) {
return Text('Compteur : ${snapshot.data}');
} else if (snapshot.connectionState == ConnectionState.waiting) {
// waiting indique qu'on attend encore les premiers events
return CircularProgressIndicator();
} else if (snapshot.hasError) {
return Text('Erreur : ${snapshot.error}');
} else if (snapshot.connectionState == ConnectionState.done) {
// quand le flux est terminé
return Text('Le stream est terminé. Dernière valeur : ${snapshot.data}');
} else {
return SizedBox.shrink();
}
},
)
Stream の新しいイベントごとにウィジェットが再ビルドされます。魔法みたい。ここではいくつかの状態を区別しています:
waiting: ストリームは作成されたがまだデータを発行していない,active: 実際にデータを受信している,done: フローが閉じられている.
Record を使った型推論
Dart 3 には小さな革命があります:レコードです!レコードは複数の異なる型の値をまとめるミニオブジェクトで、他の言語の Tuple のようなものです。これにより、無闇に List<dynamic> を扱う必要がなくなり、可読性が向上します。
なぜ有用なのか?
具体例を挙げましょう。1 回のリクエストでユーザーのニックネームと年齢を取得したいとします。リストを返す代わりに(["Martin", 42] のように順序が曖昧になり得ます)、Dart 3 では直接 レコード を返すことができます。例:(String, int)。
コード例
以前(List<dynamic> を使った場合)
// Simule une fonction qui renvoie deux infos : username et age
Future<List<dynamic>> fetchData() async {
await Future.delayed(Duration(seconds: 1));
return ["Martin", 42];
}
FutureBuilder<List<dynamic>>(
future: fetchData(),
builder: (context, snapshot) {
if (snapshot.hasData) {
final data = snapshot.data!;
final username = data[0] as String;
final age = data[1] as int;
return Text('$username, $age');
}
return CircularProgressIndicator();
},
)
- 問題点:キャスト(
as String,as int)が必要で、List<dynamic>を扱うことになります…要するに、あまり良くないです。
後(Record を使った場合)
// Même fonction, mais on renvoie un record plus explicite
Future<(String, int)> fetchData() async {
await Future.delayed(Duration(seconds: 1));
return ("Martin", 42);
}
FutureBuilder<(String, int)>(
future: fetchData(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return CircularProgressIndicator();
}
if (snapshot.hasError) {
return Text('Erreur : ${snapshot.error}');
}
if (snapshot.hasData) {
final (username, age) = snapshot.data!;
return Text('$username, $age');
}
return SizedBox.shrink(); // cas par défaut
},
)
- ここではキャストは不要です:Dart は
(String, int)であることを知っています。 - コードはより読みやすくなり、エラーのリスクを大幅に減らせます。
注意: ループして再実行したくない場合は、
FutureBuilderの中で直接fetchData()を呼び出すのは避けたほうがよいです。後ほど少し触れます。
避けるべきよくあるエラー
FutureBuilder と StreamBuilder の乱用
よくある大きなミスは、build() メソッド内で future: fetchData() や stream: getStream() を実行してしまうことです。結果は?再ビルドごとにリクエストが再実行され(そして泣くことになります)。解決策は?Future や Stream を一度だけインスタンス化することです。
❌ Mauvais usage
FutureBuilder(
future: fetchData(),
builder: (context, snapshot) {
// ...
},
)
✅ Meilleure approche
late final Future<(String, int)> future;
@override
void initState() {
super.initState();
future = fetchData(); // un seul appel
}
@override
Widget build(BuildContext context) {
return FutureBuilder<(String, int)>(
future: future,
builder: (context, snapshot) {
// ...
},
);
}
これにより予期せぬ動作や不要な呼び出しを避けられます。
パフォーマンスのヒント: 結果が頻繁に変わらない場合は、キャッシュを導入するか、再リクエストの前にデータが既にロードされているか確認することを検討してください。ネットワークの過負荷や過度な再レンダリングを避けられます。
エラーの処理
エラーが発生した場合(snapshot.hasError)、専用のメッセージを表示したり、「再試行」ボタンを提供してリクエストを再実行できるようにしたりできます。簡略化した例:
if (snapshot.hasError) {
return Column(
children: [
Text('Oups ! Erreur : \\${snapshot.error}'),
ElevatedButton(
onPressed: () {
setState(() {
// On relance le Future ou on appelle un autre mécanisme
// si on l'a stocké dans future = fetchData() etc.
future = fetchData();
});
},
child: Text('Réessayer'),
),
],
);
}
こうすることで、ユーザーはブロックされず、アクションを再試行できます。
Stream の具体的な使用例
さらに進めて、インスタントメッセージのシナリオを考えてみましょう。順次新しいメッセージを発行する Stream<String> を想像できます:
StreamBuilder<String>(
stream: chatStream(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return Text('Chargement du chat...');
} else if (snapshot.hasError) {
return Text('Erreur : ${snapshot.error}');
} else if (snapshot.hasData) {
final newMessage = snapshot.data!;
return Text('Nouveau message : $newMessage');
} else {
return Text('Aucun message');
}
},
)
ここでは、chatStream() にプッシュされる新しいメッセージごとに再ビルドが発生します。リアルタイムチャットや連続的なイベントフローに最適です。
結論
というわけで、FutureBuilder と StreamBuilder に関するベストプラクティスの概観を示しました。Dart 3 によって、レコードはコードをよりクリーンに、堅牢に、保守しやすくします。覚えておくべき点は:
build()内で futures/streams を起動しないこと。- すべての状態(loading、error、done など)を適切に処理すること。
List<dynamic>の使用を減らすためにレコードを検討すること。- パフォーマンスを考慮すること(不要な再実行を避け、キャッシュを検討する)。
- エラー発生時に処理・再実行する手段を提供すること。
正直なところ、これによりコードはより扱いやすくなり、クラッシュが起きにくくなります。さらに学びたい場合は、Flutter の公式ドキュメント と Dart(特に レコード)をチェックしてください。そこには他の例やユースケースも掲載されています。
あなたは非同期呼び出しをどのように管理していますか?コメントでフィードバックやコツを共有してください、読むのが楽しみです!
コメント
読み込み中...