Разделяне на задачите в приложенията Flutter
Engineering at ClickUp

Разделяне на задачите в приложенията Flutter

Наскоро трябваше да внедря наставления за новодошлите в ClickUp! Това беше наистина важна задача, защото много нови потребители щяха да открият платформата благодарение на невероятно забавната реклама, която премиерахме по време на Супербоул! ✨

чрез ClickUp

Ръководството позволява на нашите многобройни нови потребители, които може би все още не познават ClickUp, бързо да разберат как да използват няколко функционалности от приложението. Това е непрекъснато усилие, точно като новия ресурс ClickUp University, който разработваме! 🚀

За щастие, софтуерната архитектура зад мобилното приложение ClickUp Flutter ми позволи да имплементирам тази функционалност доста бързо, дори чрез повторно използване на реалните джаджи от приложението! Това означава, че разходката е динамична, отзивчива и точно съответства на реалните екрани на приложението – и ще продължи да бъде така, дори когато джаджите се развиват.

Успях да имплементирам функционалността и благодарение на правилното разделение на задачите.

Нека да видим какво имам предвид. 🤔

Разделяне на задачите

Проектирането на софтуерна архитектура е една от най-сложните теми за инженерните екипи. Сред всички отговорности, винаги е трудно да се предвидят бъдещите еволюции на софтуера. Ето защо създаването на добре структурирана и разделна архитектура може да ви помогне на вас и вашите колеги в много неща!

Основното предимство на създаването на малки, независими системи е безспорно възможността за тестване! Именно това ми помогна да създам демо версия на съществуващите екрани от приложението!

Ръководство стъпка по стъпка

Сега, как можем да приложим тези принципи към приложението Flutter?

Ще споделим няколко техники, които използваме за създаването на ClickUp, с прост пример.

Примерът е толкова прост, че може би не разкрива всички предимства, които стоят зад него, но повярвайте ми, той ще ви помогне да създадете много по-лесни за поддръжка Flutter приложения със сложни кодови бази. 💡

Приложението

Като пример ще създадем приложение, което показва населението на САЩ за всяка година.

чрез ClickUp

Тук имаме два екрана:

  • HomeScreen: просто изброява всички години от 2000 до сега. Когато потребителите натиснат върху плочката с дадена година, те ще преминат към DetailScreen с аргумент за навигация, зададен за избраната година.
  • DetailScreen : получава годината от аргумента за навигация, извиква API-то на datausa.io за тази година и анализира JSON данните, за да извлече свързаната стойност на населението. Ако данните са налични, се показва етикет с населението.

Ще се фокусираме върху имплементацията на DetailScreen, тъй като тя е най-интересна с асинхронното си извикване.

Стъпка 1. Наивен подход

Наивен подход Stateful Widget
чрез ClickUp

Най-очевидната реализация за нашето приложение е чрез използване на един StatefulWidget за цялата логика.

Достъп до аргумента за навигация по години

За да получим достъп до исканата година, четем RouteSettings от наследения уиджет ModalRoute.

HTTP повикване

Този пример извиква функцията get от пакета http, за да получи данните от API-то на datausa.io, анализира получения JSON с метода jsonDecode от библиотеката dart:convert и запазва Future като част от състоянието с свойство, наречено _future.

Рендеринг

За да създадем дървото с джаджи, използваме FutureBuilder, който се преизгражда според текущото състояние на нашето _future асинхронно повикване.

Преглед

Добре, имплементацията е кратка и използва само вградени джаджи, но сега помислете за първоначалното ни намерение: създаване на алтернативи за демонстрация (или тестове) за този екран. Много е трудно да се контролира резултатът от HTTP повикването, за да се принуди приложението да се визуализира в определено състояние.

Тук ни помага концепцията за инверсия на контрола. 🧐

Стъпка 2. Инверсия на контрола

Инверсия на контрола към доставчик и API клиент
чрез ClickUp

Този принцип може да е труден за разбиране от новите разработчици (а също и труден за обяснение), но общата идея е да извлечем проблемите извън нашите компоненти, така че те да не са отговорни за избора на поведението, а вместо това да го делегираме.

В по-често срещана ситуация това просто се състои в създаване на абстракции и внедряване на имплементации в нашите компоненти, така че имплементацията им да може да бъде променена по-късно, ако е необходимо.

Но не се притеснявайте, след следващия ни пример всичко ще ви стане по-ясно! 👀

Създаване на API клиентски обект

За да контролираме HTTP повикването към нашия API, ние изолирахме нашата имплементация в специален клас DataUsaApiClient. Също така създадохме клас Measure, за да улесним манипулирането и поддържането на данните.

Предоставяне на API клиент

За нашия пример използваме добре познатия пакет provider, за да инжектираме инстанция DataUsaApiClient в корена на нашето дърво.

Използване на API клиента

Доставчикът позволява на всеки потомък на джаджа (като нашата DetailScreen) да чете най-близкия DataUsaApiClient в дървото. След това можем да използваме неговия метод getMeasure, за да стартираме Future, вместо действителната HTTP имплементация.

Демо API клиент

Сега можем да се възползваме от това!

В случай, че не знаете: всички класове в dart също така имплицитно дефинират асоцииран интерфейс. Това ни позволява да предоставим алтернативна имплементация на DataUsaApiClient, която винаги връща същия инстанс от своите getMeasure метод повиквания.

Този метод

Този метод

Показване на демо страница

Сега разполагаме с всички ключове, за да покажем демо версия на DetailPage!

Просто презаписваме текущо предоставения DataUsaApiClient инстанция, като обвиваме DetailScreen в доставчик, който създава DemoDataUsaApiClient инстанция!

И това е всичко – нашият DetailScreen чете този демо пример и използва нашите demoMeasure данни вместо HTTP повикване.

Преглед

Това е чудесен пример за инверсия на контрола. Нашият DetailScreen widget вече не отговаря за логиката на получаване на данните, а вместо това я делегира на специален клиентски обект. И сега можем да създаваме демо версии на екрана или да имплементираме widget тестове за нашия екран! Страхотно! 👏

Но можем да направим още по-добре!

Тъй като не можем да симулираме състояние на зареждане, например, нямаме пълен контрол върху промените в състоянието на ниво джаджа.

Стъпка 3. Управление на състоянието

Управление на изявления в Flutter приложения
чрез ClickUp

Това е гореща тема в Flutter!

Сигурен съм, че вече сте чели дълги коментари на хора, които се опитват да изберат най-доброто решение за управление на състоянието за Flutter. И за да бъдем ясни, това не е това, което ще направим в тази статия. Според нас, стига да разделите бизнес логиката от визуалната логика, всичко е наред! Създаването на тези слоеве е много важно за поддръжката. Нашият пример е прост, но в реалните приложения логиката може бързо да стане сложна и такова разделение улеснява значително намирането на чистите логически алгоритми. Тази тема често се обобщава като управление на състоянието.

В този пример използваме основен ValueNotifier заедно с Provider. Но бихме могли да използваме и flutter_bloc или riverpod (или друго решение), и то също би работило чудесно. Принципите остават същите и стига да сте разделили състоянията и логиката си, е възможно дори да прехвърлите кодовата си база от едно от другите решения.

Това разделение ни помага да контролираме всяко състояние на нашите джаджи, за да можем да го симулираме по всеки възможен начин!

Създаване на специално състояние

Вместо да разчитаме на AsyncSnapshot от рамката, сега представяме състоянието на екрана като обект DetailState.

Също така е важно да се имплементират методите hashCode и operator ==, за да може нашият обект да бъде сравним по стойност. Това ни позволява да открием дали две инстанции трябва да се считат за различни.

💡 Пакетите equatable или freezed са чудесни опции, които ви помагат да имплементирате методите hashCode и operator ==!

💡 Пакетите equatable или freezed са чудесни опции, които ви помагат да имплементирате методите hashCode и operator ==!

Нашето състояние винаги е свързано с дадена година, но имаме и четири различни възможни състояния по отношение на това, което искаме да покажем на потребителя:

  • NotLoadedDetailState: актуализацията на данните все още не е започнала
  • LoadingDetailState: данните се зареждат в момента
  • LoadedDetailState: данните са успешно заредени с асоциирана мярка
  • NoDataDetailState: данните са заредени, но няма налични данни
  • UnknownErrorDetailState: операцията се провали поради неизвестна грешка

Тези състояния са по-ясни от AsyncSnapshot, тъй като наистина представят нашите случаи на употреба. И отново, това прави нашия код по-лесен за поддръжка!

💡 Препоръчваме ви да използвате типовете Union от пакета freezed, за да представите вашите логически състояния! Те добавя много полезни функции като методите copyWith или map.

💡 Препоръчваме ви да използвате типовете Union от пакета freezed, за да представите вашите логически състояния! Те добавя много полезни функции като методите copyWith или map.

Поставяне на логиката в Notifier

Сега, когато имаме представяне на състоянието ни, трябва да съхраним негов екземпляр някъде – това е целта на DetailNotifier. Той ще запази текущия екземпляр на DetailState в неговата стойностна собственост и ще предостави методи за актуализиране на състоянието.

Предоставяме начално състояние NotLoadedDetailState и метод за обновяване, за да се заредят данни от API и да се актуализира текущата стойност.

Определете състоянието на изгледа

За да инстанциираме и наблюдаваме състоянието на екрана ни, разчитаме и на доставчика и неговия ChangeNotifierProvider. Този вид доставчик автоматично търси всеки създаден ChangeListener и ще задейства преизграждане от потребителя всеки път, когато бъде уведомен за промяна (когато стойността на уведомлението ни е различна от предишната).

Преглед

Страхотно! Архитектурата на приложението ни започва да изглежда доста добре. Всичко е разделено на добре дефинирани слоеве, с конкретни задачи! 🤗

Все още липсва едно нещо за тестване обаче – искаме да дефинираме текущото DetailState, за да контролираме състоянието на свързания с него DetailScreen.

Стъпка 4. Визуален специален уиджет за оформление

Визуални специални джаджи за оформление в приложенията Flutter
чрез ClickUp

В последната стъпка дадохме малко прекалено много отговорности на нашия DetailScreen widget: той беше отговорен за инстанциирането на DetailNotifier. И както видяхме по-рано, ние се опитваме да избягваме всякаква логическа отговорност в слоя на изгледа!

Можем лесно да решим този проблем, като създадем още един слой за нашия екранен джаджа: ще разделим нашия DetailScreen джаджа на две части:

  • DetailScreen отговаря за настройката на различните зависимости на екрана ни от текущото състояние на приложението (навигация, известия, състояние, услуги и др.).
  • DetailLayout просто преобразува DetailState в специално дърво от джаджи.

Чрез комбинирането на двете ще можем просто да създадем демо/тестови инстанции на DetailLayout, но ще имаме DetailScreen за реалния случай на употреба в нашето приложение.

Специализирано оформление

За да постигнем по-добро разделение на задачите, преместихме всичко под джаджата Consumer в специална джаджа DetailLayout. Тази нова джаджа само консумира данни и не отговаря за никакви инстанции. Тя просто преобразува състоянието на четене в специфично дърво от джаджи.

ModalRoute. на извикването и инстанцията ChangeNotifierProvider остават в DetailScreen, а този джаджа просто връща DetailLayout с предварително конфигурирано дърво на зависимости!

Това незначително подобрение е специфично за използването на доставчици, но ще забележите, че сме добавили и ProxyProvider, така че всеки потомък на джаджата може директно да използва DetailState. Това ще улесни симулирането на данни.

Извличане на джаджи като специални класове

Не се колебайте да извлечете дърво с джаджи в специален клас! Това ще подобри производителността и ще направи кода по-лесен за поддръжка.

В нашия пример създадохме един визуален джаджа за оформление за всеки от свързаните типове състояния:

Демонстрационни примери

Сега имаме пълен контрол над това, което можем да симулираме и показваме на екрана си!

Трябва само да обгърнем DetailLayout с Provider, за да симулираме състоянието на оформлението.

Заключение

Създаването на поддържаема софтуерна архитектура определено не е лесно! Предвиждането на бъдещи сценарии може да изисква много усилия, но се надявам, че няколкото съвета, които споделих, ще ви помогнат в бъдеще!

Примерите може да изглеждат прости – може дори да ви се стори, че прекаляваме с инженерството – но с нарастването на сложността на вашето приложение, тези стандарти ще ви бъдат от голяма полза! 💪

Забавлявайте се с Flutter и следете блога, за да получавате още технически статии като тази! Останете на линия!

ClickUp Logo

Едно приложение, което заменя всички останали