FlutterMiddleTechnical

Что такое 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, включая производительность, память и сопровождение.

Sources

Related topics