Как стать автором
Обновить

Компонентный подход. Реализуем экраны с помощью библиотеки Decompose

Время на прочтение8 мин
Количество просмотров11K
Всего голосов 8: ↑8 и ↓0+8
Комментарии21

Комментарии 21

Спасибо за статью! Тоже не раз убеждался, что подобные компоненты сильно упрощают код.

После прочтения осталось пара вопросов:

  1. почему в SignInComponent добавлено несколько StateFlow для полей-состояний вместо одного? Хранение состояния "по частям" может привести к проблемам с консистентностью, а единый объект-стейт будет проще логировать и упростит отладку.

  2. как лучше упростить создание дочерних компонентов с помощью DI? Сейчас в RealMainComponent дочерние компоненты создаются вручную, и при появлении зависимостей у дочерних компонентов (например, интеракторы и репозитории) их создание превратится в бойлерплейт.

Привет, Саша.

  1. Я сделал login, password и inProgress отдельными полями, потому что они могут изменяться независимо друг от друга. Если делать состояние компонента единым объектом, то это уже будет ближе к MVI, нежели к MVVM. Я не фанат MVI и редко его использую. Но, если тебе нравится MVI, то, конечно, его можно применять с компонентным подходом.

  2. В реальных приложениях я использую DI-фреймворк Koin. Для этого я создаю класс ComponentFactory — обертку поверх DI-контейнера с методами для создания компонентов. Сам ComponentFactory тоже зарегистрирован в DI, поэтому компоненты могут получить к нему доступ. Вот gist с примером.

  1. Это больше дело привычки и удобства, чем приверженность какой-то архитектуре. Разделение на отдельные поля действительно удобен, если используешь databinding. А в Compose уже не столь удобно.

  2. За это большое спасибо, стоит изучить и перейти уже на decompose, ибо с google navigation не так удобно

Расскажите, пожалуйста, с какими неудобствами вы столкнулись, используя MVVM с Jetpack Compose?

Не с MVVM, а с выделением в отдельное поле, вместо объединения в один class.
Самое простое валидация по полям loginpassword и включение кнопки "войти".
Когда они у вам в одном классе, то достаточно написать расширение для этого класса. А когда они разделены, вы либо их будете в UI проверять, что его будет нагромождать, либо будете создавать новое поле, которое будет подписываться на нужные поля.

Выносить логику в UI, конечно, не стоит.

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

Если вы используете StateFlow, то нужно написать утилитную функцию (назовем ееderived) для комбинирования flow. С ней код получится таким:

    override val login = MutableStateFlow("")

    override val password = MutableStateFlow("")

    override val signInButtonEnabled = derived(login, password) { login, password ->
        login.isNotBlank() && password.isNotBlank()
    }

Или можно для состояния компонентов использовать State из Jetpack Compose и встроенную функцию derivedStateOf. Получится так:

    override val login by mutableStateOf("")

    override val password by mutableStateOf("")

    override val signInButtonEnabled by derivedStateOf {
        login.isNotBlank() && password.isNotBlank()
    }

Это я и имел ввиду, когда писал следующее (виноват, что написал не так конкретно, как вы)

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

P.s. для комбинирования нескольких Flow используется combine

P.s.s со State из Compose ваш пример сработает лишь раз, во время создания класса. Чтобы динамично считывать значения из State не из compose-функции надо использовать snapshotFlow { }, и тогда надо использовать первый вариант с combine

combine возвращает Flow. Утилита derived будет возвращать StateFlow, чтоб было удобнее.

Про derivedStateOf вы ошибаетесь. Она неявно подписывается на поля из лямбда-функции и потом автоматически пересчитывается при изменении любого из них.

О каком удобстве вы говорите. Из Compose-функции можно одинаково успешно подписываться и на StateFlow и Flow

derivedStateOf делает это в Compose-функции. И то с помощью remember https://developer.android.com/jetpack/compose/side-effects#derivedstateof

В отличие от StateFlow у Flow нельзя получить текущее значение. Это значит, что в collectAsState мы будем обязаны передать аргумент initial - в этом неудобство.

Про derivedStateOf просто проверьте, если не верите.

Получить текущее состояние у Flow можно, надо вызвать first(). Но это suspend функция, что не всегда удобно, не могу не согласиться. Но значение по умолчанию, всегда надо передавать в StateFlow. Поэтому достаточно вызвать stateIn у любой Flow. За своей функцией это можно скрыть, но это дело привычки.
Про derivedStateOf не поверю, ибо только на днях с ним экспериментировал.

Привет!

  1. Мне тоже ближе MVVM, и использование единого стейта из MVI позволяет получить лучшее из обоих подходов. С единым стейтом включение кнопки "войти" будет всего-лишь полем в классе стейта без необходимости создавать отдельный StateFlow или использовать derivedStateOf.

Кроме того, использование единого стейта для компонента очень удобно для написания проверок в unit-тестах. Достаточно будет проверить только 1 значение вместо нескольких.

Также в  другом комменте писали про складывание кол-ва состояний. И при использовании единого класса стейта гораздо проще будет ограничить невалидные состояния (кинуть exception в конструкторе), чем при разделении стейта на части.

  1. Только учитывайте, что одно общее состояние у Вас все равно складывается, как композиция конкретных состояний дочерних элементов. Число этих состояний равно произведению чисел состояний каждого конкретного дочернего элемента. 3 дочерних компонента по 3 состояния в каждом дают конечный автомат из 27 состояний. Теория комбинаторики же. Попытка все 27 впихнуть в 1 конечный автомат сделает Вам только больнее. А вот конечный автомат, который объединяет состояния трех вложенных компонентов будет иметь гораздо меньше состояний. Шаблон "Компоновщик" тоже очень помогает понять, как правильно работать с вложенными компонентами.

Можно еще отметить, что компонент обязан удовлетворять шаблону проектирования "Composite" ("Компоновщик"). Этот шаблон накладывает определенные ограничение на то, кто и как собирает компонент целиком. Спойлер - сам компонент себя собирает. Также этот шаблон накладывает ограничения на то, как внутренности компонента видны снаружи компонента, на то, каким вообще должен быть интерфейс компонента, и на то, как взаимодействуют вложенные и родительский компоненты друг с другом. Это тоже немаловажный момент при проектировании компонентов. Спойлера тут не будет. Предлагаю всем заинтересованным самим изучить шаблон и сообразить, как его применять на практике для проектирования компонентов.

Еще для взаимодействия дочерних и родительских компонентов крайне рекомендуется использовать шаблон проектирования "Команда". Он идеально сочетается с шаблоном "Компоновщик". И совсем высший пилотаж – это добавить в архитектуру компонента шаблон "Chain of response" или "Responder chain" (как его называет Apple), который еще прекраснее ложится на предыдущие 2 шаблона и, наверняка, должен быть реализован в Android из коробки (я iOS-разработчик, поэтому пока лишь делаю такое предположение). Эти 2 шаблона помогут вам сделать вашу схему гораздо менее связной и более переиспользуемой. Это про callback'и общения дочерних компонентов с родительскими и выше до контроллера (или презентера, или вьюмодели). Callback на самом деле в вашей схеме все очень сильно портит.

Спасибо за дополнение.
Я не пробовал использовать упомянутые вами паттерны для реализации компонентного подхода. Будет интересно попробовать.

Не соглашусь с формулировкой "обязан удовлетворять шаблону". Нет, не обязан. В Decompose компонентом может являться класс, который вообще никакой интерфейс не реализует и это нормально работает. Поскольку компоненты, про которые идет речь в статье, чаще всего не хранятся в коллекциях, потому что в этом смысла никакого нету. И искать и абстрагировать какую-то функциональность для таких компонентов тяжело и может быть бессмысленно. Поэтому компоновщик не обязан быть, но может помочь в некоторых ситуациях, с учетом того, что у компонентов древовидная связь.

А о каком архитектурном паттерне идет речь? Архитектурные паттерны – это всегда? компоновка более мелких паттернов проектирования. Раньше я думал, что вью во всех (100%) архитектур всегда должен удовлетворять шаблону "Компоновщик". MVP, например, – это MVC с пассивной моделью. Так вот в ней черным по-английски написано, что view – это "компоновщик". Пруф. Смотрите сразу картинку 7.2. Если вы не используете компоновщик, то это уже у вас и не MVP, и не MVC с пассивной моделью. Значит, это что-то ваше собственное или что-то о чем я еще не знаю. Так о каком архитектурном паттерне речь? И из каких паттернов тогда состоит вью в нем? Мне правда интересно, т.к. сейчас собираю информацию об этом для доклада.

Тут видимо у нас путаница с терминологией или я чего-то не понимаю. Мы про какие компоненты говорим? В статье речи отдельно про вью или какие-либо конкретные "архитектурные паттерны" (MV*, многими называемые как паттерны презентационного слоя) не идет. В статье используется MVVM только ради примера. Речь тут идет скорее про более высокий уровень, про структуру проекта.

В данном случае компонентом может являться либо совокупностью всех сущностей любого MV* подхода (то есть вью + совокупность логики + модель = один компонент) и в статье говорится про вариант древовидной архитектуры всего проекта на основе такого компонентного подхода, вместо тотального наслоения, как это часто делают, применяя Clean Architecture. И для таких компонентов говорить про шаблон компоновщика просто бессмысленно, это совсем про разное.

Либо компонентом можно назвать отдельный класс (как это делается в Decompose), содержащий только логику представления, грубо говоря являясь аналогом презентера или вью-модели. Тут уже есть смысл говорить про применение компоновщика, поскольку такие классы-компоненты могут быть зависимы друг от друга. Вы написали, что "компоненты обязаны удовлетворять шаблону проектирования "Composite". Я подумал, что речь именно про эти классы компонентов. И в этом случае я считаю, что не обязаны эти классы соблюдать этот паттерн. Тут может хватить обычного агрегирования, поскольку мне лично сложно найти причину использования тут компоновщика. Можно, но зачем это делать обязательно?

То что в "ортодоксальных" императивных UI-фреймворках иерархия вью обычно строится на основе паттерна компоновщик - с этим никто не спорит и это никакого отношения не имеет с компонентным подходом, про который говорится в статье.

Да, я понял, где разница. Спасибо. Вот цитата:

Ранее мы обсудили, что компонентный подход — это способ организации приложения в виде иерархии компонентов: UI-элементы ➜ функциональные блоки ➜ экраны ➜ флоу ➜ приложение. 

Я говорил выше именно про блок вью. И про вот это взаимодействие: UI-элементы ➜ Составной UI-элемент. Остальные блоки (функциональные блоки, экраны, флоу, приложение) я не имел ввиду.

Теперь, что касается сравнения "Компоновщика" и "обычного" агрегирования (указал ссылки, откуда беру терминологию). Вы правы, что оба этих подхода спрячут детали реализации от внешнего мира. Однако, агрегирование подразумевает, что компонент (в терминах статьи, тут я уже и про UI-элементы, и фукнциональные блоки, и экраны, и флоу говорю) будет создан снаружи (соответственно, "Строителем"). И вот тут ключевое отличие.

Как думаете, должна ли вьюшка собирать себя сама или это должен делать кто-то снаружи? Имхо, гораздо проще получится, если она соберет себя сама. И я считаю, что во всех архитектурах, где есть вью (а есть ли архитектуры без вью?), этот паттерн на уровне вью должен присутствовать. Тем более, что Элл его рекомендует в своей документации. У нас у иосников принято свято доверять своему Богу :) Этот шаблон упростит реализацию вью. Компоновщик все же проще. Ну, просто потому, что не требует третьего класса, который собирает компонент в агрегации (Это шаблон "Строитель", насколько я понимаю). Но так глубоко я не рассматривал ни разу. Давайте вместе попробуем обсудить и разложить этот момент по полочкам. Мне будет полезно увидеть, где я ошибаюсь.

Теперь про уровни выше. Контроллеры/презентеры и вот это в терминах статьи: функциональные блоки ➜ экраны ➜ флоу ➜ приложение. Оказывается, мое утверждение справедливо и для них. Смотрите сами. По указанной выше ссылке на MVC вы можете заметить, что контроллер содержит шаблон "Стратегия", который вроде хорошо сочетается с созданием чего-то снаружи. Вроде как именно контроллер и может тогда реализовывать агрегирование (причем, больше подходит реализация композиции, обратите внимание на этот нюанс). Однако, обратите внимание, что сам шаблон "Стратегия" не подразумевает инжектирование и подмену алгоритмов внутри себя. Он этого не ограничивает. Т.е. потенциально и для контроллеров/презентеров (а значит, и для функциональные блоки ➜ экраны ➜ флоу ➜ приложение) тоже можно использовать шаблон "Компоновщик", а не композицию. Опять же из двух вариантов лучше всегда предпочитать тот, что проще. Как я уже и сказал, выше проще и понятнее по моему скромному мнению именно "Компоновщик".

Поэтому-то я и написал "обязан удовлетворять шаблону "Компоновщик". Но соглашусь, формулировку стоит поменять на "рекомендую, чтобы компонент удовлетворял шаблону "Компоновщик".

Даже, наварное, надо как-то так: 

1. Компонент уровня вью обязан удовлетворять шаблону "Компоновщик". 

2. Компонент уровня презентера и выше рекомендовано удовлетворять шаблону "Компоновщик".

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Истории