Создание модульных приложений/библиотек для Android дает целый ряд преимуществ, а процессу разработки следует уделять достаточно внимания. Перечень получаемых плюсов достаточно большой, перечислим лишь основные из них.

Изоляция

Изоляция модулей сократит побочные эффекты и непредсказуемые задержки в исполнении кода. С увеличением количества строк они становятся намного опаснее, а в небольших и изолированных частях/модулях риск возникновения проблем гораздо меньше.

Возможность повторного использования

Одни и те же модули можно применять в разных приложениях. Если вы хотите использовать подход с монорепозиторием для нескольких приложений, тогда каждая часть кода будет модулем. Также вы можете отправить их все в репозиторий maven, что упростит использование из других проектов. Функция загрузки изображений применяется достаточно широко.

Зависимости

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

Вот самое простое расширение для загрузки изображений с помощью библиотеки Picasso. Его размещение в отдельном модуле лишь раскроет функцию load. Поэтому нам не потребуется добавлять зависимость Picasso в функциональные модули.

fun ImageView.load(url: String) {
    Picasso.get().load(url).into(this)
}

imageloader/build.gradle

implementation 'com.squareup.picasso:picasso:2.71828'

myfeature/build.gradle

implementation project(":imageloader")

Picasso можно с легкостью изменить с помощью Glide или любой другой библиотеки загрузки изображений. Дело в том, что Picasso представляет собой внутреннюю зависимость и ее можно изменять в любое время, не влияя на открытое поведение.

Модуляризация позволяет разделять внутренние и внешние зависимости.

Основной упор на поведение

Разделение на модули заставляет строить модели поведения. Как и в примере с загрузкой изображений, мы раскрываем только функцию load(), не имеющую ничего общего с Picasso.

Скорость сборки

Если в модуле нет изменений, то он не будет повторно собираться. Это коснется только измененных и зависящих от него частей.

Методы модуляризации

Есть несколько подходов к модуляризации. Их выбирают в зависимости от проекта, команды и организации.

Функциональная модуляризация

Особенности:

· Подходит для больших проектов/команд.

· Предоставляет расширенное применение модулей.

· Функциональные свойства можно легко использовать в нескольких приложениях.

· Обеспечивает слабую связь между функциональными модулями.

· Ограничена более строгими правилами.

При подобном подходе у вас будет слишком много модулей. Модуль app во втором варианте делится на функциональные модули. Некоторые общие коды функций также вынесены в отдельные части.

Модуляризация по слоям

Все функциональные модули объединены в модуле app. Это менее строгий подход. Но вы все равно можете использовать разделенные функциональные модули.

· Меньше модулей и меньше изоляции, но управление проще.

· Только слои могут использоваться в нескольких приложениях.

· Все свойства находятся в модуле app, который все же сохраняет свою монолитность.

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

· Модуль network содержит классы, относящиеся к Retrofit, интерфейс API и перехватчики.

· Модуль data содержит различные данные, включая локальные и удаленные репозитории.

· Модули library не зависят от функций или других модулей, таких как загрузка изображений или библиотека аналитики.

· Модуль app содержит все функции и классы, связанные с пользовательским интерфейсом, такие как Activity, Fragment, ViewModels и т. д.

Использование common-module.gradle

Примените этот скрипт gradle ко всем функциональным модулям  —  так вам не придется дублировать ее для каждого из них.

apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'

android {
    compileSdkVersion compileSdk
    buildToolsVersion buildTools

    defaultConfig {
        minSdkVersion minSdk
        targetSdkVersion targetSdk
        versionCode versionCode
        versionName versionName

}

    kotlinOptions {
        jvmTarget = "1.8"
    }

    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }

    buildTypes {
        debug {
            ...
        }
        release {
            ...
        }
    }
}

dependencies {

    implementation deps.kotlin
    implementation deps.koin
    // ... прочие реализации

    // Тестирование
    testImplementation project(":testing")
    testImplementation deps.junit
    // ... 

}

tasks.withType(Test) {
    testLogging {
        events "started", "passed", "skipped", "failed"
    }
}

Ресурсы

Ресурсы конкретных функций могут находиться в функциональных модулях, однако их разделение затруднит организацию. Поэтому лучше хранить их в модуле common.

Если для организации строк между клиентами Android/iOS используется многоязычный инструмент, то обычно он дает один файл strings для каждого языка. Возможно, стоит объединить строки.

Если применяются динамические модули, вам нужно позаботиться о ресурсах, которые могут повлиять на размер приложения/модуля, например, drawables.

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

Отчет охвата Jacoco

Каждый модуль имеет собственный вывод отчетов. Поэтому вам потребуется просмотреть все отчеты охвата в одном месте. Сделать это можно с помощью этого скрипта.

Добавьте строку apply from: “$rootDir/jacoco.gradle” в файл build.gradle каждого модуля.

Вот пример build.gradle:

apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply from: "../common-module.gradle"
apply from: "$rootDir/jacoco.gradle"

dependencies {
    implementation deps.room
}
Образец отчета охвата

Инструментальное тестирование

Упростить задачу тестирования классов Activity/Fragment можно с помощью mock-объекта для некоторого поведения.

В данном случае достаточно только внедрения mock-репозитория.

@RunWith(AndroidJUnit4::class)
class MainActivitySuccessTest {

    @get:Rule
    val rule = ActivityTestRule(MainActivity::class.java, false, false)

    private val repo: MainRepository = mock()

    @Before
    fun setup() {
        runBlocking {
            val resp = ApiResponse(success = listOf(Album("title")))
            whenever(repo.getAlbums()).thenReturn(resp)

        }
        StandAloneContext.loadKoinModules(module {
            single(override = true) { repo }
        })
        val intent = Intent()
        rule.launchActivity(intent)
    }

    @Test
    fun testFetchAlbums() {
        onView(withText("title")).check(matches(isDisplayed()))
    }

}

Читайте также:

Читайте нас в Telegram, VK и Яндекс.Дзен


Перевод статьи Faruk Toptaş: Android App Modularization Tips

Предыдущая статьяРазработка инфраструктуры и торговых ботов для ИИ-трейдинга
Следующая статьяСоздание простой нейронной сети на Python