Connect with us

Разработка

Введение в Trio: часть 1

Мы представили обзор архитектуры Trio и рассмотрели основные компоненты, такие как класс Trio и класс UI.

Опубликовано

/

     
     

В Airbnb мы разработали Android-фреймворк для экранной архитектуры Jetpack Compose, который мы назвали Trio. Trio построен на нашей библиотеке с открытым исходным кодом Mavericks, которая используется для поддержания навигации и состояния приложения внутри ViewModel.

Компания Airbnb начала разработку Trio более двух лет назад и использует его в продакшене уже более полутора лет. На нем построена значительная часть работающих экранов в Android-приложении Airbnb, и он позволил нашим инженерам создавать 100% функций в Compose UI.

В этой серии статей мы рассмотрим, как Mavericks можно использовать в современных приложениях на базе Compose. Мы обсудим проблемы архитектуры на базе Compose и то, как Trio пытался их решить. В частности, мы рассмотрим такие концепции, как:

  • безопасная для типов навигация между функциональными модулями
  • хранение состояния навигации в ViewModel
  • коммуникация между экранами на основе Compose, включая открытие экранов для получения результатов и двустороннее взаимодействие между экранами
  • валидация навигационных и коммуникационных интерфейсов во время компиляции
  • инструменты разработчика, созданные для поддержки рабочих процессов Trio

Эта серия состоит из трех частей. В первой части рассматривается высокоуровневая архитектура Trio. Во второй  части будет подробно описана система навигации Trio, и в третье мы расскажем, как Trio использует Props для связи между экранами.

Общие сведения о Mavericks

Чтобы понять архитектуру Trio, важно знать основы Mavericks, на базе которого построен Trio. Изначально Airbnb выложил Mavericks в открытый доступ в 2018 году, чтобы упростить и стандартизировать управление состоянием в Jetpack ViewModel. Ознакомьтесь с этим постом, представляющим запуск Mavericks («MvRx») для более глубокого погружения.

Используемая практически во всех сотнях экранов приложения Airbnb для Android (и во многих других компаниях!), Mavericks — это библиотека управления состоянием, которая отделена от пользовательского интерфейса и может быть использована с любой системой пользовательского интерфейса. Основная концепция заключается в том, что экранный пользовательский интерфейс моделируется как функция состояния. Это гарантирует, что даже самый сложный экран может быть отрисован безопасным для потоков способом, не зависящим от порядка событий, приведших к его появлению, и легко поддающимся пониманию и тестированию.

Для достижения этой цели Mavericks применяет шаблон, согласно которому все данные, отображаемые ViewModel, должны содержаться в одном классе данных MavericksState. В простом примере со счетчиком состояние будет содержать текущий счет.

Свойства состояния могут быть обновлены в ViewModel только через вызов функции setState. Функция setState принимает лямбду “переходник” (reducer), которая, учитывая предыдущее состояние, выводит новое состояние. Мы можем использовать этот переходник для увеличения счета, просто добавив 1 к предыдущему значению.

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

MavericksViewModel отображает изменения состояния через свойство Coroutine Flow. В паре с реактивным пользовательским интерфейсом, например Compose, мы можем собирать последнее значение состояния и гарантировать, что пользовательский интерфейс будет обновляться при каждом изменении состояния.

Этот однонаправленный цикл можно представить с помощью следующей диаграммы:

Введение в Trio: часть 1

Проблемы с архитектурой на основе Фрагментов

Хотя Mavericks хорошо работает с управлением состояниями, мы все же столкнулись с некоторыми проблемами при разработке пользовательского интерфейса в Android, связанными с тем, что мы использовали архитектуру на основе Фрагментов, интегрированную с Mavericks. При таком подходе ViewModels в основном привязаны к Активити и разделяются между Фрагментами с помощью инъекций. Представления Фрагментов обновляются при изменении состояния ViewModel и обращаются к ViewModel для внесения изменений в состояние. Менеджер Фрагментов самостоятельно управляет навигацией, когда фрагменты нужно подтолкнуть или вытолкнуть.

Введение в Trio: часть 1

Из-за такой архитектуры мы столкнулись с некоторыми трудностями, которые и послужили стимулом для создания Trio.

  1. Область действия — Совместное использование ViewModel несколькими Фрагментами основано на неявной инъекции ViewModel. Таким образом, неясно, какой фрагмент отвечает за первоначальное создание модели Активити ViewModel или за предоставление ей начальных аргументов.
  2. Коммуникация — сложно обмениваться данными между Фрагментами напрямую и с обеспечением безопасности типов. Опять же, поскольку ViewModel инжектируются, трудно обеспечить их прямое взаимодействие, и мы не можем контролировать порядок их создания.
  3. Навигация — Навигация осуществляется через менеджер фрагментов и должна происходить во фрагменте. Однако изменение состояния происходит в ViewModel. Это приводит к проблемам синхронизации между состояниями ViewModel и навигации. Сложно скоординировать сценарии типа «если — то», например, вызов навигации только после обновления значения состояния во ViewModel.
  4. Тестируемость — сложно изолировать пользовательский интерфейс для тестирования, поскольку он обернут во Фрагмент. Скриншот-тесты подвержены расслоению, а для мокирования состояния ViewModel требуется много непрямых действий, поскольку ViewModel внедряются во Фрагмент с помощью делегатов свойств.
  5. Реактивность — Mavericks обеспечивает однонаправленный поток состояний в представление, что полезно для согласованности и тестирования, но система представлений не очень хорошо приспособлена для реактивного обновления изменений состояния, и может быть сложно или неэффективно обновлять представление инкрементально при каждом изменении состояния.

Хотя некоторые из этих проблем можно было бы решить с помощью более совершенной архитектуры, основанной на Фрагментах, мы обнаружили, что Фрагменты слишком ограничивают возможности Compose, и решили полностью отказаться от них.

Почему мы создали Trio

В 2021 году наша команда начала изучать возможность внедрения Jetpack Compose и полного отказа от Фрагментов. Полностью перейдя на Compose, мы смогли бы лучше подготовиться к будущему Android-разработки и избавиться от многолетнего технического долга.

Продолжение использования Mavericks было важно для нас, поскольку у нас есть большой внутренний опыт работы с ним, и мы не хотели еще больше усложнять архитектурную миграцию, меняя подход к управлению состояниями. Мы увидели возможность переосмыслить, как Mavericks может поддерживать современное Android-приложение, и решить проблемы, с которыми мы столкнулись в нашей предыдущей архитектуре.

Используя Фрагменты, мы не могли гарантировать безопасное для типов взаимодействие между экранами в рантайме. Мы хотели иметь возможность кодифицировать ожидания относительно того, как ViewModel используются и шарятся, и как выглядят интерфейсы между экранами.

Мы также не чувствовали, что наши потребности полностью удовлетворяются компонентом Jetpack Navigation, особенно учитывая нашу сильно модульную кодовую базу и большое приложение. Компонент Navigation не является безопасным для типов, требует определения графа навигации в одном месте и не позволяет нам размещать состояние в нашей ViewModel. Мы искали новую архитектуру, которая могла бы обеспечить лучшую безопасность типов и поддержку модульности.

Наконец, нам нужна была архитектура, которая улучшила бы тестируемость, например, более стабильные скриншот и UI тесты, а также более простое тестирование навигации.

Мы рассматривали библиотеки с открытым исходным кодом Workflow и RIBs, но решили не использовать их, потому что они не были Compose-first и не были совместимы с Mavericks и другими уже существующими внутренними фреймворками.

Учитывая эти требования, мы решили разработать собственное решение, которое назвали Trio.

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

Trio — это фреймворк для создания фич. Он помогает нам определять и управлять границами и состоянием в Compose UI. Trio также стандартизирует то, как состояние поднимается из Compose UI и как обрабатываются события, обеспечивая однонаправленный поток данных в Mavericks. Дизайн был вдохновлен библиотекой Workflow от Square — Trio отличается тем, что он был разработан специально для Compose и использует ViewModel Mavericks для управления состоянием и событиями.

Автономные блоки называются «Trio», по имени трех основных классов, которые они содержат. Каждый Trio имеет свою собственную ViewModel, State и UI, а также может взаимодействовать с другими Trio и быть вложенным в них. На следующей диаграмме показано, как эти компоненты работают вместе. ViewModel вносит изменения в состояние с помощью редукторов Mavericks, UI получает последнее значение состояния для рендеринга, а события направляются обратно во ViewModel для дальнейшего обновления состояния.

Введение в Trio: часть 1

Если вы уже знакомы с Mavericks, этот паттерн должен выглядеть очень похоже! Использование ViewModel и State очень похоже на то, что мы делали с Фрагментами. Новым является то, как мы встраиваем ViewModel в Compose UI и добавляем Роутинг и взаимодействие на основе Props через Trio.

Трио вложены друг в друга для формирования кастомных, гибких навигационных иерархий. «Родительские» Trio создают дочерние Trio с начальными аргументами через Router и хранят их в своем State. Затем родитель может динамически взаимодействовать со своими дочерними элементами через поток реквизитов (Props), которые предоставляют данные, зависимости и функциональные колбеки.

Фреймворк помогает нам гарантировать безопасность типов при навигации и взаимодействии между Trio, особенно при переходе через границы модулей.

Каждый Trio можно протестировать отдельно, инстанцировав его с мокированными аргументами, состоянием и реквизитами. В сочетании с рендерингом на основе состояний Compose и паттернами неизменяемого состояния Maverick это обеспечивает контролируемую и детерминированную среду тестирования.

Класс Trio

Создание новой реализации Trio требует подклассификации базового класса Trio. Класс Trio типизирован для определения Args, Props, State, ViewModel и UI; это позволяет нам гарантировать безопасную для типов навигацию и межэкранное взаимодействие.

Trio создается либо с начальным набором аргументов, либо с начальным состоянием, которые упаковываются в sealed класс под названием Initializer. В проде Initializer будет содержать только Args, переданные из другого экрана, но в разработке мы можем поместить в Initializer имитацию состояния, чтобы экран мог загружаться автономно, независимо от обычной иерархии навигации.

Затем, в теле нашего подкласса, мы определяем, как мы хотим создать State, ViewModel и UI, учитывая начальные значения Args и Props.

И Args, и Props предоставляют входные данные, с той разницей, что Args статичны, а Props динамичны. Args гарантируют стабильность статической информации, такой как идентификаторы, используемые для запуска экрана, в то время как Props позволяют нам подписываться на данные, которые могут меняться с течением времени.

Trio предоставляет инициализатор для создания нового экземпляра ViewModel, передавая необходимую информацию, такую как уникальный ID Trio, поток реквизитов и ссылку на родительскую Активити. Зависимости из графа зависимостей приложения также могут быть переданы ViewModel через его конструктор.

Наконец, класс UI оборачивает composable код, используемый для рендеринга Trio. Класс UI получает поток последних состояний от ViewModel, а также использует ссылку на ViewModel для обращений к ней при обработке событий UI.

Нам нравится, что группировка всех этих фабричных функций в классе Trio позволяет наглядно показать, как создается каждый класс, и стандартизировать, куда смотреть, чтобы понять зависимости. Однако это также может показаться избыточным. В качестве улучшения мы часто используем отражение для создания UI-класса, а для автоматизации создания ViewModel с зависимостями Dagger мы используем Assisted Inject.

В результате декларация Trio в целом выглядит следующим образом:

UI-класс

UI-класс Trio реализует единственную Composable функцию под названием Content, которая определяет пользовательский интерфейс, отображаемый Trio. Кроме того, функция Content имеет тип приемника TrioRenderScope. Это скоуп Compose-анимации, который позволяет нам настраивать анимацию Trio при его отображении.

Функция Content перекомпонуется каждый раз, когда изменяется состояние ViewModel. UI направляет все события пользовательского интерфейса, такие как тапы, обратно во ViewModel для обработки.

Такая конструкция обеспечивает однонаправленный поток данных, а тестирование пользовательского интерфейса упрощается, поскольку он отделен от логики изменения состояния и обработки событий. Кроме того, он стандартизирует способ загрузки состояния Compose для обеспечения согласованности на разных экранах, а также избавляет от необходимости настраивать доступ к потоку состояния ViewModel.

Рендеринг Trio

Получив экземпляр Trio, мы можем отрисовать его, вызвав его функцию Content, которая использует ранее упомянутые фабричные функции для создания начальных значений ViewModel, State и UI. Поток состояний собирается из ViewModel и передается в функцию Content пользовательского интерфейса. UI оборачивается в Box, чтобы соблюсти ограничения и модификатор вызывающей стороны.

Для настройки анимации входа и выхода функция Content также использует приемник TrioRenderScope; он оборачивает реализацию AnimatedVisibilityScope от Compose, которая отображает содержимое. Для координации этого используется вспомогательная функция.

На практике фактическая реализация Trio.Content довольно сложна из-за дополнительных инструментов и крайних случаев, которые мы хотим поддерживать — таких как отслеживание жизненного цикла Trio, управление сохраненным состоянием и мокирование ViewModel при показе в скриншот-тестировании или при предварительном просмотре в IDE.

Заключение

В этом вступлении мы обсудили историю Airbnb с Mavericks и Фрагментами, а также то, почему мы создали Trio для перехода к архитектуре на основе Jetpack Compose. Мы представили обзор архитектуры Trio и рассмотрели основные компоненты, такие как класс Trio и класс UI.

В следующих статьях мы продолжим эту серию, подробно рассказав о том, как работает навигация в Trio, и как Props в Trio обеспечивают динамическое взаимодействие между экранами.

Источник

Если вы нашли опечатку - выделите ее и нажмите Ctrl + Enter! Для связи с нами вы можете использовать info@apptractor.ru.
Telegram

Популярное

Сообщить об опечатке

Текст, который будет отправлен нашим редакторам: