Ответ
В архитектуре BLoC (Business Logic Component) отслеживание изменений состояния строится на Streams. Вот полная реализация с использованием flutter_bloc пакета:
1. Определение состояний и событий:
// events/counter_event.dart
abstract class CounterEvent {}
class IncrementCounter extends CounterEvent {}
class DecrementCounter extends CounterEvent {}
class ResetCounter extends CounterEvent {}
// states/counter_state.dart
abstract class CounterState {
const CounterState();
}
class CounterInitial extends CounterState {
const CounterInitial();
}
class CounterLoading extends CounterState {}
class CounterLoaded extends CounterState {
final int count;
final DateTime lastUpdated;
const CounterLoaded(this.count, this.lastUpdated);
@override
bool operator ==(Object other) {
return identical(this, other) ||
other is CounterLoaded &&
runtimeType == other.runtimeType &&
count == other.count &&
lastUpdated == other.lastUpdated;
}
@override
int get hashCode => count.hashCode ^ lastUpdated.hashCode;
}
class CounterError extends CounterState {
final String message;
const CounterError(this.message);
}
2. Реализация BLoC:
// bloc/counter_bloc.dart
import 'package:bloc/bloc.dart';
import 'package:meta/meta.dart';
class CounterBloc extends Bloc<CounterEvent, CounterState> {
CounterBloc() : super(const CounterInitial()) {
on<IncrementCounter>(_onIncrement);
on<DecrementCounter>(_onDecrement);
on<ResetCounter>(_onReset);
}
Future<void> _onIncrement(
IncrementCounter event,
Emitter<CounterState> emit,
) async {
try {
emit(CounterLoading());
// Имитация асинхронной операции
await Future.delayed(Duration(milliseconds: 100));
if (state is CounterLoaded) {
final currentState = state as CounterLoaded;
emit(CounterLoaded(
currentState.count + 1,
DateTime.now(),
));
} else {
emit(CounterLoaded(1, DateTime.now()));
}
} catch (e) {
emit(CounterError('Ошибка при инкременте: $e'));
}
}
Future<void> _onDecrement(
DecrementCounter event,
Emitter<CounterState> emit,
) async {
if (state is CounterLoaded) {
final currentState = state as CounterLoaded;
if (currentState.count > 0) {
emit(CounterLoaded(
currentState.count - 1,
DateTime.now(),
));
}
}
}
Future<void> _onReset(
ResetCounter event,
Emitter<CounterState> emit,
) async {
emit(CounterLoaded(0, DateTime.now()));
}
}
3. Отслеживание изменений в UI:
// main.dart или виджет
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class CounterPage extends StatelessWidget {
const CounterPage({super.key});
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => CounterBloc(),
child: Scaffold(
appBar: AppBar(title: Text('BLoC Counter')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// BlocBuilder перестраивает виджет при изменении состояния
BlocBuilder<CounterBloc, CounterState>(
builder: (context, state) {
return Column(
children: [
if (state is CounterLoading)
CircularProgressIndicator(),
if (state is CounterLoaded)
Column(
children: [
Text(
'Счётчик: ${state.count}',
style: TextStyle(fontSize: 48),
),
Text(
'Обновлено: ${state.lastUpdated.toString()}',
style: TextStyle(fontSize: 12, color: Colors.grey),
),
],
),
if (state is CounterError)
Text(
state.message,
style: TextStyle(color: Colors.red),
),
],
);
},
),
SizedBox(height: 20),
// BlocListener для side effects (навигация, показ диалогов)
BlocListener<CounterBloc, CounterState>(
listener: (context, state) {
if (state is CounterLoaded && state.count == 10) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Достигнут счётчик 10!')),
);
}
},
child: SizedBox.shrink(),
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
onPressed: () {
context.read<CounterBloc>().add(IncrementCounter());
},
child: Text('+'),
),
SizedBox(width: 20),
ElevatedButton(
onPressed: () {
context.read<CounterBloc>().add(DecrementCounter());
},
child: Text('-'),
),
SizedBox(width: 20),
ElevatedButton(
onPressed: () {
context.read<CounterBloc>().add(ResetCounter());
},
child: Text('Сброс'),
),
],
),
],
),
),
),
);
}
}
4. Cubit как упрощённая альтернатива:
// cubit/counter_cubit.dart
import 'package:flutter_bloc/flutter_bloc.dart';
class CounterCubit extends Cubit<int> {
CounterCubit() : super(0);
void increment() => emit(state + 1);
void decrement() => emit(state - 1);
void reset() => emit(0);
}
// Использование в UI:
BlocBuilder<CounterCubit, int>(
builder: (context, count) {
return Text('Счётчик: $count');
},
);
5. Мониторинг и отладка:
void main() {
Bloc.observer = SimpleBlocObserver();
runApp(MyApp());
}
class SimpleBlocObserver extends BlocObserver {
@override
void onCreate(BlocBase bloc) {
super.onCreate(bloc);
print('Создан: ${bloc.runtimeType}');
}
@override
void onEvent(Bloc bloc, Object? event) {
super.onEvent(bloc, event);
print('Событие: ${event.runtimeType} в ${bloc.runtimeType}');
}
@override
void onChange(BlocBase bloc, Change change) {
super.onChange(bloc, change);
print('Изменение в ${bloc.runtimeType}: $change');
}
@override
void onError(BlocBase bloc, Object error, StackTrace stackTrace) {
print('Ошибка в ${bloc.runtimeType}: $error');
super.onError(bloc, error, stackTrace);
}
}
Ключевые принципы:
- State — иммутабельный — каждое изменение создаёт новый объект состояния
- Event — триггеры изменений — пользовательские действия или системные события
- Bloc/Cubit — бизнес-логика — преобразует события в новые состояния
- BlocBuilder/BlocListener — реактивность — автоматическое обновление UI
- BlocProvider — dependency injection — предоставляет экземпляр BLoC в дереве виджетов
Преимущества подхода:
- Чёткое разделение бизнес-логики и представления
- Предсказуемое управление состоянием
- Легко тестируется (можно тестировать BLoC без UI)
- Поддержка undo/redo через отслеживание состояний
- Интеграция с
hydrated_blocдля сохранения состояния