Небольшой туториал на тему «Как уменьшить количество Android-модулей в проекте при помощи оберток над Android-классами»
На решение, которое будет описано ниже, меня натолкнула статья Оптимизация Gradle: избавляемся от Android-модулей. В ней приведен синтетический бенчмарк, из которого видно, что количество Gradle-модулей с Android-плагином негативно влияет на скорость конфигурации проекта, а также на количество необходимой памяти. Мы, как инженеры, должны воспользоваться каждой возможностью ускорить сборку проекта. Однако в статье приведено несколько недостатков описанного подхода, и на момент написания статьи они были критичными и не давали возможности использовать его в нашем проекте.
Итак, теперь сначала..
Немного о проекте Альфа-Бизнес Мобайл
Привет, Хабр! Меня зовут Алексей, главный разработчик проекта Альфа-Бизнес и лид архитектурной компетенции проекта.
Сейчас у нас около 500 модулей и, так как 60 разработчиков постоянно дописывают новый функционал, это количество постоянно растёт.
Многомодульность в нашем проекте имеет классический вид, который я уже описал в статье про разделение презентационного слоя по модулям.
Чтобы не переходить туда-сюда по статьям, оставлю здесь цитату:
«У нас есть главный модуль app — это модуль-медиатор, который знает про все другие модули проекта. Его главная задача — собрать весь граф зависимостей и предоставить их в другие модули проекта.
Также у нас есть базовые модули, в которых находятся общие утилиты, и базовые классы, которые могут понадобиться в любом модуле проекта. Например, это может быть код, который необходим для работы с сетью, или базовые классы для MVI.
Для фичей мы заводим два модуля, с приставками -api и -impl с интерфейсом и реализацией фичи»
Базовых модулей в проекте около 20, все остальные — api и impl модули. C impl-модулями всё ясно, это классическое разбиение по слоям domain, presentation, data. В presentation много работы с Android-классами, и в целом с impl модулями довольно сложно что-то сделать.
Меня заинтересовали api-модули
В случае проекта АБМ в них содержится в основном логика запуска другой фичи через mediator или получения данных из SharedRepository
. SharedRepository
работает только с доменными моделями, и там Android быть не может, а вот mediator чаще всего выглядит так:
interface ChangeEmailMediator {
fun startChangeEmailScreen(input: ChangeEmailInput, containerId: Int, fragmentManager: FragmentManager)
}
Это интерфейс, в который мы передаем fragmentManager
и, по сути, это единственная точка, где нам нужен Android в api-модулях.
То есть, если мы придумаем как избавиться от прямого использования FragmentManager
, мы сможем сделать около 250 модулей pure Kotlin.
И как это сделать?
Первое, что приходит в голову, завернуть Android-класс в какую-то обёртку по типу
class FragmentManagerWrapper(
val fragmentManager: FragmentManager,
)
Но в этом случае api-модулю всё равно нужно знать про класс Android...
Тогда идея такая: скрыть враппер за маркерным интерфейсом, который будет видет api-модуль:
interface FragmentManagerWrapper
А враппер будет его реализовывать:
class FragmentManagerWrapperImpl(
val fragmentManager: FragmentManager,
) : FragmentManagerWrapper
Также добавим удобные утилиты для «запаковки» и «распаковки» FragmentManager
:
val FragmentManagerWrapper.unwrap: FragmentManager
get() = (this as FragmentManagerWrapperImpl).fragmentManager
val FragmentManager.wrap: FragmentManagerWrapper
get() = FragmentManagerWrapperImpl(this)
На этом всё, идея довольно простая.
Ещё немного теории
Теперь давайте посмотрим, как это будет выглядеть на схеме классов:
Всё как и писал выше, FragmentManagerWrapper
— просто маркерный интерфейс, по нему мы можем получить данные из FragmentManagerWrapperImpl
. Это уже класс, единственным полем которого может быть только сам FragmentManager
. WrapperUtils
содержит утилиты, необходимы для того, чтобы убрать некрасивые даункасты из кода.
В нашем проекте уже были модули core_fragment
и core_activity
, поэтому я разнёс по этим модулям реализации. Плюс это позволяет ограничить использование врапперов только там, где это точно нужно.
И теперь рассмотри, как модули зависят друг от друга, чтобы убедиться, что зависимости на Android в api-модуле фичи не будет:
Для полноты картины хотелось бы привести какие-то цифры, но на проекте подход только недавно внедрили, новые модули все разрабатываются при помощи врапперов, а старые постепенно переписываются в рамках технического времени. Из-за простоты подхода это занимает немного времени.
У способа есть несколько недостатков, специфичных для нашего проекта, которые для нас показались не критичными:
containerId
уже не пометить аннотацией@IdRes
— думаю, это не самое страшное, что могло произойти. Это в целом можно решить добавлением враппера для контейнера, что, на мой взгляд, излишне.Не решает проблемы с
Parcelable
, которые иногда используются у нас в api-модулях. В целом и это можно решить, добавив дополнительную прослойку мапперов. Но переписывание такого модуля займёт больше времени, и пока мы их не трогаем.Необходимо контролировать количество врапперов и запрещать делать какие-либо изменения в них. Плюс, чтобы не нарваться на
ClassCastException
, нужно следить, чтобы не создавалось больше одной реализации врапперов. В целом это легко контролировать на код ревью. Но можно и добавить проверки линтера для этого.
Ну вот и всё, просто небольшой пример того, как можно сделать код api-модулей независимым от Android-фреймворка.
В этом подходе есть и косвенные плюсы — код станет чище, и прежде чем разработчик захочет добавить что-то из Android-фреймворка в api модуль, ему нужно будет подумать, как это сделать: добавить новые врапперы или подключить зависимость на Android-плагин. А может быть, всё-таки код, который он хочет добавить, – это приватная часть модуля, и её не стоит выносить в api.
Насчёт того, на сколько этот подход нас ускорил, говорить пока рано — на момент написания статьи переписано всего 50 модулей из возможных 250+. Если у вас есть какие-то комментарии о том, насколько способ работоспособен, и/или другие дополнения/предложения, буду рад их изучить.
Комментарии (7)
Trig3G
20.11.2024 07:37Может не совсем по теме, но как на счёт того, чтобы избавиться от какой-то особенной роли App, а добавить его в проект также, как добавляются другие feature? Возможно так сделать? Просто если в проекте мелькают классы с core в названии, это уже говорит о проблеме.
AlexeyMinay Автор
20.11.2024 07:37А что вы имеете в виду под особенной ролью? Медиатором, может быть любой модуль, просто так повелось, что через app это удобнее сделать. И про core до конца не понял.. о какой пробеме вы говорите?
Trig3G
20.11.2024 07:37О проблеме, когда всё по сути завязано на него, и его "кривое" изменение рушит всё остальное. Короче говоря одна большая точка отказа. Все приложение вроде бы слабо связанное, но имеет в себе одну общую часть на чем все держится.
Evgenij_Popovich
20.11.2024 07:37Есть ещё вариант через стаб реализацию, как это делается в библиотеке cicerone https://github.com/terrakok/Cicerone
TheBasta
20.11.2024 07:37Теперь бы ещё взять этот общий модуль, добавить модуль со списком и балансом карт, модуль платежей по СБП и собрать в отдельное Lite-приложение без всего остального. Будет идеально для повседневного применения.
namikiri
Когда-нибудь мы будем вновь читать о том, насколько ускоряется софт, а не его разработка.
AlexeyMinay Автор
Обе темы очень важны. И ускорение разработки особенно заметно, когда в проекте большое количество разработчиков. Например, пока я писал статью, нас уже стало 66, а не 60 и это количество постоянно растет. И у всех разная техника, и кто-то очень страдает от долгих сборок. Но и по ускорению софта, я надеюсь, тоже в ближайшее время смогу написать.