BLoC у Flutter без стресу: Як нарешті розібратися зі станами
Гайд для початківців
Щойно ви починаєте розробляти Flutter-застосунок, усе здається простим: натиснув кнопку — оновив екран. Але тільки-но з’являється більше екранів, динамічних даних і взаємодій, утримати все в голові стає складно. Як зрозуміти, що саме змінилось і чому? Як оновити інтерфейс, не заплутуючись у купі змінних?
У таких ситуаціях приходить на допомогу керування станом — спосіб організувати логіку змін у застосунку так, щоб інтерфейс автоматично реагував на них. Один із підходів у Flutter — це BLoC (Business Logic Component). Він дає змогу розділити бізнес-логіку та UI, що особливо важливо у складних або масштабованих проєктах.
У цій статті ми з’ясуємо, як працює BLoC, чому він корисний, і розберемо приклади.
Що таке BLoC?
BLoC — це архітектурний патерн, який дає змогу відокремити логіку програми (тобто те, як ухвалюються рішення, змінюються дані, обробляються дії користувача) від інтерфейсу користувача. Основна ідея полягає в тому, щоб UI лише «слухав» зміни, а не сам вирішував, що й коли змінювати.
Усе працює на основі потоків (Streams):
1. Користувач викликає подію — наприклад, натискає кнопку.
2. Ця подія надходить у BLoC, де обробляється згідно з бізнес-логікою.
3. Як результат BLoC «видає» новий стан (state), який оновлює інтерфейс.
Завдяки такому підходу можна:
- централізувати логіку ухвалення рішень;
- полегшити тестування (бо UI вже не містить складної логіки);
- створювати повторно використовувані та незалежні частини застосунку.
Переваги та недоліки BLoC
Flutter BLoC став одним із найпопулярніших рішень для керування станом у Flutter, і на це є кілька вагомих причин.
З одного боку, він забезпечує чіткий розподіл відповідальностей: інтерфейс просто відтворює те, що надсилає логіка, і не знає, як саме ці дані оброблялися. Завдяки цьому програма стає більш передбачуваною, зручною для налагодження й тестування. Крім того, Flutter BLoC має гарну документацію, активну спільноту й великий набір інструментів, які полегшують розробку.
З іншого боку, Flutter BLoC — це не завжди найпростіший варіант. Для новачків він може здатися трохи складним через велику кількість шаблонного коду та абстракцій. І якщо у вас дуже простий застосунок без складної логіки — наприклад, лише кілька кнопок та форм — то простіші підходи (як-от Provider або setState) можуть бути доречнішими.
Навчання через практику
Щоб по-справжньому зрозуміти, як працює BLoC, найкраще не просто читати теорію, а одразу спробувати її в дії.
Ми створимо невеликий застосунок, який покаже, як події в ньому змінюють стани, а стани — змінюють UI.
Цей підхід допоможе вам:
- зрозуміти основну ідею потоку «подія → обробка → стан → інтерфейс»;
- попрактикуватися у створенні власних подій і станів;
- побачити, як Flutter BLoC дає змогу відокремити бізнес-логіку від UI.
Як приклад зробимо простий застосунок, який змінює текст на екрані за кожного натискання кнопки.
Початкове налаштування
Перед тим як зануритися в код, варто налаштувати проєкт і встановити потрібні залежності. Це невеликий приклад, але навіть у ньому ми одразу дотримуватимемося гарної структури — це спростить масштабування та підтримку коду в майбутньому.
1. Додайте потрібні пакети
Відкрийте файл pubspec.yaml і переконайтеся, що у вас додані такі залежності:
dependencies:
flutter:
sdk: flutter
flutter_bloc: ^8.1.2
equatable: ^2.0.5
dev_dependencies:
flutter_test:
sdk: flutter
bloc_test: ^9.1.1
flutter_bloc — основна бібліотека для реалізації BLoC у Flutter;
equatable — дає змогу легко порівнювати об’єкти станів без зайвого коду;
bloc_test — допомагає писати тести для BLoC-логіки.
Після цього виконайте команду:
flutter pub get
2. Організуйте структуру проєкту
Незалежно від розміру застосунку структурований код — це ключ до зручності. Наш проєкт матиме таку структуру:
lib/
├── app.dart # Точка входу, основна обгортка застосунку
├── text_controller.dart # UI-компонент із кнопкою й текстом
└── bloc/
├── app_bloc.dart # Основна BLoC-логіка
├── app_event.dart # Події (Events)
└── app_state.dart # Стани (States)
Тека bloc містить усю бізнес-логіку, а інтерфейс залишається максимально простим і «чистим». Такий поділ дає змогу змінювати чи переробляти логіку без втручання в UI — і навпаки.
Події та стани в BLoC: основа реактивності
Щоб зрозуміти, як працює BLoC, потрібно чітко розрізняти два ключові поняття:
- Подія (Event) — це дія користувача або якась зміна, що запускає обробку. Наприклад, натискання кнопки чи надходження нових даних.
- Стан (State) — це «знімок» поточного стану застосунку, який може змінитися у відповідь на подію.
BLoC — це механізм, який приймає потік подій, обробляє їх та генерує відповідний потік станів.
Візуально це має такий вигляд:
[Event] → (BLoC логіка) → [State] → UI
Створення події
Створимо подію, яка запускатиме зміну тексту за натискання кнопки.
bloc/app_event.dart
import 'package:meta/meta.dart';
@immutable
abstract class AppEvent {
const AppEvent();
}
class ChangeTextEvent extends AppEvent {
const ChangeTextEvent();
}
Пояснення:
- Ми створюємо абстрактний клас
AppEvent, щоб у майбутньому легко додавати нові типи подій. - Клас
ChangeTextEventзастосовуватиметься, коли користувач натискає кнопку.
Важливо: BLoC очікує, що всі події наслідуватимуть один базовий клас. Це дає змогу обробляти різні події через єдиний вхід у BLoC.
Створення стану
Тепер опишемо стан, який зберігає інформацію, що має відтворитися в UI — наприклад, індекс і текст.
bloc/app_state.dart
import 'package:equatable/equatable.dart';
import 'package:meta/meta.dart';
@immutable
class AppState extends Equatable {
final int index;
final String text;
const AppState({
required this.index,
required this.text,
});
const AppState.initial()
: index = 0,
text = 'Початковий текст';
@override
List<Object> get props => [index, text];
}
Пояснення:
AppStateописує все, що нам потрібно для побудови UI.AppState.initial()— це початковий стан, з яким завантажується застосунок.- Ми використовуємо
Equatable, щоб Flutter міг порівнювати стани (і не оновлював UI зайвий раз).
Реалізація BLoC: як працює логіка
На цьому етапі ми об’єднаємо все, що зробили раніше: події, стани та їхню обробку. Тут і відбувається вся «магія» — ми реагуємо на події, оновлюємо стан і передаємо його назад в UI.
bloc/app_bloc.dart
import 'package:flutter_bloc/flutter_bloc.dart';
import 'app_event.dart';
import 'app_state.dart';
class AppBloc extends Bloc<AppEvent, AppState> {
final List<String> _texts = [
'Початковий текст',
'Змінений текст',
'Ще одна зміна',
];
AppBloc() : super(const AppState.initial()) {
on<ChangeTextEvent>((event, emit) {
int nextIndex = state.index + 1;
if (nextIndex >= _texts.length) {
nextIndex = 0;
}
emit(
AppState(
index: nextIndex,
text: _texts[nextIndex],
),
);
});
}
}
Як це працює:
- Клас
AppBlocуспадковується відBloc<AppEvent, AppState>— тобто він приймає події та виводить стани. - У конструкторі ми визначаємо, що за надходження
ChangeTextEventтреба:- збільшити індекс;
- взяти відповідний рядок з
_texts; - створити новий
AppStateз оновленими даними; - emit — передати новий стан далі, у потік, який слухає UI.
Цей підхід дає нам змогу повністю відокремити логіку від інтерфейсу, що робить застосунок гнучким і передбачуваним.
Підключення BLoC до інтерфейсу
Щоб BLoC міг працювати у Flutter-застосунку, потрібно:
1. Надати BLoC на верхньому рівні віджета через BlocProvider.
2. Відтворювати UI відповідно до стану через BlocBuilder або BlocConsumer.
3. Тригерити події, наприклад, у відповідь на натискання кнопки.
main.dart або app.dart
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'bloc/app_bloc.dart';
import 'bloc/app_state.dart';
import 'bloc/app_event.dart';
import 'text_controller.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter BLoC Demo',
home: BlocProvider(
create: (_) => AppBloc(),
child: const TextChangePage(),
),
);
}
}
Що тут відбувається:
- Ми огортаємо головну сторінку в
BlocProvider, який створює та надає екземплярAppBloc. - Завдяки цьому всі дочірні віджети зможуть взаємодіяти з BLoC через
context.read<AppBloc>()абоcontext.watch<AppBloc>().
text_change_page.dart — інтерфейс і взаємодія
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'bloc/app_bloc.dart';
import 'bloc/app_event.dart';
import 'bloc/app_state.dart';
class TextChangePage extends StatelessWidget {
const TextChangePage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Зміна тексту')),
body: Center(
child: BlocConsumer<AppBloc, AppState>(
listener: (context, state) {
// можна виконувати побічні дії при зміні стану
},
builder: (context, state) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
state.text,
style: const TextStyle(fontSize: 24),
),
const SizedBox(height: 20),
ElevatedButton(
onPressed: () {
context.read<AppBloc>().add(const ChangeTextEvent());
},
child: const Text('Змінити текст'),
),
],
);
},
),
),
);
}
}
Компоненти:
BlocConsumerпоєднує:builder— перебудовує UI за зміни стану;listener— дозволяє реагувати на зміну стану побічними ефектами (наприклад, показати SnackBar).
- Кнопка додає подію
ChangeTextEvent, що запускає зміну стану. - Новий текст відтворюється в UI — і все це без жодного
setState.
Як бачимо, UI став максимально «реактивним»: BLoC просто повідомляє про новий стан, а інтерфейс автоматично реагує.
Тестування BLoC
Тестування BLoC — одна з переваг цього підходу. Завдяки повному відокремленню логіки від інтерфейсу ми можемо просто й точно перевірити, як BLoC поводиться у відповідь на події.
Що нам потрібно:
Додайте до dev_dependencies у pubspec.yaml:
dev_dependencies:
flutter_test:
sdk: flutter
bloc_test: ^9.0.3
bloc_test — це офіційний пакет для зручного тестування Flutter BLoC.
Створимо файл test/app_bloc_test.dart
import 'package:bloc_test/bloc_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:your_app_name/bloc/app_bloc.dart';
import 'package:your_app_name/bloc/app_event.dart';
import 'package:your_app_name/bloc/app_state.dart';
void main() {
group('AppBloc', () {
blocTest<AppBloc, AppState>(
'має початковий стан AppState.initial()',
build: () => AppBloc(),
verify: (bloc) =>
expect(bloc.state, const AppState.initial()),
);
blocTest<AppBloc, AppState>(
'повертає новий стан після ChangeTextEvent',
build: () => AppBloc(),
act: (bloc) => bloc.add(const ChangeTextEvent()),
expect: () => const [
AppState(index: 1, text: 'Змінений текст'),
],
);
blocTest<AppBloc, AppState>(
'циклічно повертається до початкового тексту після кількох натискань',
build: () => AppBloc(),
act: (bloc) {
bloc.add(const ChangeTextEvent());
bloc.add(const ChangeTextEvent());
bloc.add(const ChangeTextEvent()); // має повернутися до індексу 0
},
expect: () => const [
AppState(index: 1, text: 'Змінений текст'),
AppState(index: 2, text: 'Ще одна зміна'),
AppState(index: 0, text: 'Початковий текст'),
],
);
});
}
Що ми тут перевіряємо:
- початковий стан BLoC має бути
AppState.initial(); - після одного
ChangeTextEventмає повернутися правильний новий стан; - після кількох натискань BLoC має правильно обробити логіку циклічної зміни.
Тести легко читаються, швидко запускаються та дають змогу впевнено змінювати логіку в майбутньому без страху все зламати.
На завершення
BLoC відкриває багато можливостей для створення складних та ефективних застосунків, що можуть масштабуватися без проблем.
Щоб дійсно зрозуміти, як ефективно працювати з Flutter BLoC, потрібно продовжувати експериментувати з ним, формувати нові події, стани та складнішу логіку. Вивчення таких інструментів, як-от freezed, допоможе вам значно покращити структуру коду й зменшити кількість шаблонного коду. Також варто приділяти увагу тестуванню, оскільки воно допомагає забезпечити надійність вашого застосунку на всіх етапах розробки.
Щоб отримати максимальну вигоду від цього підходу, досліджуйте всі можливості BLoC і з'ясуйте, як його можна використати в реальних складних проєктах, щоб створювати не лише робочі, а й ефективні застосунки.