BLoC паттерн во Flutter: Управление состоянием

Курс посвящен изучению архитектурного паттерна BLoC (Business Logic Component) для создания масштабируемых приложений на Flutter. Вы освоите работу с потоками, библиотекой flutter_bloc, Cubit и научитесь разделять бизнес-логику и пользовательский интерфейс.

1. Введение в реактивное программирование и концепцию BLoC: Streams и Sinks

Введение в реактивное программирование и концепцию BLoC: Streams и Sinks

Добро пожаловать в курс «BLoC паттерн во Flutter: Управление состоянием». Это первая статья, в которой мы заложим фундамент для понимания одного из самых популярных и мощных архитектурных паттернов во Flutter — BLoC (Business Logic Component).

Прежде чем мы начнем писать код и создавать сложные приложения, нам необходимо разобраться с базовыми концепциями, на которых строится этот паттерн. Речь пойдет о реактивном программировании, потоках данных (Streams) и точках входа (Sinks).

Что такое реактивное программирование?

В традиционном императивном программировании мы привыкли явно приказывать компьютеру, что делать. Например: «возьми значение А, прибавь к нему Б и запиши результат в Ц». Если значение А изменится позже, значение Ц останется прежним, пока мы снова явно не выполним команду обновления.

Реактивное программирование — это парадигма, ориентированная на потоки данных и распространение изменений. В этой модели, если меняется А, то Ц обновляется автоматически, потому что между ними установлена постоянная связь.

Представьте себе таблицу в Excel. Если вы зададите ячейке формулу =A1 + B1, то при любом изменении чисел в ячейках A1 или B1, результат обновится мгновенно. Вам не нужно нажимать кнопку «Пересчитать». Это и есть простейший пример реактивного поведения.

Во Flutter, который является декларативным фреймворком, реактивный подход работает идеально. Интерфейс (UI) — это просто отражение текущего состояния приложения. Когда состояние меняется, интерфейс перерисовывается.

Анатомия потоков: Streams и Sinks

В языке Dart, на котором написан Flutter, основой реактивности является класс Stream. Чтобы понять BLoC, вы должны чувствовать себя уверенно при работе с потоками.

Давайте используем аналогию с водопроводом.

!Схема работы потока данных: вход через Sink, передача через Stream, получение через Subscription.

1. Stream (Поток)

Stream — это источник асинхронных событий данных. Представьте его как конвейерную ленту, по которой к вам едут коробки (данные). Вы не знаете точно, когда приедет следующая коробка, но вы стоите у ленты и ждете. Как только коробка появляется, вы на нее реагируете.

В Dart потоки могут передавать: * Данные (значения, объекты). * Ошибки (если что-то пошло не так). * Сигнал о завершении («Done», когда поток закрывается).

2. Sink (Воронка/Вход)

Если Stream — это то, откуда данные выходят, то Sink — это то, куда мы эти данные кладем. Это входное отверстие нашей трубы. В контексте BLoC мы будем добавлять события (нажатия кнопок, запросы) именно в Sink.

3. StreamController (Контроллер потока)

Чтобы управлять этой системой, в Dart существует класс StreamController. Это «пульт управления», который объединяет в себе и Stream, и Sink.

Рассмотрим простой пример на Dart:

Когда вы запустите этот код, вы увидите в консоли:

Типы потоков

Важно знать, что в Dart существует два основных типа потоков:

  • Single-subscription Stream (Поток с одной подпиской).
  • Это самый частый тип. Такой поток позволяет подключить только одного слушателя (подписчика). Если вы попытаетесь подписаться второй раз, возникнет ошибка. Это похоже на обычный телефонный звонок: вы можете говорить только с одним человеком.

  • Broadcast Stream (Широковещательный поток).
  • Этот поток позволяет иметь множество слушателей одновременно. Это похоже на радиостанцию: передача идет одна, но слушать её могут тысячи приемников. Чтобы создать такой поток, используется StreamController.broadcast().

    Концепция BLoC

    Теперь, когда мы понимаем, как работают потоки, мы можем перейти к самому паттерну BLoC. Аббревиатура расшифровывается как Business Logic Component.

    Этот паттерн был разработан Google для того, чтобы отделить бизнес-логику от пользовательского интерфейса (UI). Это критически важно для создания поддерживаемых, тестируемых и масштабируемых приложений.

    Как это работает?

    BLoC — это «черный ящик», который преобразует поток Событий (Events) в поток Состояний (States).

    !Архитектура BLoC: UI отправляет события, BLoC обрабатывает их и возвращает состояния.

  • Events (События): Пользователь нажимает кнопку, вводит текст или приложение получает уведомление от сети. Это входные данные для BLoC. Они попадают в BLoC через Sink.
  • Business Logic (Логика): Внутри BLoC происходит магия. Мы анализируем событие, возможно, делаем запрос к базе данных или серверу, производим вычисления.
  • States (Состояния): Результат работы логики. Это может быть «Загрузка», «Данные получены» или «Ошибка». Состояния выходят из BLoC через Stream.
  • UI (Интерфейс): Экран подписывается на поток состояний и перерисовывается каждый раз, когда приходит новое состояние.
  • Почему именно потоки?

    Использование Streams для BLoC не случайно. Потоки обеспечивают: * Асинхронность: Интерфейс не зависает, пока выполняется тяжелая логика. * Независимость: BLoC не знает, кто его слушает. Он просто «вещает» состояния. Это позволяет легко тестировать логику отдельно от верстки.

    Простой пример счетчика на чистых потоках

    Чтобы закрепить материал, давайте представим, как выглядел бы простейший BLoC для счетчика без использования сторонних библиотек.

    В этом примере: * UI будет вызывать bloc.incrementEventSink.add(null), когда пользователь нажмет кнопку. * UI будет подписан на bloc.counterStream, чтобы обновлять цифру на экране.

    Преимущества и недостатки подхода

    Преимущества:

  • Разделение ответственности: UI занимается только рисованием, BLoC — только логикой.
  • Тестируемость: Легко написать Unit-тесты для BLoC, так как он не зависит от виджетов Flutter.
  • Переиспользование: Один и тот же BLoC можно использовать на разных экранах.
  • Недостатки:

  • Порог входа: Новичкам сложно сразу понять концепцию потоков и асинхронности.
  • Многословность (Boilerplate): Даже для простых задач приходится писать много кода (создавать контроллеры, закрывать их, определять события).
  • К счастью, сообщество Flutter создало отличную библиотеку flutter_bloc, которая скрывает большую часть рутинной работы с StreamController и делает код чище. Именно её мы будем изучать в следующих уроках.

    Заключение

    Сегодня мы разобрали фундамент паттерна BLoC. Мы узнали, что: * Reactive Programming — это про автоматическое распространение изменений. * Stream — это конвейер, доставляющий данные асинхронно. * Sink — это вход для данных. * BLoC принимает события через Sink и отдает состояния через Stream.

    В следующей статье мы перейдем от теории к практике и разберем, как устроена библиотека flutter_bloc, которая является стандартом индустрии для управления состоянием во Flutter.

    Готовы проверить свои знания? Попробуйте ответить на вопросы ниже.

    2. Основы библиотеки flutter_bloc: Использование Cubit для простого управления состоянием

    Основы библиотеки flutter_bloc: Использование Cubit для простого управления состоянием

    В предыдущей статье мы погрузились в мир реактивного программирования, изучив, как работают Stream и Sink в Dart. Мы создали простейший BLoC своими руками и увидели, что, несмотря на мощь этого подхода, он требует написания большого количества шаблонного кода (boilerplate). Нам приходилось вручную создавать контроллеры, следить за их закрытием и связывать потоки.

    Сегодня мы переходим на новый уровень абстракции. Мы познакомимся с библиотекой flutter_bloc — инструментом, который стал стандартом де-факто в мире Flutter. И начнем мы с самого простого и доступного компонента этой библиотеки — Cubit.

    Зачем нам flutter_bloc?

    Библиотека flutter_bloc, разработанная Феликсом Ангеловым (Felix Angelov), решает несколько критических задач, с которыми мы столкнулись при работе с «чистыми» потоками:

  • Управление ресурсами: Она автоматически закрывает потоки, когда они больше не нужны, предотвращая утечки памяти.
  • Упрощение API: Вместо сложной работы с StreamController, мы получаем понятные классы и методы.
  • Интеграция с UI: Библиотека предоставляет готовые виджеты (например, BlocBuilder), которые эффективно перерисовывают интерфейс только тогда, когда это действительно нужно.
  • Что такое Cubit?

    Cubit — это упрощенная версия паттерна BLoC. Если классический BLoC работает по схеме «Событие (Event) -> Состояние (State)», то Cubit исключает понятие событий. Вместо отправки событий в Sink, мы просто вызываем функции (методы) класса Cubit.

    Представьте, что BLoC — это сложный пульт управления атомной станцией с множеством кнопок и рычагов, а Cubit — это выключатель света. Он проще, понятнее и идеально подходит для ситуаций, где сложная логика обработки событий не требуется.

    Архитектура Cubit

    Давайте визуализируем, как работает Cubit по сравнению с тем, что мы изучили ранее.

    !Диаграмма потока данных в Cubit: UI вызывает методы класса Cubit, который в ответ отправляет новые состояния обратно в UI.

    Как видите, входной точкой теперь является не Sink, а обычный метод класса. Это делает код намного более читаемым и привычным для разработчиков, пришедших из императивного программирования.

    Подключение библиотеки

    Прежде чем начать писать код, необходимо добавить зависимость в ваш файл pubspec.yaml:

    Создание первого Cubit

    Давайте перепишем наш пример со счетчиком из прошлой статьи, используя Cubit. Теперь нам не нужно создавать StreamController вручную. Класс Cubit уже содержит в себе всю необходимую логику для управления состоянием.

    1. Определение состояния

    Для простого счетчика состоянием является просто число (int). В более сложных случаях это будет класс (например, UserProfileState).

    2. Создание класса Cubit

    Наш класс должен наследоваться от Cubit<T>, где T — это тип нашего состояния.

    Обратите внимание, насколько меньше кода стало по сравнению с версией на чистых стримах! Нет никаких контроллеров, синк-ов и подписок внутри класса.

    Магия метода emit

    Ключевым элементом здесь является защищенный метод emit(T state).

    * Только внутри: Вы можете вызывать emit только внутри класса Cubit. Снаружи (из UI) вы не можете просто сказать кубиту «стань равным 5». Вы должны вызвать метод (например, increment), который внутри себя решит, нужно ли менять состояние. * Проверка изменений: emit умный. Он проверяет, отличается ли новое состояние от текущего. Если вы вызовете emit(5), когда state уже равно 5, ничего не произойдет. UI не будет перерисован лишний раз. Это огромная оптимизация производительности «из коробки».

    Связывание Cubit с Flutter UI

    Теперь, когда у нас есть логика, нам нужно отобразить её на экране. Библиотека flutter_bloc предоставляет для этого три основных виджета, но сегодня мы сосредоточимся на двух самых важных: BlocProvider и BlocBuilder.

    BlocProvider: Внедрение зависимости

    BlocProvider — это виджет, который создает экземпляр вашего Cubit и делает его доступным для всех виджетов ниже по дереву (в child). Это реализация паттерна Dependency Injection (DI) через InheritedWidget.

    Важная особенность BlocProvider: он автоматически вызывает метод close() у вашего Cubit, когда виджет удаляется из дерева. Вам больше не нужно беспокоиться об утечках памяти!

    BlocBuilder: Реактивная перерисовка

    BlocBuilder — это виджет, который слушает изменения состояния Cubit и перестраивает свой интерфейс в ответ на них. Он требует два типа: сам Cubit и тип состояния.

    Заключение

    Сегодня мы сделали огромный шаг вперед. Мы ушли от ручного управления потоками и начали использовать профессиональные инструменты.

    Мы узнали, что: * Cubit — это упрощенный BLoC, где состояния меняются через вызов функций. * BlocProvider отвечает за создание и закрытие Cubit. * BlocBuilder отвечает за построение UI на основе текущего состояния. * Метод emit используется для обновления состояния и работает только внутри Cubit.

    Cubit идеально подходит для большинства задач управления состоянием благодаря своей простоте и лаконичности. Однако в следующей статье мы рассмотрим «старшего брата» — полноценный BLoC, чтобы понять, как обрабатывать сложные потоки событий и реализовывать продвинутую бизнес-логику.

    3. Полноценный BLoC: Работа с событиями, состояниями и асинхронными операциями

    Полноценный BLoC: Работа с событиями, состояниями и асинхронными операциями

    Добро пожаловать на третий урок курса «BLoC паттерн во Flutter: Управление состоянием». В предыдущей статье мы познакомились с Cubit — упрощенной версией BLoC, которая позволяет управлять состоянием через вызов функций. Cubit отлично справляется с простыми задачами, где действие пользователя напрямую вызывает изменение состояния.

    Однако, реальные приложения часто требуют более сложной логики. Что если нам нужно отслеживать не только результат действия, но и причину его возникновения? Что если пользователь слишком часто нажимает на кнопку «Обновить», и нам нужно игнорировать лишние нажатия? Что если одно событие должно запускать цепочку асинхронных операций?

    Здесь на сцену выходит «старший брат» — полноценный класс Bloc.

    Фундаментальное отличие: События вместо Функций

    Главное архитектурное отличие между Cubit и Bloc заключается в способе взаимодействия с UI.

    * Cubit: UI вызывает функции (методы) класса (например, cubit.increment()). * Bloc: UI отправляет события (events) в поток (например, bloc.add(IncrementEvent())).

    Это изменение кажется небольшим, но оно меняет парадигму. BLoC превращается в «машину», которая принимает поток входящих событий, обрабатывает их и выдает поток исходящих состояний.

    !UI отправляет события в BLoC, который преобразует их в состояния и возвращает обратно.

    Анатомия BLoC

    Чтобы создать BLoC, нам нужно определить три компонента: События, Состояния и сам Класс BLoC.

    1. События (Events)

    События — это входные данные. Это всё, что происходит в приложении: нажатия кнопок, ввод текста, таймеры, ответы от API. В коде события обычно представляются абстрактным классом и набором его наследников.

    Рассмотрим пример приложения погоды:

    2. Состояния (States)

    Состояния описывают, как выглядит интерфейс в данный момент времени. Здесь всё точно так же, как и в Cubit.

    3. Класс Bloc и обработчик on<Event>

    Теперь самое интересное. Мы создаем класс, наследуемый от Bloc<Event, State>. В отличие от Cubit, здесь мы не пишем публичные методы для изменения состояния. Вместо этого мы регистрируем обработчики событий в конструкторе, используя метод on<Event>.

    Асинхронные операции

    Одной из главных задач BLoC является работа с асинхронностью (запросы к серверу, работа с БД). Внутри обработчика on<Event> мы можем использовать ключевые слова async и await.

    Давайте реализуем логику загрузки погоды:

    Важные правила работы с emit:

  • Последовательность: Вы можете вызывать emit несколько раз внутри одного обработчика. Это позволяет создавать цепочки состояний: Загрузка -> Данные.
  • Завершенность: Обработчик on должен завершить свою работу. Если вы запустите бесконечный цикл внутри on без пауз, вы заблокируете обработку других событий этого типа (по умолчанию события обрабатываются последовательно).
  • Продвинутые возможности: Трансформация событий

    Вы можете спросить: «Зачем мне писать классы событий и регистрировать обработчики, если в Cubit я мог просто написать функцию getWeather(city)? Кода же меньше!»

    Сила BLoC раскрывается, когда нам нужно управлять потоком событий. Представьте поиск с автодополнением (Search Autocomplete). Пользователь быстро печатает слово «Flutter».

    * В Cubit: При каждом нажатии клавиши вы вызываете метод search(). Если пользователь печатает быстро, вы отправите 7 запросов на сервер. Это неэффективно и может привести к багам (гонка состояний). * В Bloc: Вы отправляете события SearchQueryChanged. BLoC позволяет вам использовать Event Transformers (трансформаторы событий).

    С помощью трансформаторов мы можем сказать BLoC: «Подожди, пока пользователь перестанет печатать на 300 миллисекунд, и только потом обрабатывай последнее событие. Все предыдущие — игнорируй».

    Это называется Debounce (устранение дребезга).

    Пример (с использованием пакета bloc_concurrency):

    * restartable(): Если приходит новое событие, пока старое еще обрабатывается, старое отменяется, и начинается обработка нового. Идеально для поиска. * droppable(): Если приходит новое событие во время обработки старого, новое игнорируется. Полезно для кнопок отправки формы, чтобы избежать двойной отправки.

    В Cubit реализовать такую логику отмены предыдущих запросов гораздо сложнее и требует ручного управления StreamSubscription или CancelToken.

    Сравнение Cubit и Bloc: Итоговая таблица

    | Характеристика | Cubit | Bloc | | :--- | :--- | :--- | | Входные данные | Вызов методов | Отправка событий (Events) | | Сложность кода | Низкая (меньше boilerplate) | Средняя (нужны классы событий) | | Отслеживание | Сложно понять, почему изменилось состояние | Полная история (Event -> State) | | Трансформация | Ограничена | Мощная (Debounce, Throttle, SwitchMap) | | Использование | Простые экраны, формы, настройки | Сложные фичи, поиск, корзина, чаты |

    Пример использования в UI

    Интеграция с Flutter UI практически не отличается от Cubit. Мы используем те же виджеты BlocProvider и BlocBuilder.

    Единственное отличие — способ взаимодействия:

    Метод add() — это способ положить событие в Sink нашего BLoC.

    Заключение

    Полноценный Bloc — это мощный инструмент для управления сложным состоянием. Он требует написания чуть большего количества кода, чем Cubit, но взамен дает полный контроль над потоком данных, возможность отмены устаревших операций и четкую структуру «Событие — Реакция».

    В большинстве проектов вы будете использовать комбинацию обоих подходов: Cubit для простых виджетов и Bloc для ключевых бизнес-процессов.

    В следующей статье мы разберем тему, которая часто вызывает вопросы даже у опытных разработчиков: «Архитектура приложения с BLoC: Внедрение зависимостей и связь блоков».

    4. Интеграция с UI: Виджеты BlocProvider, BlocBuilder, BlocListener и BlocConsumer

    Интеграция с UI: Виджеты BlocProvider, BlocBuilder, BlocListener и BlocConsumer

    Добро пожаловать на четвертый урок курса «BLoC паттерн во Flutter: Управление состоянием». В предыдущих статьях мы проделали большой путь: разобрались с теорией реактивного программирования, научились создавать простые Cubit и мощные Bloc, а также обрабатывать асинхронные события.

    Однако, до сих пор наша бизнес-логика жила в вакууме. Мы писали классы, тесты, но не видели результата на экране смартфона. Пришло время соединить «мозги» нашего приложения (BLoC) с его «лицом» (UI).

    Библиотека flutter_bloc предоставляет набор специализированных виджетов, которые делают эту интеграцию бесшовной, эффективной и чистой. Сегодня мы разберем «большую четверку» этих виджетов: BlocProvider, BlocBuilder, BlocListener и BlocConsumer.

    BlocProvider: Фундамент и внедрение зависимостей

    Прежде чем использовать BLoC в интерфейсе, его нужно создать и передать в дерево виджетов. Для этого используется BlocProvider.

    BlocProvider — это виджет, который внедряет экземпляр BLoC (или Cubit) в дерево виджетов, делая его доступным для всех дочерних элементов. Технически он работает на базе InheritedWidget.

    !BlocProvider внедряет зависимость в дерево виджетов, делая её доступной для потомков.

    Создание нового BLoC

    Самый частый сценарий — создание нового экземпляра BLoC. В этом случае BlocProvider берет на себя ответственность за управление жизненным циклом.

    Важный момент: Когда BlocProvider, создавший BLoC, удаляется из дерева виджетов (например, пользователь закрыл экран), он автоматически вызывает метод close() у вашего BLoC. Это предотвращает утечки памяти. Вам не нужно закрывать потоки вручную.

    Передача существующего BLoC

    Иногда вам нужно передать уже существующий BLoC на другой экран (например, при навигации или в модальное окно). В этом случае нельзя использовать create, так как это создаст новый экземпляр. Используйте конструктор value:

    При использовании BlocProvider.value провайдер не закрывает BLoC автоматически, так как он его не создавал.

    Доступ к BLoC из дерева

    Чтобы получить доступ к BLoC внутри дочернего виджета, используется расширение контекста:

    BlocBuilder: Реактивная перерисовка

    BlocBuilder — это «рабочая лошадка» библиотеки. Этот виджет слушает изменения состояний BLoC и перестраивает свой дочерний интерфейс в ответ на них.

    Он требует два типа данных: класс вашего BLoC и класс состояния.

    Оптимизация с buildWhen

    Представьте, что ваш BLoC отправляет состояния очень часто (например, прогресс загрузки: 1%, 2%, 3%...). Но вы хотите перерисовывать экран только тогда, когда загрузка завершена. Для этого существует параметр buildWhen.

    buildWhen — это функция, которая принимает предыдущее и текущее состояние и возвращает bool. Если вернуть true, перерисовка произойдет. Если falsebuilder вызван не будет.

    BlocListener: Одноразовые действия (Side Effects)

    Это одна из самых частых ошибок новичков: попытка показать SnackBar, диалоговое окно или выполнить навигацию внутри BlocBuilder.

    Запомните правило: Метод buildbuilder) должен быть «чистым». Он должен только возвращать виджеты. Он может вызываться фреймворком многократно. Если вы покажете диалог в builder, он может открыться 10 раз подряд.

    Для одноразовых действий («побочных эффектов») существует BlocListener.

    Он не рисует интерфейс (у него есть параметр child, который он просто возвращает без изменений), но он слушает поток состояний.

    Управление с listenWhen

    Аналогично buildWhen, у BlocListener есть параметр listenWhen. Он позволяет игнорировать определенные изменения состояний, чтобы не вызывать слушатель лишний раз.

    BlocConsumer: Два в одном

    Часто возникает ситуация, когда нам нужно и перерисовать экран, и выполнить действие. Например, при ошибке мы хотим показать текст ошибки на экране (Builder) И показать всплывающее уведомление (Listener).

    Вместо того чтобы вкладывать BlocListener в BlocBuilder (или наоборот), создавая «лесенку» из кода, мы используем BlocConsumer.

    BlocConsumer объединяет в себе параметры обоих виджетов:

    !BlocConsumer объединяет функциональность построения интерфейса и выполнения одноразовых действий.

    MultiBlocProvider и MultiBlocListener

    В реальных приложениях часто нужно предоставить доступ к нескольким BLoC одновременно или слушать несколько источников.

    Вместо глубокой вложенности:

    Используйте MultiBlocProvider:

    Аналогично работает MultiBlocListener для объединения нескольких слушателей.

    Резюме: Какой виджет выбрать?

    Чтобы вам было проще ориентироваться, я составил небольшую таблицу-шпаргалку:

    | Задача | Виджет | | :--- | :--- | | Создать BLoC и передать его вниз | BlocProvider | | Перерисовать часть экрана при изменении состояния | BlocBuilder | | Показать SnackBar, Dialog или сделать навигацию | BlocListener | | И то, и другое одновременно | BlocConsumer | | Избежать вложенности провайдеров | MultiBlocProvider |

    Заключение

    Теперь у вас есть полный набор инструментов для создания полноценных приложений на Flutter с использованием BLoC. Вы знаете, как создавать логику, как обрабатывать события и как правильно отображать результаты пользователю.

    В этой статье мы научились:

  • Использовать BlocProvider для внедрения зависимостей.
  • Строить реактивный UI с BlocBuilder.
  • Обрабатывать навигацию и ошибки с BlocListener.
  • Комбинировать подходы с BlocConsumer.
  • В следующей статье мы поднимемся на уровень выше и поговорим об Архитектуре приложения. Мы разберем, как правильно организовать папки, как связывать несколько BLoC между собой и как внедрять репозитории данных.

    5. Тестирование BLoC, обработка ошибок и лучшие архитектурные практики

    Тестирование BLoC, обработка ошибок и лучшие архитектурные практики

    Добро пожаловать на пятый и заключительный урок курса «BLoC паттерн во Flutter: Управление состоянием». Мы прошли долгий путь: от понимания потоков и создания первых Cubit до реализации сложных BLoC и интеграции их с пользовательским интерфейсом.

    Однако, написать код, который работает — это только половина дела. В профессиональной разработке важно написать код, который:

  • Надежен: Он не падает при ошибках сети или некорректных данных.
  • Тестируем: Мы можем автоматически проверить, что логика работает верно.
  • Масштабируем: Архитектура позволяет легко добавлять новые функции.
  • В этой статье мы сосредоточимся на «инженерной» части работы с BLoC: архитектуре слоев, написании Unit-тестов и глобальной обработке ошибок.

    Архитектура приложения: Слои ответственности

    Одна из главных ошибок новичков — писать запросы к базе данных или API прямо внутри BLoC. BLoC — это компонент бизнес-логики, он должен отвечать на вопрос «Что делать с данными?», а не «Откуда их взять?».

    Чтобы приложение было чистым, мы используем слоистую архитектуру.

    !Визуализация разделения ответственности на слои: Данные, Репозиторий и Презентация.

    1. Data Layer (Слой данных)

    Это самый низкий уровень. Здесь находятся классы, которые умеют делать «грязную» работу: отправлять HTTP-запросы, читать JSON, писать в локальную базу данных.

    * Data Provider: Делает прямой запрос (например, http.get(...)). * Data Model: Простые классы (DTO), отражающие структуру JSON.

    2. Domain Layer / Repository (Слой репозитория)

    Это «прослойка» между данными и BLoC. Репозиторий абстрагирует источник данных. BLoC не должен знать, откуда пришли данные о погоде — из интернета или из кэша.

    3. Presentation Layer (Слой представления)

    Здесь живут наши Виджеты и BLoC. BLoC общается только с Репозиторием.

    Такое разделение позволяет нам легко менять источники данных (например, сменить REST API на GraphQL), не переписывая ни строчки в BLoC или UI.

    Тестирование BLoC

    BLoC идеально подходит для Unit-тестирования, потому что он отделен от UI. Нам не нужен эмулятор или запущенное приложение, чтобы проверить логику. Мы просто подаем на вход события и проверяем выходные состояния.

    Для тестирования стандартом является библиотека bloc_test. Добавьте её в dev_dependencies:

    Анатомия теста

    Тест с bloc_test состоит из трех основных фаз:

  • build: Создание экземпляра BLoC.
  • act: Добавление события в BLoC.
  • expect: Ожидание списка состояний, которые BLoC должен выдать.
  • Давайте протестируем наш CounterCubit из второго урока:

    Тестирование с моками (Mocking)

    Когда BLoC зависит от репозитория, мы не хотим, чтобы тест делал реальные запросы в сеть. Мы используем «моки» — поддельные объекты, которые имитируют поведение реальных.

    Обработка ошибок и BlocObserver

    В больших приложениях важно знать, что происходит внутри всех BLoC одновременно. Для этого существует BlocObserver. Это глобальный наблюдатель, который позволяет отслеживать все события, изменения состояний и ошибки в одном месте.

    Это идеальное место для подключения аналитики (Firebase, Sentry) или логирования.

    Создание наблюдателя

    Подключение

    Чтобы активировать наблюдатель, нужно назначить его при запуске приложения:

    Теперь каждое действие в вашем приложении будет залогировано в консоли.

    Важность Equatable

    В предыдущих уроках мы упоминали, что BLoC не обновляет UI, если состояние не изменилось. Но как Dart понимает, что состояние изменилось?

    По умолчанию в Dart сравнение объектов происходит по ссылке.

    Если ваш BLoC выдаст emit(UserState('Ivan')), а текущее состояние уже 'Ivan', BlocBuilder все равно перерисует экран, потому что для Dart это разные объекты. Это вызывает лишние перерисовки и потерю производительности.

    Чтобы исправить это, мы используем пакет equatable.

    Лучшая практика: Всегда наследуйте свои классы Events и States от Equatable. Это спасет вас от множества трудноуловимых багов.

    Структура папок

    Как организовать файлы в проекте? Существует два основных подхода:

    1. Layer-first (По слоям)

    Мы группируем файлы по их технической роли. * /blocs (все блоки приложения) * /repositories (все репозитории) * /screens (все экраны)

    Этот подход плох для больших проектов. Чтобы изменить одну фичу, приходится прыгать по разным папкам.

    2. Feature-first (По фичам) — Рекомендуется

    Мы группируем файлы по функциональности. * /features * /login * /bloc * /view * /repo * /weather * /bloc * /view * /models

    Такая структура делает проект модульным и понятным.

    Заключение курса

    Поздравляю! Вы завершили курс по BLoC паттерну во Flutter.

    Мы изучили:

  • Реактивность: Как работают Streams и Sinks.
  • Cubit: Как управлять простым состоянием.
  • Bloc: Как обрабатывать сложные события и асинхронность.
  • UI: Как связывать логику и верстку через BlocProvider и BlocBuilder.
  • Качество: Как тестировать код, обрабатывать ошибки и строить архитектуру.
  • BLoC — это не просто библиотека, это философия разделения ответственности. Освоив её, вы сможете создавать сложные, надежные и поддерживаемые приложения, которые легко масштабировать.

    Удачи в ваших проектах и чистого кода!