Что такое BehaviorSubject?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
BehaviorSubject: Что это и зачем нужен
BehaviorSubject — это специальный тип Subject в Dart (из пакета rxdart), который хранит последнее значение и автоматически передаёт его новым подписчикам при подписке. Это один из самых важных паттернов для state management в Flutter.
Основные концепции
Subject — это объект, который является одновременно:
- Observable (издатель событий)
- Observer (потребитель событий)
BehaviorSubject добавляет к этому:
- Хранение последнего значения (state)
- Автоматическая отправка этого значения новым подписчикам
// Обычный Stream — новый подписчик не получит старые значения
final stream = Stream.fromIterable([1, 2, 3]);
stream.listen(print); // Выведет: 1, 2, 3
stream.listen(print); // Тоже выведет: 1, 2, 3
// BehaviorSubject — хранит состояние
final subject = BehaviorSubject<int>();
subject.add(1);
subject.listen(print); // Выведет: 1 (текущее значение!)
subject.add(2);
subject.listen(print); // Выведет: 2 (текущее значение!)
Установка пакета
flutter pub add rxdart
dependencies:
rxdart: ^0.27.0
Создание и использование
Базовый пример:
import 'package:rxdart/rxdart.dart';
class CounterService {
// Создаёмся BehaviorSubject с начальным значением
final BehaviorSubject<int> _counterSubject = BehaviorSubject<int>(seededValue: 0);
// Expose как Stream (только для чтения)
Stream<int> get counterStream => _counterSubject.stream;
// Получить текущее значение синхронно
int get currentCounter => _counterSubject.value;
void increment() {
_counterSubject.add(_counterSubject.value + 1);
}
void dispose() {
_counterSubject.close();
}
}
// Использование:
void main() {
final service = CounterService();
// Подписка 1
service.counterStream.listen((value) {
print("Listener 1: $value");
});
// Выведет: "Listener 1: 0" (текущее значение)
service.increment();
// Выведет: "Listener 1: 1"
// Подписка 2
service.counterStream.listen((value) {
print("Listener 2: $value");
});
// Выведет: "Listener 2: 1" (текущее значение, не 0!)
service.increment();
// Выведет: "Listener 1: 2" и "Listener 2: 2"
}
BehaviorSubject в State Management
Пример: Аутентификация пользователя
class AuthService {
final BehaviorSubject<User?> _userSubject = BehaviorSubject<User?>(seededValue: null);
Stream<User?> get userStream => _userSubject.stream;
User? get currentUser => _userSubject.value;
bool get isAuthenticated => _userSubject.value != null;
Future<void> login(String email, String password) async {
try {
final user = await api.login(email, password);
_userSubject.add(user); // Обновляем состояние
} catch (e) {
_userSubject.addError(e); // Отправляем ошибку подписчикам
}
}
Future<void> logout() async {
await api.logout();
_userSubject.add(null); // Очищаем состояние
}
void dispose() {
_userSubject.close();
}
}
// Использование в Widget:
class LoginScreen extends StatelessWidget {
final AuthService authService = AuthService();
@override
Widget build(BuildContext context) {
return StreamBuilder<User?>(
stream: authService.userStream,
builder: (context, snapshot) {
if (snapshot.hasData && snapshot.data != null) {
// Пользователь залогинен
return HomeScreen(user: snapshot.data!);
} else if (snapshot.hasError) {
// Ошибка при логине
return ErrorScreen(error: snapshot.error);
} else {
// Загрузка
return LoadingScreen();
}
},
);
}
}
Варианты BehaviorSubject
1. BehaviorSubject с начальным значением
// Есть начальное значение
final subject = BehaviorSubject<int>(seededValue: 0);
subject.listen(print); // Сразу выведет: 0
2. BehaviorSubject без начального значения (если значение обязательно)
// Опция 1: Nullable
final subject = BehaviorSubject<int?>(seededValue: null);
// Опция 2: Обрабатываем отсутствие значения
final subject = BehaviorSubject<int>();
try {
final value = subject.value; // Выбросит StateError если нет значения
} catch (e) {
print("Нет значения в subject");
}
// Опция 3: Проверяем наличие значения
if (subject.hasValue) {
print("Значение есть: ${subject.value}");
}
Практические примеры
Пример 1: Фильтрация и трансформация данных
class SearchService {
final BehaviorSubject<String> _querySubject = BehaviorSubject<String>(seededValue: '');
late BehaviorSubject<List<Product>> _resultsSubject;
Stream<List<Product>> get results => _resultsSubject.stream;
SearchService() {
// Создаём новый stream, который зависит от query
_resultsSubject = BehaviorSubject<List<Product>>(seededValue: []);
// Слушаем изменения в query
_querySubject.stream
.debounceTime(const Duration(milliseconds: 300))
.asyncMap((query) => api.search(query))
.listen(
(results) => _resultsSubject.add(results),
onError: (error) => _resultsSubject.addError(error),
);
}
void search(String query) {
_querySubject.add(query);
}
void dispose() {
_querySubject.close();
_resultsSubject.close();
}
}
Пример 2: Форма со множественными полями
class FormService {
final BehaviorSubject<String> emailSubject = BehaviorSubject(seededValue: '');
final BehaviorSubject<String> passwordSubject = BehaviorSubject(seededValue: '');
final BehaviorSubject<bool> agreeTermsSubject = BehaviorSubject(seededValue: false);
// Комбинируем все поля для проверки валидности
late Stream<bool> isFormValid;
FormService() {
isFormValid = Rx.combineLatest3(
emailSubject,
passwordSubject,
agreeTermsSubject,
(email, password, agreeTerms) {
return email.isNotEmpty &&
password.length >= 8 &&
agreeTerms;
},
).startWith(false); // Начальное значение
}
void updateEmail(String email) => emailSubject.add(email);
void updatePassword(String password) => passwordSubject.add(password);
void toggleTerms(bool value) => agreeTermsSubject.add(value);
void dispose() {
emailSubject.close();
passwordSubject.close();
agreeTermsSubject.close();
}
}
// Использование:
class RegisterScreen extends StatefulWidget {
@override
State<RegisterScreen> createState() => _RegisterScreenState();
}
class _RegisterScreenState extends State<RegisterScreen> {
late FormService formService;
@override
void initState() {
super.initState();
formService = FormService();
}
@override
Widget build(BuildContext context) {
return Column(
children: [
TextField(
onChanged: formService.updateEmail,
decoration: const InputDecoration(hintText: 'Email'),
),
TextField(
onChanged: formService.updatePassword,
obscureText: true,
decoration: const InputDecoration(hintText: 'Password'),
),
StreamBuilder<bool>(
stream: formService.isFormValid,
builder: (context, snapshot) {
final isValid = snapshot.data ?? false;
return ElevatedButton(
onPressed: isValid ? submit : null,
child: const Text('Register'),
);
},
),
],
);
}
@override
void dispose() {
formService.dispose();
super.dispose();
}
}
Пример 3: Кэширование с BehaviorSubject
class CacheService<T> {
final BehaviorSubject<T?> _cacheSubject = BehaviorSubject(seededValue: null);
Stream<T> get cache => _cacheSubject.stream.whereType<T>();
T? get value => _cacheSubject.value;
Future<T> fetch(Future<T> Function() fetcher) async {
// Если уже есть в кэше, вернуть его
if (_cacheSubject.value != null) {
return _cacheSubject.value!;
}
try {
final data = await fetcher();
_cacheSubject.add(data);
return data;
} catch (e) {
_cacheSubject.addError(e);
rethrow;
}
}
void invalidate() {
_cacheSubject.add(null);
}
void dispose() {
_cacheSubject.close();
}
}
BehaviorSubject vs другие Subject'ы
// PublishSubject — не хранит значение
final publish = PublishSubject<int>();
publish.add(1);
publish.listen(print); // Ничего не выведет (1 уже прошла)
// ReplaySubject — хранит несколько последних значений
final replay = ReplaySubject<int>(maxSize: 3);
replay.add(1);
replay.add(2);
replay.add(3);
replay.listen(print); // Выведет: 1, 2, 3
// BehaviorSubject — хранит только последнее значение
final behavior = BehaviorSubject<int>(seededValue: 0);
behavior.add(1);
behavior.add(2);
behavior.add(3);
behavior.listen(print); // Выведет: 3
Best Practices
✅ ДЕЛАЙ:
// Expose stream, не сам subject
Stream<int> get counter => _counterSubject.stream;
// Используй seededValue для начального состояния
final subject = BehaviorSubject<int>(seededValue: 0);
// Закрывай subject в dispose
void dispose() {
_subject.close();
}
// Обрабатывай ошибки
.catchError((e) => defaultValue)
❌ НЕ ДЕЛАЙ:
// Не expose сам subject
BehaviorSubject<int> get counter => _counterSubject; // ❌
// Не забывай закрывать
// (может привести к memory leak)
// Не используй в build method без StreamBuilder
stream.listen(...) // ❌ Создаст новую подписку каждый раз
Альтернативы
Сейчас есть более современный подход с Riverpod:
final counterProvider = StateNotifierProvider<CounterNotifier, int>((ref) {
return CounterNotifier();
});
class CounterNotifier extends StateNotifier<int> {
CounterNotifier() : super(0);
void increment() => state++;
}
Это более type-safe и лучше интегрируется с Flutter.
Итог
BehaviorSubject — это:
- Способ хранить и распределять состояние (state management)
- Гарантирует, что новые подписчики получат последнее значение
- Essential для реактивного программирования в Flutter
- Требует аккуратного управления памятью (dispose)
Это один из основных инструментов для работы с асинхронным состоянием в Flutter приложениях.