[Разработка мобильных приложений, Разработка под Android] Властелин модулей. Продолжение истории
Автор
Сообщение
news_bot ®
Стаж: 6 лет 9 месяцев
Сообщений: 27286
В 2018 году на одной из конференций я представил доклад «Властелин модулей». С тех пор утекло много воды, а многомодульность в нашем проекте приняла финальные очертания. В этой статье я расскажу о допущенных ранее ошибках, как выглядит работа с модулями сейчас и как проектировать сложные решения.
Данная статья является пересказом истории, которую мы с @horseunnamed рассказали в нашем видео подкасте, если вы предпочитаете текстовый вариант, то вам сюда. Для удобства и экономии времени приведу краткое содержание статьи:
- В двух словах о том, в чем проблема многомодульности
- Косяки старой реализации
- Проблема 1. Управление жизненным циклом компонентов
- Проблема 2. Оверинжиниринг
- Проблема 3. Копипаста
- Так в чем же был корень зла?
- Новый подход с минимальным количеством boilerplate
- Встречайте — Feature Facade!
- Пример взаимодействия модуля профиля и модуля выбора фото
- Общие зависимости и модели
- Подводя итоги
- Что же в конце концов мы получили?
- Какие точки роста и что еще можно сделать?
- Ничего не забыли?
- Полезные ссылки
В двух словах о том, в чем проблема многомодульности В чем была соль старого доклада, и почему тема многомодульности до сих пор не закрыта?Существуют три иерархии:
- Иерархия фрагментов, которые видны на экране телефона;
- Иерархия зависимостей;
- Иерархия модулей, которая возникает при работе с многомодульностью.
В 2018 году мы безуспешно пробовали соединить их в одну, а потом изобрели довольно сложный подход с медиаторами и ScopeHolder-ами. Саурон пал, эльфы дружной вереницей потянулись на запад, а орки разбрелись по нашему коду.Косяки старой реализации
Для начала давайте освежим в памяти старую реализацию. В ее основе лежали три принципа:
- Деление модулей на три слоя: application, feature-модули и core-модули;
- Каждый feature-модуль не зависит от других feature-модулей и имеет свою анатомию:
- Интерфейс Deps, описывающий зависимости фичи, которые ей нужны снаружи;
- Интерфейс API, описывающий то, что фича может отдать наружу;
- Внутренняя реализация фичи;
- На уровне application-модулей есть слой медиаторов -- «волшебная сущность», которая осуществляет склейку между зависимостями одних фич и API других.
Проблема 1. Управление жизненным циклом компонентовПри реализации концепции мы выделили абстракцию Component, которая объединяла в себе Deps, API и внутреннюю реализацию фичи. Эта структура держалась в памяти за счет ComponentHolder-а, к которому и обращался медиатор. Усугубляло картину то, что Component Holder – это штука с жизненным циклом (куда же без него в Android?). При уничтожении процесса система убивала ComponentHolder, а при следующем запуске не восстанавливала его, как и всю статику, при этом любезно вернув стек фрагментов к последнему состоянию. Как итог, нам приходилось «воскрешать» все holder-ы вручную, накинув поверх них абстракцию ForceComponentInitializer.Проблема 2. ОверинжинирингДругая сложность заключалась в большом количестве вспомогательных классов и абстракций. Чем глубже в иерархии располагался фрагмент, тем больше boilerplate кода нам приходилось писать. Для того, чтобы держать инстансы DI-скоупов в памяти, мы ввели специальную абстракцию ScopeHolder. Она по своей сути дублировала механизм хранения и инициализации scope-ов зависимостей Toothpick-а. Из-за того, что ScopeHolder-ы инициализировались и чистились вручную, приходилось прокидывать параметры открытия экранов по всей цепочке в иерархии от верхнего в нижний.Проблема 3. КопипастаКак итог, в фичах часть инфраструктурного кода копировалась, вместе с бездумным копированием приходили глупые ошибки, а с ними и краши.К примеру, одной из частых и особо больных проблем было несоответствие между иерархиями фрагментов и DI-скоупов. Мы управляли закрытием / открытием ScopeHolder-ов вручную, и когда одному ScopeHolder-у соответствовали сразу же несколько фрагментов, становилось непонятно, lifecycle какого фрагмента должен определять жизненный цикл ScopeHolder-а.Тут возникает желание спустить всех собак на то, что Toothpick - это runtime DI-фреймворк, и считать его рассадником крашей! Но нет, Toothpick-специфичных крашей на проде мы не ловили. Причиной крашей была именно кривая архитектура. Был бы на его месте Dagger 2, все разваливалось бы с тем же успехом!Так в чем же был корень зла? Сложность архитектурных задач можно разложить на два компонента:
- Естественная сложность задачи, на которую никак не можем повлиять, — устройство Android Framework с его жизненным циклом, сама специфика задачи, в рамках которой нам надо научиться передавать зависимости из одних модулей в другие, избегая их прямого подключения друг к другу.
- Добавочная сложность, которую разработчики сами себе создали в процессе решения задачи, — использование инструментов, из-за которых и появились новые проблемы (медиаторы, ComponentHolder-ы, ComponentKeeper-ы и другие).
Новый подход с минимальным количеством boilerplateМы решили, что нужны дополнительные ограничения на структуру DI-скоупов, так как дополнительные ограничения упрощают контроль за системой. В результате разделили скоупы на два типа: структурные и присоединяемые.
1. «Структурные» скоупы без жизненного цикла.Это скоупы, у которых нет четкого старта и конца жизни. Они описывают постоянные связи между интерфейсами и их реализациями на межмодульном уровне.Еще одна особенность «структурных» скоупов – вся информация для их открытия известна на момент запуска приложения.Структурные скоупы делятся на два подтипа:
- Первый существует в единственном экземпляре — это AppScope. В нем происходит склейка интерфейсов зависимостей фичей с их реализациями;
- Второй тип — корневой scope фичи, который связывает API feature-модуля с его реализацией.
2. «Присоединяемые» скоупы.Это scope-ы, которые связаны с фрагментами при помощи фрагмент-плагинов - специальных делегатов жизненного цикла фрагментов, которые позволяют нам автоматически открывать и закрывать scope-ы на основании ЖЦ фрагмента. Для таких scope-ов могут понадобиться аргументы, которые будут известны только в runtime-е. Эти аргументы передаются в скоупы исключительно из Bundle-ов фрагментов. Bundle - естественный механизм Android, который будет переживать смену конфигурации, следовательно, мы сможем восстановить scope-ы с помощью нужных аргументов. Восстановление иерархии фрагментов осуществляется сверху вниз. Таким образом, можно будет восстановить всё DI-дерево с использованием исходных данных, сохранившихся в аргументах.Используя эту идею, мы получили следующие правила для иерархии scope-ов:1. Время жизни родительского скоупа включает в себя время жизни дочернего;2. Структурный скоуп может быть открыт только от структурного;3. Присоединяемый скоуп может быть открыт только от структурного или другого присоединяемого.Встречайте — Feature Facade!Мы избавились от холдеров, компонентов, медиаторов и сделали единую абстракцию на наши feature-модули, которую назвали FeatureFacade.FeatureFacade - это удобная синтаксическая обёртка над деревом scope-ов, которая не хранит никакого состояния и служит только для построения нужной части дерева DI-scope-ов. Роль хранения и поддержки scope-ов в этом случае берёт на себя сам Toothpick.FeatureFacade работает в двух направлениях: он дает доступ к внешним зависимостям фичи изнутри и открывает доступ к API фичи для внешнего взаимодействия с ней. Пример взаимодействия модуля профиля и модуля выбора фотоДавайте рассмотрим типичное взаимодействие между фичами. В приложении есть профиль пользователя и фича выбора фотографии (которая может работать не только для профиля). Обе фичи находятся в отдельных модулях, между которыми нет прямой связи. Мы хотим, чтобы при нажатии на аватарку в профиле запустился photo picker, через который пользователь сможет выбрать фотографию. После этого мы должны вернуть результат выбора на профиль.В случае приложения hh.ru, профиль – это резюме пользователя. Во-первых, профилей может быть больше одного, а во-вторых, их можно открывать одновременно на нескольких вкладках приложения. Мы хотим, чтобы при выборе фотки, она возвращалась к нужному фрагменту с нужным результатом ID профиля.Данный пример можно пощупать руками в репозитории.Для начала опишем интерфейс зависимостей, которые нужны фиче Profile. Нам требуются две вещи:
- Возможность встроить в экран профиля PhotoPicker – для этого снаружи будем запрашивать его фрагмент. Не будем ссылаться на PhotoPickerFragment, сошлемся на общий тип Fragment;
- Возможность реактивно слушать выбор фотографии на профиле и обновлять ее. Слушать мы можем только снаружи, соответственно, это тоже уходит в ProfileDeps.
interface ProfileDeps {
fun photoPickerFragment(profileId: String): Fragment
fun photoSelections(profileId: String): Observable<String>
}
Наружу модуль профиля будет предоставлять фрагмент, зависящий от ID пользователя:
@InjectConstructor
class ProfileApi {
fun profileFragment(userProfile: UserProfile): Fragment =
ProfileFragment.newInstance(userProfile)
}
ProfileFacade (реализация FeatureFacade для этого кейса) – это класс, который позволяет получить доступ к зависимостям модуля и его API. Через него мы сможем передать список модулей, которые опишут binding для реализации API-модуля:
class ProfileFacade : FeatureFacade<ProfileDeps, ProfileApi>(
depsClass = ProfileDeps::class.java,
apiClass = ProfileApi::class.java,
featureScopeName = "ProfileFeature",
featureScopeModule = {
Module().apply {
bind<ProfileApi>().singleton().releasable()
}
}
)
При запуске фрагмента ProfileFragment мы сможем получить доступ к скоупу фичи через фиче-фасад. Это происходит автоматически через фрагмент-плагин, который откроет и закроет скоуп, когда нужно.
internal class ProfileFragment : Fragment(R.layout.fragment_profile) {
private val di = DiFragmentPlugin(
fragment = this,
parentScope = { ProfileFacade().featureScope },
scopeNameSuffix = { userProfile.id },
scopeModules = { arrayOf(ProfileScreenModule(userProfile)) }
)
private val viewModel by lazy { di.get<ProfileViewModel>() }
}
@InjectConstructor
internal class ProfileViewModel(
private val initialUserProfile: UserProfile,
private val deps: ProfileDeps,
disposable: CompositeDisposable
)
В модуле Photo Picker-а мы объявим структуру PhotoSelection, которая будет реактивным стримом возвращаться наружу. API фичи будет выглядеть следующим образом:
data class PhotoSelection(
val selectionId: String,
val photo: Photo
)
@InjectConstructor
class PhotoPickerApi {
private val photoSelectionRelay = PublishRelay.create<PhotoSelection>()
fun photoPickerFragment(args: PhotoPickerArgs): Fragment = PhotoPickerFragment.newInstance(args)
fun photoSelections(): Observable<PhotoSelection> = photoSelectionRelay.hide()
internal fun postPhotoSelection(photoSelection: PhotoSelection) = photoSelectionRelay.accept(photoSelection)
}
Уже знакомым способом объявляем FeatureFacade для фичи выбора фото:
class PhotoPickerFacade : FeatureFacade<PhotoPickerDeps, PhotoPickerApi>(
depsClass = PhotoPickerDeps::class.java,
apiClass = PhotoPickerApi::class.java,
featureScopeName = "PhotoPickerFeature",
featureScopeModule = {
Module().apply {
bind<PhotoPickerApi>().singleton()
}
}
)
Теперь нам необходимо связать эти фичи воедино. Перейдем в application-модуль и реализуем интерфейс ProfileDeps. Мы можем напрямую обращаться к фасадам фич и использовать вызовы методов их API для реализации нужных зависимостей:
@InjectConstructor
internal class ProfileDepsImpl(
// для реализации зависимостей feature-модуля,
// может понадобиться API другого feature-модуля
private val photoPickerApi: PhotoPickerApi
) : ProfileDeps {
override fun photoPickerFragment(profileId: String): Fragment =
photoPickerApi.photoPickerFragment(PhotoPickerArgs((profileId)))
override fun photoSelections(profileId: String): Observable<String> =
photoPickerApi.photoSelections()
.filter { it.selectionId == profileId }
.map { it.photo.url }
}
Нам осталось в AppScope описать биндинг интерфейса ProfileDeps к ProfileDepsImpl:
private fun initTp() {
// Используем rootScope Toothpick-а в качестве AppScope
// и устанавливаем туда зависимости для feature-модулей
Toothpick.openRootScope().installModules(FeatureDepsModule())
}
/**
* Здесь происходит описание связей для склейки feature-модулей
*/
internal class FeatureDepsModule : Module() {
init {
bind<ProfileDeps>().toClass<ProfileDepsImpl>()
bind<PhotoPickerApi>().toProviderInstance { PhotoPickerFacade().api }
}
}
Общие зависимости и моделиНа этом месте у Вас, как внимательного читателя, возникают два закономерных вопроса:
- все ли зависимости передаются вот таким способом?
- все ли модели конвертируются в реализации Deps?
Нет и нет. Некоторые зависимости (типа аналитики или абстракций для работы с сетью) протягиваются неявно через дерево Toothpick. Что касается моделей — некоторые из них достаточно развесистые, и конвертация их при передаче между модулями превратилась бы в переливание воды из пустого в порожнее! Если мы захотим пошарить модель PhotoInfo, которая содержится внутри Photo Picker-а, между двумя фичами, мы переложим ее в core-модуль, который подключим к обоим фичам. Таким образом мы дадим им знание об этом классе, которое фичи смогут использовать на уровне своих контрактов.Таким образом, мы ввели core-модели приложения, которые описывают доменную область: резюме, вакансии, отклики и т.п. И есть временные модели, которые могут дублироваться в разных модулях. Application-модуль знает про эти модельки и может сконвертировать одну в другую.Подводя итогиЧто же в конце концов мы получили?
- Мы сместили иерархию скоупов на уровень фич;
- Все абстракции свели до одного FeatureFacade;
- Отдельные сервис-локаторы для работы со скоупом мы заменили на Toothpick;
- Сделали шаблонную генерацию заглушки feature-модуля: для Deps, API и коротенькую реализацию Feature Facade.
Какие точки роста, что еще можно сделать?
- Научиться хорошо делать Sample Apps, которые позволяли бы при разработке иметь дело только с частью кодовой базы;
- Попробовать вытащить инициализацию нашего AppScope-а и научить разворачиваться нужным способом в рамках других application;
- Написать плагин, который будет сразу генерить Sample Apps для нужной фичи и автоматически ее подключать;
- Попробовать отказаться от FeatureFacade в текущем виде. Возможно, сделаем две функции, чтобы не наследоваться каждый раз от базового класса.
Отмечу, что вышеперечисленные доработки уже косметические и, возможно, никогда в работу не пойдут.Ничего не забыли? — А что, если использовать другой DI-фреймворк?Внутри feature-модулей наша идея связки фрагментов с DI-скоупами легко реализовалась бы и с Dagger 2.На уровне межмодульного взаимодействия можно сделать то же самое, но тогда механизм управления жизненным циклом инстансов dagger-компонентов нужно реализовать самостоятельно. Toothpick же нам такое предоставляет в виде глобального синглтона. Подробнее о варианте реализации можно посмотреть в докладе Миши.— Тема модулей закрыта?Нет, мы еще расскажем, как разделяли модули и раскладывали их по репозиторию. Недостаточно придумать способ соединения модулей друг с другом, нужно и придумать, как следить за всей структурой модулей, чтобы не нарушалась корректность связей— Сколько модулей сейчас?260 штук.Полезные ссылки
- Доклад на Mobius, с которого все началось — если хотите посмотреть на то, как все было в 2018
- Видео версия этого доклада — если предпочитаете все же смотреть видео
- Наш telegram канал — в нем новости о наших статьях, видео и конференциях
- Чат с разработчиками — можно задать вопрос про модули и прочее нашим техническим специалистам
- Сэмпл проект с примерами кода — если хотите пощупать идею в IDE
- YouTube канал hh_tech — тут все о хэхэ и вообще охэхэнно
===========
Источник:
habr.com
===========
Похожие новости:
- [Разработка мобильных приложений, Конференции] За что мы (не) любим нативную мобильую разработку в 2021: обсуждаем с 2ГИС, Самокатом, Podlodka и CocoaHeads в четверг
- [Высокая производительность, Визуализация данных, Хранение данных, Облачные сервисы] ZEN’изация по полной, выбираем правильную память для EPYC процессоров
- [Разработка мобильных приложений, Интерфейсы, Машинное обучение, Смартфоны] Как мы ускоряли ввод текста на смартфоне: динамическая сетка в Яндекс.Клавиатуре
- [Платежные системы, Разработка под Android, Администрирование баз данных, Финансы в IT] Запускаем softPOS. Почему пилоты бывают полезны не только бизнесу, но и разработчикам
- [Разработка мобильных приложений, Монетизация мобильных приложений, Законодательство в IT] Россиянам предложат получать госуслуги через сторонние приложения
- [Разработка под iOS, Разработка мобильных приложений, Разработка под Android, Swift] Онлайн-митап DevDay Mobile: C++ -> Swift, скрытый API Android и будни разработчика
- [Разработка мобильных приложений, Здоровье, IT-компании] Google завела на Android прививочные карточки с QR-кодами
- [Анализ и проектирование систем] Как описать архитектуру продукта по нотации C4 (перевод)
- [Разработка мобильных приложений, Разработка под Android] Фантастические RecyclerView.ViewHolder и где они создаются
- [IT-инфраструктура, ERP-системы, Хранение данных, Управление разработкой] SOA для проектного управления. С оркестром
Теги для поиска: #_razrabotka_mobilnyh_prilozhenij (Разработка мобильных приложений), #_razrabotka_pod_android (Разработка под Android), #_moduli (модули), #_mnogomodulnost (многомодульность), #_mnogomodulnost_v_android (многомодульность в Android), #_vlastelin_modulej (властелин модулей), #_arhitektura (архитектура), #_ohehennye_istorii (охэхэнные истории), #_blog_kompanii_headhunter (
Блог компании HeadHunter
), #_razrabotka_mobilnyh_prilozhenij (
Разработка мобильных приложений
), #_razrabotka_pod_android (
Разработка под Android
)
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 06:51
Часовой пояс: UTC + 5
Автор | Сообщение |
---|---|
news_bot ®
Стаж: 6 лет 9 месяцев |
|
В 2018 году на одной из конференций я представил доклад «Властелин модулей». С тех пор утекло много воды, а многомодульность в нашем проекте приняла финальные очертания. В этой статье я расскажу о допущенных ранее ошибках, как выглядит работа с модулями сейчас и как проектировать сложные решения. Данная статья является пересказом истории, которую мы с @horseunnamed рассказали в нашем видео подкасте, если вы предпочитаете текстовый вариант, то вам сюда. Для удобства и экономии времени приведу краткое содержание статьи:
Для начала давайте освежим в памяти старую реализацию. В ее основе лежали три принципа:
1. «Структурные» скоупы без жизненного цикла.Это скоупы, у которых нет четкого старта и конца жизни. Они описывают постоянные связи между интерфейсами и их реализациями на межмодульном уровне.Еще одна особенность «структурных» скоупов – вся информация для их открытия известна на момент запуска приложения.Структурные скоупы делятся на два подтипа:
interface ProfileDeps {
fun photoPickerFragment(profileId: String): Fragment fun photoSelections(profileId: String): Observable<String> } @InjectConstructor
class ProfileApi { fun profileFragment(userProfile: UserProfile): Fragment = ProfileFragment.newInstance(userProfile) } class ProfileFacade : FeatureFacade<ProfileDeps, ProfileApi>(
depsClass = ProfileDeps::class.java, apiClass = ProfileApi::class.java, featureScopeName = "ProfileFeature", featureScopeModule = { Module().apply { bind<ProfileApi>().singleton().releasable() } } ) internal class ProfileFragment : Fragment(R.layout.fragment_profile) {
private val di = DiFragmentPlugin( fragment = this, parentScope = { ProfileFacade().featureScope }, scopeNameSuffix = { userProfile.id }, scopeModules = { arrayOf(ProfileScreenModule(userProfile)) } ) private val viewModel by lazy { di.get<ProfileViewModel>() } } @InjectConstructor internal class ProfileViewModel( private val initialUserProfile: UserProfile, private val deps: ProfileDeps, disposable: CompositeDisposable ) data class PhotoSelection(
val selectionId: String, val photo: Photo ) @InjectConstructor class PhotoPickerApi { private val photoSelectionRelay = PublishRelay.create<PhotoSelection>() fun photoPickerFragment(args: PhotoPickerArgs): Fragment = PhotoPickerFragment.newInstance(args) fun photoSelections(): Observable<PhotoSelection> = photoSelectionRelay.hide() internal fun postPhotoSelection(photoSelection: PhotoSelection) = photoSelectionRelay.accept(photoSelection) } class PhotoPickerFacade : FeatureFacade<PhotoPickerDeps, PhotoPickerApi>(
depsClass = PhotoPickerDeps::class.java, apiClass = PhotoPickerApi::class.java, featureScopeName = "PhotoPickerFeature", featureScopeModule = { Module().apply { bind<PhotoPickerApi>().singleton() } } ) @InjectConstructor
internal class ProfileDepsImpl( // для реализации зависимостей feature-модуля, // может понадобиться API другого feature-модуля private val photoPickerApi: PhotoPickerApi ) : ProfileDeps { override fun photoPickerFragment(profileId: String): Fragment = photoPickerApi.photoPickerFragment(PhotoPickerArgs((profileId))) override fun photoSelections(profileId: String): Observable<String> = photoPickerApi.photoSelections() .filter { it.selectionId == profileId } .map { it.photo.url } } private fun initTp() {
// Используем rootScope Toothpick-а в качестве AppScope // и устанавливаем туда зависимости для feature-модулей Toothpick.openRootScope().installModules(FeatureDepsModule()) } /** * Здесь происходит описание связей для склейки feature-модулей */ internal class FeatureDepsModule : Module() { init { bind<ProfileDeps>().toClass<ProfileDepsImpl>() bind<PhotoPickerApi>().toProviderInstance { PhotoPickerFacade().api } } }
=========== Источник: habr.com =========== Похожие новости:
Блог компании HeadHunter ), #_razrabotka_mobilnyh_prilozhenij ( Разработка мобильных приложений ), #_razrabotka_pod_android ( Разработка под Android ) |
|
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 06:51
Часовой пояс: UTC + 5