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

Что такое BehaviorSubject?

2.0 Middle🔥 171 комментариев
#State Management#Асинхронность

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

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

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

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 приложениях.