Что такое FutureBuilder и StreamBuilder и когда использовать каждый?
FutureBuilder подписывается на Future (разовый результат) и перестраивает UI по трём состояниям: ожидание, данные, ошибка. StreamBuilder работает с непрерывным Stream, обновляясь при каждом событии. Главная ошибка: создавать Future прямо в build() — это запускает новый запрос при каждом rebuild.
FutureBuilder — разовый асинхронный результат
FutureBuilder подписывается на Future и перестраивает дерево виджетов при изменении состояния: ожидание, данные, ошибка. Используется для однократных операций — загрузки данных при открытии экрана, отправки формы, чтения из кэша.
import 'package:flutter/material.dart';
Future<String> fetchUserName(String userId) async {
// Имитируем сетевой запрос
await Future.delayed(const Duration(seconds: 1));
if (userId.isEmpty) throw Exception('User not found');
return 'Alice';
}
class UserProfilePage extends StatelessWidget {
final String userId;
const UserProfilePage({super.key, required this.userId});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Profile')),
body: FutureBuilder<String>(
// Future создаём ВНЕ build() или через late/initState,
// иначе при каждом rebuild будет новый запрос
future: fetchUserName(userId),
builder: (context, snapshot) {
// Три состояния ConnectionState
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
}
if (snapshot.hasError) {
return Center(child: Text('Error: ${snapshot.error}'));
}
// snapshot.data гарантированно non-null здесь
return Center(child: Text('Hello, ${snapshot.data}!'));
},
),
);
}
}
StreamBuilder — непрерывный поток данных
StreamBuilder подписывается на Stream и обновляет UI при каждом новом событии. Используется для real-time данных — WebSocket, Firestore snapshots, локальные события (датчики, таймеры, состояние авторизации).
import 'dart:async';
import 'package:flutter/material.dart';
// Stream, который каждую секунду отдаёт текущее время
Stream<DateTime> clockStream() =>
Stream.periodic(const Duration(seconds: 1), (_) => DateTime.now());
class ClockPage extends StatelessWidget {
const ClockPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Live Clock')),
body: StreamBuilder<DateTime>(
stream: clockStream(),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return const Center(child: Text('Starting...'));
}
if (snapshot.hasError) {
return Center(child: Text('Error: ${snapshot.error}'));
}
final time = snapshot.data!;
return Center(
child: Text(
'${time.hour}:${time.minute.toString().padLeft(2, '0')}:${time.second.toString().padLeft(2, '0')}',
style: const TextStyle(fontSize: 48),
),
);
},
),
);
}
}
Правильное место для создания Future
Критическая ошибка: создавать Future прямо в методе build(). Каждый rebuild создаёт новый Future и новый запрос:
// ПЛОХО: новый запрос при каждом setState() родителя
FutureBuilder(
future: api.fetchData(), // вызывается при каждом build!
...
)
// ХОРОШО: храним Future в State
class _MyPageState extends State<MyPage> {
late final Future<Data> _dataFuture;
@override
void initState() {
super.initState();
_dataFuture = api.fetchData(); // создаётся один раз
}
@override
Widget build(BuildContext context) {
return FutureBuilder(
future: _dataFuture, // передаём сохранённый Future
builder: (context, snapshot) { ... },
);
}
}
Когда что использовать
| Сценарий | Виджет |
|---|---|
| HTTP-запрос при загрузке экрана | FutureBuilder |
| Firestore / WebSocket данные | StreamBuilder |
| Чтение из локальной БД (один раз) | FutureBuilder |
| Состояние авторизации (auth changes) | StreamBuilder |
| Загрузка файла с прогрессом | StreamBuilder |
Подводные камни
- Future в build() — двойной запрос. Если родитель вызовет setState(), виджет перестроится и создаст новый Future. Всегда сохраняйте Future в полях State или используйте state management (Provider, Riverpod, BLoC).
- Stream не закрывается автоматически. Если Stream создаётся в State, его нужно закрывать в
dispose(). Незакрытый StreamController — утечка памяти. - ConnectionState.none vs waiting. Future возвращает
ConnectionState.noneесли переданnull, иwaitingпока Future не завершился. Игнорированиеnoneприводит к непредвиденному UI-состоянию. - snapshot.data может быть null даже при hasData == false. Всегда проверяйте
snapshot.hasDataиsnapshot.hasErrorперед обращением кsnapshot.data. - StreamBuilder держит последнее значение. При ошибке в Stream,
snapshot.dataвсё ещё содержит последнее успешное значение. Проверяйтеsnapshot.hasErrorпервым. - Лишние rebuild при каждом событии Stream. StreamBuilder перестраивает весь поддерево при каждом событии. Для частых событий (GPS, датчики) используйте
where/throttleна Stream или вынесите FutureBuilder/StreamBuilder на минимальный виджет.
Common mistakes
- Сводить «
FutureBuilderиStreamBuilderи когда использовать каждый» к синтаксису и не объяснять platform channel. - Игнорировать жизненный цикл, основной поток или момент освобождения ресурсов в сценарии flutter-14.
- Выбирать API по привычке, не проверяя состояние, ошибки, доступность и платформенные ограничения.
What the interviewer is testing
- Формулирует точную модель для «
FutureBuilderиStreamBuilderи когда использовать каждый» и подтверждает ее корректным примером. - Умеет связать ответ с widget tree, тестированием и отладкой на устройстве.
- Называет ограничения подхода flutter-14, включая производительность, память и сопровождение.