Управление состоянием с паттерном BLoC во Flutter
Управление состоянием — один из наиболее важных аспектов разработки на Flutter. Среди различных решений для управления состоянием паттерн BLoC (Business Logic Component) зарекомендовал себя как мощный и масштабируемый подход. Давайте разберёмся, как BLoC может преобразить ваши Flutter-приложения.
Что такое BLoC?
BLoC расшифровывается как Business Logic Component (компонент бизнес-логики). Это паттерн проектирования, который отделяет бизнес-логику от слоя пользовательского интерфейса, делая код более тестируемым, переиспользуемым и поддерживаемым. Паттерн использует Streams для обработки потока данных и изменений состояния.
Архитектура BLoC: разделение UI и бизнес-логики
Почему стоит выбрать BLoC?
BLoC обладает рядом преимуществ, которые выделяют его среди других решений для управления состоянием:
- Разделение ответственности: бизнес-логика полностью отделена от кода UI
- Тестируемость: легко писать юнит-тесты для бизнес-логики без зависимости от UI
- Переиспользуемость: BLoC можно использовать в нескольких виджетах и экранах
- Предсказуемость: однонаправленный поток данных делает изменения состояния предсказуемыми
- Независимость от платформы: логику BLoC можно переиспользовать между Flutter Web, Mobile и Desktop
Начало работы с BLoC
Для начала добавьте пакет BLoC в pubspec.yaml:
dependencies: flutter_bloc: ^8.1.3 equatable: ^2.0.5
Основные концепции
1. События (Events)
События — это входные данные для BLoC. Они представляют действия пользователя или системные события, которые вызывают изменения состояния. Вот как определяются события:
abstract class CounterEvent extends Equatable {
const CounterEvent();
@override
List<Object> get props => [];
}
class IncrementEvent extends CounterEvent {}
class DecrementEvent extends CounterEvent {}
class ResetEvent extends CounterEvent {}
2. Состояния (States)
Состояния — это выходные данные BLoC. Они описывают текущее состояние вашего приложения:
class CounterState extends Equatable {
final int count;
final bool isLoading;
const CounterState({
this.count = 0,
this.isLoading = false,
});
CounterState copyWith({
int? count,
bool? isLoading,
}) {
return CounterState(
count: count ?? this.count,
isLoading: isLoading ?? this.isLoading,
);
}
@override
List<Object> get props => [count, isLoading];
}
3. Создание BLoC
Теперь создадим BLoC, который связывает события с состояниями:
class CounterBloc extends Bloc<CounterEvent, CounterState> {
CounterBloc() : super(const CounterState()) {
on<IncrementEvent>(_onIncrement);
on<DecrementEvent>(_onDecrement);
on<ResetEvent>(_onReset);
}
void _onIncrement(IncrementEvent event, Emitter<CounterState> emit) {
emit(state.copyWith(count: state.count + 1));
}
void _onDecrement(DecrementEvent event, Emitter<CounterState> emit) {
emit(state.copyWith(count: state.count - 1));
}
void _onReset(ResetEvent event, Emitter<CounterState> emit) {
emit(const CounterState());
}
}
Поток архитектуры BLoC
Паттерн BLoC следует однонаправленному потоку данных:
- Слой UI: пользователь взаимодействует с интерфейсом и инициирует событие
- Событие: событие отправляется в BLoC
- BLoC: обрабатывает событие и выполняет бизнес-логику
- Состояние: BLoC генерирует новое состояние на основе результата
- Обновление UI: интерфейс перестраивается на основе нового состояния
Ключевые компоненты
BlocProvider
BlocProvider — это виджет, предоставляющий BLoC своим дочерним элементам. Он автоматически управляет созданием и уничтожением BLoC:
void main() {
runApp(
BlocProvider(
create: (context) => CounterBloc(),
child: MyApp(),
),
);
}
BlocBuilder
BlocBuilder — виджет, который перестраивается при получении новых состояний:
BlocBuilder<CounterBloc, CounterState>(
builder: (context, state) {
return Text(
'${state.count}',
style: Theme.of(context).textTheme.headline4,
);
},
)
BlocListener
BlocListener используется для побочных эффектов, таких как навигация или отображение snackbar:
BlocListener<CounterBloc, CounterState>(
listener: (context, state) {
if (state.count == 10) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Вы достигли 10!')),
);
}
},
child: Container(),
)
BlocConsumer
BlocConsumer объединяет BlocBuilder и BlocListener:
BlocConsumer<CounterBloc, CounterState>(
listener: (context, state) {
if (state.count == 10) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Вы достигли 10!')),
);
}
},
builder: (context, state) {
return Text('${state.count}');
},
)
Полный пример: приложение-счётчик
Вот полный пример, объединяющий всё воедино:
class CounterPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('BLoC Счётчик')),
body: Center(
child: BlocBuilder<CounterBloc, CounterState>(
builder: (context, state) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'Значение счётчика:',
style: TextStyle(fontSize: 20),
),
Text(
'${state.count}',
style: TextStyle(
fontSize: 48,
fontWeight: FontWeight.bold,
),
),
SizedBox(height: 20),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
FloatingActionButton(
onPressed: () {
context.read<CounterBloc>()
.add(DecrementEvent());
},
child: Icon(Icons.remove),
),
SizedBox(width: 20),
FloatingActionButton(
onPressed: () {
context.read<CounterBloc>()
.add(IncrementEvent());
},
child: Icon(Icons.add),
),
],
),
],
);
},
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
context.read<CounterBloc>().add(ResetEvent());
},
child: Icon(Icons.refresh),
),
);
}
}
Реальный пример: API-запрос с BLoC
Рассмотрим более практичный пример с API-запросами:
// События
abstract class UserEvent extends Equatable {
@override
List<Object> get props => [];
}
class FetchUserEvent extends UserEvent {}
// Состояния
abstract class UserState extends Equatable {
@override
List<Object> get props => [];
}
class UserInitial extends UserState {}
class UserLoading extends UserState {}
class UserLoaded extends UserState {
final User user;
UserLoaded(this.user);
@override
List<Object> get props => [user];
}
class UserError extends UserState {
final String message;
UserError(this.message);
@override
List<Object> get props => [message];
}
// BLoC
class UserBloc extends Bloc<UserEvent, UserState> {
final UserRepository repository;
UserBloc(this.repository) : super(UserInitial()) {
on<FetchUserEvent>(_onFetchUser);
}
Future<void> _onFetchUser(
FetchUserEvent event,
Emitter<UserState> emit,
) async {
emit(UserLoading());
try {
final user = await repository.fetchUser();
emit(UserLoaded(user));
} catch (e) {
emit(UserError(e.toString()));
}
}
}
Использование API BLoC в UI
class UserProfilePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Профиль пользователя')),
body: BlocBuilder<UserBloc, UserState>(
builder: (context, state) {
if (state is UserLoading) {
return Center(child: CircularProgressIndicator());
} else if (state is UserLoaded) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Имя: ${state.user.name}'),
Text('Email: ${state.user.email}'),
],
),
);
} else if (state is UserError) {
return Center(child: Text('Ошибка: ${state.message}'));
}
return Center(child: Text('Нажмите кнопку для загрузки пользователя'));
},
),
floatingActionButton: FloatingActionButton(
onPressed: () {
context.read<UserBloc>().add(FetchUserEvent());
},
child: Icon(Icons.refresh),
),
);
}
}
Лучшие практики
- Единственная ответственность: каждый BLoC должен отвечать за одну конкретную функцию или область
- Неизменяемые состояния: всегда используйте неизменяемые классы состояний с Equatable
- Именование событий: используйте чёткие, основанные на действиях названия (например,
LoginButtonPressed) - Именование состояний: используйте описательные названия, отражающие состояние
(например,
LoginLoading,LoginSuccess) - Обработка ошибок: всегда включайте состояния ошибок и обрабатывайте их корректно
- Тестирование: пишите полноценные юнит-тесты для ваших BLoC
Тестирование BLoC
Одно из главных преимуществ BLoC — это тестируемость. Вот как можно его протестировать:
void main() {
group('CounterBloc', () {
late CounterBloc counterBloc;
setUp(() {
counterBloc = CounterBloc();
});
tearDown(() {
counterBloc.close();
});
test('начальное состояние — CounterState с count равным 0', () {
expect(counterBloc.state, CounterState(count: 0));
});
blocTest<CounterBloc, CounterState>(
'генерирует [CounterState(count: 1)] при добавлении IncrementEvent',
build: () => counterBloc,
act: (bloc) => bloc.add(IncrementEvent()),
expect: () => [CounterState(count: 1)],
);
blocTest<CounterBloc, CounterState>(
'генерирует [CounterState(count: -1)] при добавлении DecrementEvent',
build: () => counterBloc,
act: (bloc) => bloc.add(DecrementEvent()),
expect: () => [CounterState(count: -1)],
);
});
}
BLoC и другие решения для управления состоянием
BLoC vs Provider
Provider проще и требует меньше шаблонного кода, однако BLoC обеспечивает лучшее разделение ответственности и лучше подходит для сложных приложений с большой бизнес-логикой.
BLoC vs Riverpod
Riverpod более гибок и обеспечивает безопасность на этапе компиляции, тогда как BLoC предлагает более структурированный подход с чёткими паттернами для событий и состояний.
BLoC vs GetX
GetX легковесен и прост в освоении, однако BLoC лучше тестируется и следует устоявшимся паттернам проектирования, что делает его предпочтительным для крупных приложений.
Когда использовать BLoC?
BLoC идеально подходит для:
- Крупных приложений со сложной бизнес-логикой
- Проектов с высокими требованиями к тестируемости
- Команд, ценящих структурированную архитектуру
- Приложений, требующих переиспользования логики на разных платформах
- Проектов, над которыми работает несколько разработчиков одновременно
Заключение
Паттерн BLoC — это мощное решение для управления состоянием, привносящее структуру, тестируемость и масштабируемость во Flutter-приложения. Несмотря на более высокий порог вхождения по сравнению с более простыми решениями, преимущества становятся очевидными по мере роста приложения.
Отделяя бизнес-логику от UI, BLoC делает код более поддерживаемым и удобным для тестирования. Если вы создаёте продакшн-приложение на Flutter, особенно с комплексными требованиями, BLoC определённо заслуживает вашего внимания.
Список статей