Избавляемся от Android в api-модулях

Небольшой туториал на тему «Как уменьшить количество 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)

На этом всё, идея довольно простая.

Ещё немного теории

Теперь давайте посмотрим, как это будет выглядеть на схеме классов:

e2b20cb2a39f517381a0b51e9e745148.png

Всё как и писал выше, FragmentManagerWrapper — просто маркерный интерфейс, по нему мы можем получить данные из FragmentManagerWrapperImpl. Это уже класс, единственным полем которого может быть только сам FragmentManager. WrapperUtils содержит утилиты, необходимы для того, чтобы убрать некрасивые даункасты из кода. 

В нашем проекте уже были модули core_fragment и core_activity, поэтому я разнёс по этим модулям реализации. Плюс это позволяет ограничить использование врапперов только там, где это точно нужно.

И теперь рассмотри, как модули зависят друг от друга, чтобы убедиться, что зависимости на Android в api-модуле фичи не будет:

Зеленым выделены pure kotlin модули, а стрелочками, направление зависимостей

Зеленым выделены pure kotlin модули, а стрелочками, направление зависимостей

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

У способа есть несколько недостатков, специфичных для нашего проекта, которые для нас показались не критичными:

  • containerId уже не пометить аннотацией @IdRes — думаю, это не самое страшное, что могло произойти. Это в целом можно решить добавлением враппера для контейнера, что, на мой взгляд, излишне.

  • Не решает проблемы с Parcelable, которые иногда используются у нас в api-модулях. В целом и это можно решить, добавив дополнительную прослойку мапперов. Но переписывание такого модуля займёт больше времени, и пока мы их не трогаем.

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

Ну вот и всё, просто небольшой пример того, как можно сделать код api-модулей независимым от Android-фреймворка. 

В этом подходе есть и косвенные плюсы — код станет чище, и прежде чем разработчик захочет добавить что-то из Android-фреймворка в api модуль, ему нужно будет подумать, как это сделать: добавить новые врапперы или подключить зависимость на Android-плагин. А может быть, всё-таки код, который он хочет добавить, — это приватная часть модуля, и её не стоит выносить в api.

Насчёт того, на сколько этот подход нас ускорил, говорить пока рано — на момент написания статьи переписано всего 50 модулей из возможных 250+. Если у вас есть какие-то комментарии о том, насколько способ работоспособен, и/или другие дополнения/предложения, буду рад их изучить. 

© Habrahabr.ru