Небольшой туториал на тему «Как уменьшить количество Android-модулей в проекте при помощи оберток над Android-классами»

На решение, которое будет описано ниже, меня натолкнула статья Оптимизация Gradle: избавляемся от Android-модулей. В ней приведен синтетический бенчмарк, из которого видно, что количество Gradle-модулей с Android-плагином негативно влияет на скорость конфигурации проекта, а также на количество необходимой памяти. Мы, как инженеры, должны воспользоваться каждой возможностью ускорить сборку проекта. Однако в статье приведено несколько недостатков описанного подхода, и на момент написания статьи они были критичными и не давали возможности использовать его в нашем проекте. 

Итак, теперь сначала..

Немного о проекте Альфа-Бизнес Мобайл

Привет, Хабр! Меня зовут Алексей, главный разработчик проекта Альфа-Бизнес и лид архитектурной компетенции проекта.

Сейчас у нас около 500 модулей и, так как 60 разработчиков постоянно дописывают новый функционал, это количество постоянно растёт. 

Схема зависимостей модулей проекта АБМ
Схема зависимостей модулей проекта АБМ

Многомодульность в нашем проекте имеет классический вид, который я уже описал в статье про разделение презентационного слоя по модулям.

Разделение презентационного слоя фичи на модули в Android приложении
Привет, Хабр! Я, Алексей , ведущий разработчик в платформенной команде Альфа-Бизнес Мобайл. В этой с...
habr.com

Чтобы не переходить туда-сюда по статьям, оставлю здесь цитату:

«У нас есть главный модуль 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-модуле фичи не будет:

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

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

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

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

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

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

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

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

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

Комментарии (7)


  1. namikiri
    20.11.2024 07:37

    Насчёт того, на сколько этот подход нас ускорил, говорить пока рано

    Когда-нибудь мы будем вновь читать о том, насколько ускоряется софт, а не его разработка.


    1. AlexeyMinay Автор
      20.11.2024 07:37

      Обе темы очень важны. И ускорение разработки особенно заметно, когда в проекте большое количество разработчиков. Например, пока я писал статью, нас уже стало 66, а не 60 и это количество постоянно растет. И у всех разная техника, и кто-то очень страдает от долгих сборок. Но и по ускорению софта, я надеюсь, тоже в ближайшее время смогу написать.


  1. Trig3G
    20.11.2024 07:37

    Может не совсем по теме, но как на счёт того, чтобы избавиться от какой-то особенной роли App, а добавить его в проект также, как добавляются другие feature? Возможно так сделать? Просто если в проекте мелькают классы с core в названии, это уже говорит о проблеме.


    1. AlexeyMinay Автор
      20.11.2024 07:37

      А что вы имеете в виду под особенной ролью? Медиатором, может быть любой модуль, просто так повелось, что через app это удобнее сделать. И про core до конца не понял.. о какой пробеме вы говорите?


      1. Trig3G
        20.11.2024 07:37

        О проблеме, когда всё по сути завязано на него, и его "кривое" изменение рушит всё остальное. Короче говоря одна большая точка отказа. Все приложение вроде бы слабо связанное, но имеет в себе одну общую часть на чем все держится.


  1. Evgenij_Popovich
    20.11.2024 07:37

    Есть ещё вариант через стаб реализацию, как это делается в библиотеке cicerone https://github.com/terrakok/Cicerone


  1. TheBasta
    20.11.2024 07:37

    Теперь бы ещё взять этот общий модуль, добавить модуль со списком и балансом карт, модуль платежей по СБП и собрать в отдельное Lite-приложение без всего остального. Будет идеально для повседневного применения.