IT-инфраструктура для бизнеса и творчества
Разработка
Friflex

Архитектурный паттерн BLoC в проекте: используем легкий Cubit

Привет! Меня зовут Юрий Петров, я Flutter-разработчик в Friflex и автор канала о мобильной разработке Мобильный разработчик. В этой статье я хотел бы обсудить библиотеку flutter_bloc и одну из ее реализаций Cubit.

Для чего нужна библиотека flutter_bloc

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

Библиотека flutter_bloc и реализует этот паттерн и является менеджером состояния приложения. С помощью этой библиотеки мы можем:

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

У библиотеки flutter_bloc есть две реализации: Bloc и Cubit.

Для того, чтобы изменить состояние в реализации Bloc, нам необходимо отправить специальный event.

Изменение состояния в Bloc

Cubit же немного отличается тем, что events отсутствует, и напрямую мы обращаемся в Сubit, который в свою очередь генерирует (emit) новое состояние.

Изменение состояния в Cubit

Многие считают, что Cubit – это урезанная версия Bloc. Но это не так. Cubit – довольно мощный и гибкий инструмент, который может решать широкий спектр задач.

Возможности Cubit: пример авторизации в приложении

И для того, чтобы продемонстрировать возможности Cubit, попробуем рассмотреть простой пример регистрации пользователя в приложении. Если копнуть глубже – обнаружим, что необходимо авторизовать пользователя после успешной регистрации, то есть изменить глобальный AppState нашего приложения.

Как это можно реализовать? Для начала нужно понять, что такое AppState. AppState – это глобальное состояние нашего приложения. В целом, состояний может быть два – авторизован пользователь в приложении или нет. От этого состояния зависит многое, например, как будут вести себя другие блоки, какие виджеты будут нарисованы на экране и так далее. На примере приложения AliExpress, видно как меняется интерфейс при изменении состояния приложения «Авторизован/Не авторизован».

Для решения задачи регистрации будем использовать три Cubit: AuthCubit, RegistrationCubit, UserCubit.

Согласно этой схеме, мы видим, что при успешной регистрации RegistrationCubit получает АuthData, в которой хранится apiToken. Эти данные мы передаем в AuthCubit, который меняет своё состояние на «Авторизован». Далее мы видим, что у нас есть UserCubit, который подписан с помощью Stream Subscription на AuthCubit. И в случае изменения состояния в AuthCubit, UserCubit обращается к репозиторию и получает данные о пользователе.

И сразу встает вопрос: как пересоздавать интерфейс при изменении состояния AuthCubit? Есть несколько решений. Первое и самое простое – в методе build() мы будем проверять с помощью условного оператора If, в каком состоянии AuthCubit. Но есть и более гибкое решение.

Для этого мы создадим обертку AuthBuilder и в зависимости от состояния AuthCubit будем создавать нужный виджет. В этом случае не нужно проверять, в каком состоянии AuthCubit, так как AuthCubitBuilder сам везде перестроит интерфейс.

В коде это реализовано так

Таким образом мы перестраиваем интерфейс.

Данные Сubit

AuthCubit отвечает за глобальное состояние нашего приложения. В данном случае его задача – изменить свое состояние, авторизован пользователь или нет. Код данного Cubit очень простой. У него есть два метода: onAuthorized() и logOut(). Метод onAuthorized() принимает AuthData (модель хранит apiToken для валидации запросов в api). В нашем случае, при регистрации мы будем получать и записывать apiToken в эту модель. Далее мы изменяем состояние нашего cubit из UnAuthorizedState в AuthorizedState. А метод logOut(), просто переводит состояние Cubit в UnAuthorizedState.

Задача RegistrationCubit – провести успешную регистрацию и вызвать у AuthCubit метод onAuthorized и передать AuthData.

И теперь самое интересное: как уведомить UserCubit, что AuthСubit изменил состояние? Для решения этой задачи мы реализуем механизм подписок с помощью StreamSubsription. В конструкторе подписываемся на изменения AuthСubit, и как только поменялось состояние, мы получаем данные о пользователе. Тут обязательно необходимо закрыть нашу подписку в методе close().

Полный код по решению этих задач можно посмотреть на GitHub

И видео моего выступления на митапе Flutter в большом проекте

{ "author_name": "Friflex", "author_type": "editor", "tags": [], "comments": 6, "likes": 23, "favorites": 19, "is_advertisement": false, "subsite_label": "dev", "id": 293853, "is_wide": true, "is_ugc": false, "date": "Thu, 16 Sep 2021 10:30:40 +0300", "is_special": false }
(function () { let cdnUrl = `https://specialsf378ef5-a.akamaihd.net/SelectelBranding/images/` let previousArticleNumber = null let currentArticleNumber = 0 let platform = 'Desktop' let articles = [ // { // name: 'camera', // url: `${cdnUrl}CameraCat`, // text: 'умную камеру для\u00A0наблюдения за\u00A0котиками', // link: '1', // num: 3 // }, { name: 'chill', url: `${cdnUrl}ChillCat`, text: 'трекер, который подскажет, когда пора отдохнуть', link: 'https://vc.ru/promo/288561-eye-tracker', num: 1 }, { name: 'cloud', url: `${cdnUrl}CloudCat`, text: 'котика: даёшь ему «пять», а\u00A0он делает бэкап в облако', link: 'https://vc.ru/dev/294799-maneki-neko', num: 2 } ] let buttonCycle = document.querySelector('.button--cycle') let buttonChoose = document.querySelector('.button--choose') let buttonMobile = document.querySelector('.button--mobile') let textField = document.querySelector('.selectel-footer-subtitle') let imageAgent = document.querySelector('.image--agent') let banner = document.querySelector('.selectel-footer') buttonCycle.addEventListener('click', cycleClick) buttonChoose.addEventListener('click', () => sendEvent(`Promo ${articles[currentArticleNumber].num} Left`, 'Click')) buttonMobile.addEventListener('click', () => sendEvent(`Promo ${articles[currentArticleNumber].num} Left`, 'Click')) let media = window.matchMedia("(max-width: 570px)") media.addEventListener('change', matchMedia) function matchMedia() { if (media.matches) { platform = 'Mobile' } else { platform = 'Desktop' } update() } matchMedia() function cycleClick(event) { sendEvent(`Promo ${articles[currentArticleNumber].num} Right`, 'Click') if (event) { event.preventDefault() event.stopPropagation() } window.open('https://vc.ru/tag/selectelDIY', '_blank') //cycle(event) } function cycle(event) { // incrementArticleNumber() textField.innerHTML = generatedText() imageAgent.src = articles[currentArticleNumber].url + platform + '.svg?3' imageAgent.setAttribute("class", "") imageAgent.classList.add('image--agent', articles[currentArticleNumber].name) banner.href = articles[currentArticleNumber].link } function update() { banner.href = articles[currentArticleNumber].link imageAgent.src = articles[currentArticleNumber].url + platform + '.svg' textField.innerHTML = generatedText() } function incrementArticleNumber() { previousArticleNumber = currentArticleNumber if (currentArticleNumber >= articles.length - 1) { currentArticleNumber = 0 } else { currentArticleNumber++ } } const sendEvent = (label, action = 'Click') => { const value = `SelectelDIY — loc: Footer — ${label} — ${action}`; if (window.dataLayer !== undefined) { window.dataLayer.push({ event: 'data_event', data_description: value, }); } }; function generatedText() { let defaultText if (platform === 'Desktop') { defaultText = `Мы тут собрали %text%. Хотите научим?` } else { defaultText = `Мы тут собрали %text%.` } return defaultText.replace('%text%', articles[currentArticleNumber].text) } function getRandom(min, max) { min = Math.ceil(min) max = Math.floor(max) return Math.floor(Math.random() * (max - min + 1)) + min } (function create() { currentArticleNumber = getRandom(0, articles.length - 1) cycle() let page = document.querySelector('.page--entry') if (page) { function insertAfter() { let parents = page.querySelectorAll('[data-id="7"]') let referenceNode = parents[0] referenceNode.parentNode.insertBefore(banner, referenceNode.nextSibling); loaded() } setTimeout(() => insertAfter(), 0) } }()) function loaded() { banner.classList.add('loaded') } loadImages([ `${cdnUrl}CameraCatDesktop.svg`, `${cdnUrl}ChillCatDesktop.svg`, `${cdnUrl}CloudCatDesktop.svg`, `${cdnUrl}CameraCatMobile.svg`, `${cdnUrl}ChillCatMobile.svg`, `${cdnUrl}CloudCatMobile.svg?3`, ]) function loadImages(urls) { return Promise.all(urls.map(function (url) { return new Promise(function (resolve) { var img = document.createElement('img'); img.onload = resolve; img.onerror = resolve; img.src = url; }); })); } }())
0
6 комментариев
Популярные
По порядку
Написать комментарий...

Смотрел видео доклад по этой теме от автора. Отличный материал! Автору большое спасибо за статью!

2

Спасибо, очень актуально

1

Давно использую BLoC и кубиты в частности. Очень удобно.

1

Честно говоря ваш подход очень похож на event, обычно emit делают на простых свойствах или моделях(ИМХО), не делая нормальные state. 
Я не поклонник cubit, на мой взгляд он не совсем универсален и дает меньшее разделение логики и отрисовки.
Свой подход и опыт я описал в https://vas3k.club/post/10567/
Вам советую обратить внимание на https://marketplace.visualstudio.com/items?itemName=gornivv.vscode-flutter-files - позволяет сократить работу с шаблонами для flutter_bloc , шаблонов под cubit из коробки нет, но есть возможность сделать свои.

1

У вас очень интересная статья👍. Но если вы пройдёте на мой гитхаб, то увидите,что я реализовал стейты разными способами. Просто в статье я не акцентировал внимание на этом. 

0

Благодарю за отзыв!

0
Читать все 6 комментариев
Крохотные кулер, буфет и кондиционер: как развивается японский рынок миниатюр «гачапон» Статьи редакции

В автоматах продают не только игрушки для детей, но и работающие мини-технику, мебель и посуду. Зачем компании сертифицируют псевдоутварь и можно ли на ней заработать — в пересказе NYT.

Мини-автомат с содовой Toys Spirits
Hashbon Rocket — децентрализованная платформа кросс-блокчейн обмена токенов ERC-20 на BEP-20

История создания платформы Hashbon Rocket от начала запуска до покорения DeFi рынка

«Что вы нам предлагаете. Это всё шарлатанство. Ничего не сработает». Как УБРиР меняет подход к технологиям изнутри
Вице-премьер Татьяна Голикова предложила ввести нерабочие дни с 30 октября по 7 ноября Статьи редакции

В некоторых регионах — с 23 октября.

Защита интеллектуальной собственности – это защита инвестиций

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

1₽ на хорошее дело: как работает Добрая подписка от Альфа-Банка
В Москве обязали компании перевести на удалёнку 30% сотрудников с 25 октября по 25 февраля Статьи редакции

И всех сотрудников старше 60 лет и страдающих хроническими заболеваниями.

До AI Journey осталось меньше месяца. Рассказываем, что будет на конференции

Совсем скоро стартует AI Journey — международная конференция по искусственному интеллекту и анализу данных. Её проводит Сбер, и в этом году она пройдёт в онлайн-формате. Зарегистрироваться можно уже сейчас, а мы пока расскажем, почему мероприятие будет интересно всем: от учёных до школьников.

«СберАвто» запустил продажу электромобилей Tesla в Москве с доставкой в день заказа Статьи редакции

Электромобили будут стоит от 3,5 млн рублей.

Как я умоляю Samsung уже два месяца взять мои 160 000 рублей

11 августа 2021 смотрю презентацию Samsung Galaxy Z Fold3 5G, после чего решаю что надо брать, ибо девайс интересный, а мне давно пора было поменять свой рабочий телефон. OMG, давно я так не ошибался…

null