← Назад к вопросам

Приведи пример использования StreamBuilder

2.0 Middle🔥 261 комментариев
#Flutter виджеты#State Management#Асинхронность

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI26 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

StreamBuilder в Flutter: работа с асинхронными потоками данных

StreamBuilder — это виджет, позволяющий строить UI на основе Stream данных. Это одна из самых важных техник реактивного программирования в Flutter.

Что такое StreamBuilder

StreamBuilder — это виджет, который "слушает" Stream и перестраивается каждый раз, когда в Stream поступляют новые данные. Это идеально подходит для реактивного обновления UI.

// Базовая структура
StreamBuilder<T>(
  stream: myStream, // Stream данных
  initialData: initialValue, // начальное значение (опционально)
  builder: (context, snapshot) {
    // snapshot содержит текущее состояние Stream
    if (snapshot.connectionState == ConnectionState.waiting) {
      return CircularProgressIndicator(); // загрузка
    } else if (snapshot.hasError) {
      return Text('Error: ${snapshot.error}'); // ошибка
    } else if (snapshot.hasData) {
      return Text('Data: ${snapshot.data}'); // данные готовы
    } else {
      return Text('No data'); // нет данных
    }
  },
)

Как работает Stream

Stream — это асинхронная последовательность событий (data events или error events). Думайте о нём как о потоке данных со временем.

// Stream можно создать несколькими способами

// 1. Из List через Stream.fromIterable
var myStream = Stream.fromIterable([1, 2, 3, 4, 5]);

// 2. С задержкой через Stream.periodic
var tickStream = Stream.periodic(Duration(seconds: 1), (count) => count);

// 3. Вручную через StreamController
var controller = StreamController<int>();
controller.add(1);
controller.add(2);
controller.close();

var myStream = controller.stream;

Практический пример: загрузка данных пользователя

class UserRepository {
  // Возвращаем Stream вместо Future для continuous обновлений
  Stream<User> getUser(String userId) async* {
    try {
      // Первый emit — загружаем базовые данные
      final response = await http.get(
        Uri.parse('https://api.example.com/users/$userId'),
      );
      
      if (response.statusCode == 200) {
        final user = User.fromJson(jsonDecode(response.body));
        yield user; // первое значение в Stream
      } else {
        throw Exception('Failed to load user');
      }
    } catch (e) {
      // Если ошибка, Stream автоматически её обработает
      rethrow;
    }
  }
}

// Использование StreamBuilder
class UserProfileScreen extends StatelessWidget {
  final String userId;
  final userRepository = UserRepository();

  UserProfileScreen({required this.userId});

  @override
  Widget build(BuildContext context) {
    return StreamBuilder<User>(
      stream: userRepository.getUser(userId),
      builder: (context, snapshot) {
        if (snapshot.connectionState == ConnectionState.waiting) {
          return Scaffold(
            appBar: AppBar(title: const Text('Profile')),
            body: const Center(child: CircularProgressIndicator()),
          );
        } else if (snapshot.hasError) {
          return Scaffold(
            appBar: AppBar(title: const Text('Profile')),
            body: Center(
              child: Text('Error: ${snapshot.error}'),
            ),
          );
        } else if (snapshot.hasData) {
          final user = snapshot.data!;
          return Scaffold(
            appBar: AppBar(title: Text(user.name)),
            body: Center(
              child: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  Text('Name: ${user.name}'),
                  Text('Email: ${user.email}'),
                  Text('Age: ${user.age}'),
                ],
              ),
            ),
          );
        } else {
          return Scaffold(
            appBar: AppBar(title: const Text('Profile')),
            body: const Center(child: Text('No data')),
          );
        }
      },
    );
  }
}

Пример с real-time обновлениями: счётчик

class CounterService {
  final _counterController = StreamController<int>();

  // Expose Stream для слушания
  Stream<int> get counterStream => _counterController.stream;

  int _count = 0;

  void increment() {
    _count++;
    _counterController.add(_count); // emit новое значение
  }

  void decrement() {
    _count--;
    _counterController.add(_count);
  }

  void reset() {
    _count = 0;
    _counterController.add(_count);
  }

  void dispose() {
    _counterController.close();
  }
}

class CounterApp extends StatefulWidget {
  @override
  State<CounterApp> createState() => _CounterAppState();
}

class _CounterAppState extends State<CounterApp> {
  late CounterService _counterService;

  @override
  void initState() {
    super.initState();
    _counterService = CounterService();
  }

  @override
  void dispose() {
    _counterService.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Counter')),
      body: Center(
        child: StreamBuilder<int>(
          stream: _counterService.counterStream,
          initialData: 0, // начальное значение
          builder: (context, snapshot) {
            return Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                Text(
                  'Count: ${snapshot.data}',
                  style: Theme.of(context).textTheme.headlineMedium,
                ),
                const SizedBox(height: 20),
                Row(
                  mainAxisAlignment: MainAxisAlignment.center,
                  children: [
                    ElevatedButton(
                      onPressed: _counterService.decrement,
                      child: const Text('Decrement'),
                    ),
                    const SizedBox(width: 20),
                    ElevatedButton(
                      onPressed: _counterService.reset,
                      child: const Text('Reset'),
                    ),
                    const SizedBox(width: 20),
                    ElevatedButton(
                      onPressed: _counterService.increment,
                      child: const Text('Increment'),
                    ),
                  ],
                ),
              ],
            );
          },
        ),
      ),
    );
  }
}

Поиск в реальном времени (с debounce)

class SearchService {
  final _searchController = StreamController<String>();
  final _resultsController = StreamController<List<String>>();

  Stream<List<String>> get resultsStream => _resultsController.stream;

  SearchService() {
    // Слушаем поисковые запросы с debounce
    _searchController.stream
        .debounceTime(Duration(milliseconds: 300)) // ждём 300ms без ввода
        .distinct() // игнорируем дублирующиеся запросы
        .asyncMap((query) => _performSearch(query))
        .listen(
          (results) => _resultsController.add(results),
          onError: (error) => _resultsController.addError(error),
        );
  }

  void search(String query) {
    _searchController.add(query);
  }

  Future<List<String>> _performSearch(String query) async {
    if (query.isEmpty) return [];
    
    // Имитируем API запрос
    await Future.delayed(Duration(milliseconds: 500));
    
    final allItems = [
      'Flutter',
      'Dart',
      'Firebase',
      'Provider',
      'Riverpod',
    ];
    
    return allItems
        .where((item) => item.toLowerCase().contains(query.toLowerCase()))
        .toList();
  }

  void dispose() {
    _searchController.close();
    _resultsController.close();
  }
}

class SearchScreen extends StatefulWidget {
  @override
  State<SearchScreen> createState() => _SearchScreenState();
}

class _SearchScreenState extends State<SearchScreen> {
  late SearchService _searchService;
  late TextEditingController _textController;

  @override
  void initState() {
    super.initState();
    _searchService = SearchService();
    _textController = TextEditingController();
    _textController.addListener(() {
      _searchService.search(_textController.text);
    });
  }

  @override
  void dispose() {
    _searchService.dispose();
    _textController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Search')),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          children: [
            TextField(
              controller: _textController,
              decoration: InputDecoration(
                hintText: 'Search...',
                prefixIcon: const Icon(Icons.search),
                border: OutlineInputBorder(
                  borderRadius: BorderRadius.circular(8),
                ),
              ),
            ),
            const SizedBox(height: 16),
            Expanded(
              child: StreamBuilder<List<String>>(
                stream: _searchService.resultsStream,
                builder: (context, snapshot) {
                  if (snapshot.connectionState == ConnectionState.waiting) {
                    return const Center(child: CircularProgressIndicator());
                  } else if (snapshot.hasError) {
                    return Center(
                      child: Text('Error: ${snapshot.error}'),
                    );
                  } else if (snapshot.hasData && snapshot.data!.isNotEmpty) {
                    return ListView.builder(
                      itemCount: snapshot.data!.length,
                      itemBuilder: (context, index) {
                        return ListTile(
                          title: Text(snapshot.data![index]),
                        );
                      },
                    );
                  } else {
                    return const Center(
                      child: Text('No results found'),
                    );
                  }
                },
              ),
            ),
          ],
        ),
      ),
    );
  }
}

Мониторинг WebSocket соединения

class WebSocketService {
  late WebSocket _webSocket;
  final _messageController = StreamController<String>.broadcast();

  Stream<String> get messageStream => _messageController.stream;

  Future<void> connect(String url) async {
    try {
      _webSocket = await WebSocket.connect(url);
      _webSocket.listen(
        (data) => _messageController.add(data), // новое сообщение
        onError: (error) => _messageController.addError(error),
        onDone: () => _messageController.close(),
      );
    } catch (e) {
      _messageController.addError(e);
    }
  }

  void send(String message) {
    _webSocket.add(message);
  }

  void dispose() {
    _webSocket.close();
    _messageController.close();
  }
}

class ChatScreen extends StatefulWidget {
  @override
  State<ChatScreen> createState() => _ChatScreenState();
}

class _ChatScreenState extends State<ChatScreen> {
  late WebSocketService _wsService;

  @override
  void initState() {
    super.initState();
    _wsService = WebSocketService();
    _wsService.connect('ws://echo.websocket.org');
  }

  @override
  void dispose() {
    _wsService.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Chat')),
      body: StreamBuilder<String>(
        stream: _wsService.messageStream,
        builder: (context, snapshot) {
          if (snapshot.connectionState == ConnectionState.waiting) {
            return const Center(child: CircularProgressIndicator());
          } else if (snapshot.hasError) {
            return Center(
              child: Text('Connection error: ${snapshot.error}'),
            );
          } else if (snapshot.hasData) {
            return Center(
              child: Text('Received: ${snapshot.data}'),
            );
          } else {
            return const Center(child: Text('No messages'));
          }
        },
      ),
    );
  }
}

Stream vs StreamBuilder vs FutureBuilder

// FutureBuilder — для одноразовых асинхронных операций
FutureBuilder<List<User>>(
  future: userRepository.getUsers(), // один раз
  builder: (context, snapshot) { ... },
)

// StreamBuilder — для continuous потока данных
StreamBuilder<List<User>>(
  stream: userRepository.getUsersStream(), // постоянно обновляется
  builder: (context, snapshot) { ... },
)

Best Practices

Всегда закрывайте StreamController:

@override
void dispose() {
  _controller.close(); // обязательно!
  super.dispose();
}

Используйте .broadcast() для множественных слушателей:

// ❌ Плохо — не может быть двух слушателей
final stream = Stream.periodic(Duration(seconds: 1));

// ✅ Хорошо — может быть множество слушателей
final stream = Stream.periodic(Duration(seconds: 1)).asBroadcastStream();
// или
final controller = StreamController<int>.broadcast();

Используйте RxDart для удобных операций:

import 'package:rxdart/rxdart.dart';

// debounce, throttle, switchMap и другие операторы
searchStream
    .debounceTime(Duration(milliseconds: 300))
    .switchMap((query) => _performSearch(query))
    .listen((results) => updateUI(results));

Выводы

StreamBuilder идеален для:

  • Real-time обновлений данных
  • WebSocket соединений
  • Поиска и фильтрации в реальном времени
  • Мониторинга состояния приложения
  • Реактивного программирования в Flutter

Это один из самых мощных инструментов для создания реактивных и отзывчивых приложений.