Автоматизируем рутину в Android разработке: плагин для создания шаблонного кода на примере создания -api -impl модулей

от автора

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

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

Также мы привыкли автоматизировать свою собственную работу. Инструменты CI/CD позволяют быстрее доставлять ценность клиентам, чем ручное развертывание, линты помогают уменьшить время, которое мы тратим на прохождение задачи код ревью. 

В процессе написания кода тоже встречается много рутинной работы. Это может быть какой-то шаблонный код для написания тестов, например, создание моков и прокидывание в конструктор при создании экземпляра тестируемого класса или добавление нового экрана. Если вы используете подход MVI, что очень актуально для андроид-разработки, то сталкивались с тем, что для каждого экрана приходится создавать множество отдельных файлов с шаблонным кодом для редьюсера, экзекутора, стейта, экшены, сами файлы экрана, и это тоже хотелось бы делать «в один клик».

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

Задача

Сначала определимся со структурой файлов, которые мы хотим генерировать при создании модулей. Для проекта Альфа-Бизнес в каждой фиче нужно создавать два модуля с постфиксами -api и -impl. 

  • В api из дефолтного нам нужен класс-медиатор запуска флоуфрагмента фичи, реализация его будет в модуле impl.

  • Из основного и самого трудозатратно шаблонного кода нам нужно создать классы для di, они располагаются в отдельном пакете. 

  • На слое presentation у нас лежит два пакета view, в котором расположен FlowFragment — это фрагмент, который хостит все другие фрагменты фичи, а навигация лежит в пакете navigation. 

  • Можно также создать файлы для репозитория и api, так как они требуются практически в каждой фиче. 

У меня получилась такая структура модулей, которую мы хотим сгенерировать автоматически.

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

Настройка проекта

Для создания плагина нужно использовать IntelliJ IDEA, подойдет как enterprice так и community edition. Первое, что нам нужно сделать — это создать проект. Для этого выбираем new project -> IDE plugin, вводим название и нажимаем create.

После того как IDE создаст необходимые файлы с зависимостями, мы увидим следующую структуру файлов.

Первое, на что тут стоит обратить внимание, — это файл plugin.xml. В нём содержится необходимая метаинформация о вашем плагине. Подробнее о том, что можно указать в plugin.xml можно прочитать в документации

После того как уберём лишние строки и заполним необходимые на данном этапе поля, в файле останется такая структура.

<idea-plugin>    <id>com.example.plugin</id>    <name>TestPlugin</name>    <vendor email="alexey.minay.dev@gmail.com">TestPluginVendor</vendor>    <depends>com.intellij.modules.platform</depends>    <depends>org.jetbrains.android</depends>    <extensions defaultExtensionNs="com.intellij">    </extensions> </idea-plugin>

После откроем файл build.gradle.kts и настроим его для создания плагина для Android Studio.

intellij {    version.set("2022.3.1.22")    type.set("AI") // Target IDE Platform     plugins.set(listOf("org.jetbrains.android")) }

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

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

Предоставление плагина в Android Studio

Первое, что нам нужно сделать, так это добавить класс, который будет наследником абстрактного класса WizardTemplateProvider. В нём нужно будет переопределить метод getTemplate и предоставить туда все наши шаблоны. В нашем случае это будет moduleTemplate, который мы напишем позже.

class MyWizardTemplateProvider : WizardTemplateProvider() {    override fun getTemplates(): List<Template> {        return listOf(            moduleTemplate        )    } }

Теперь добавим этот провайдер в plugin.xml.

<extensions defaultExtensionNs="com.android.tools.idea.wizard.template">    <wizardTemplateProvider implementation="com.example.plugin.MyWizardTemplateProvider"/> </extensions>

Создание файла Template

Добавим пустую форму для шаблона, которая пока ничего создать не может.

val moduleTemplate    get() = template {        name = "Фиче модули -api и -impl"        minApi = 24        description = "Создание фичемодулей"         category = Category.Folder        formFactor = FormFactor.Mobile        useGenericAndroidTests = false        useGenericLocalTests = false         screens = listOf(            WizardUiContext.ActivityGallery,            WizardUiContext.MenuEntry,            WizardUiContext.NewModule        )         recipe = { data: TemplateData ->         }    }

Если мы запустим таску runIde, запустится новый инстанс Android Studio, в которой можно будет создать новый проект. При создании модуля можно будет увидеть наш шаблон.

Теперь приступим к созданию всех необходимых параметров, которые будут отображаться при выборе шаблона. Опишу для примера пару из них. Первым делом создаём строковую константу, которую будем конкатенировать к названиям файлов. Для этого используем stringParameter, в котором зададим имя шаблона.

val classPrefix = stringParameter {    name = "Введите префикс для классов модуля"    default = "" }  val withDiFilesParam = booleanParameter {    name = "Добавить классы di"    default = true }  val withDiSharedModuleParam = booleanParameter {    name = "Создать SharedModule di"    default = true    enabled = { withDiFilesParam.value } }

WithDiFilesParam — чекбокс: если пользователь плагина выбирает чекбокс, мы будем создавать шаблоны классов di. 

WithDiSharedModuleParam пригодится в случае, если наша фича не предоставляет наружу никаких интерфейсов. Я её сюда добавил, чтобы показать, как сделать чекбокс зависимым от других чекбоксов. В поле enabled мы передаем значение чекбокса withDiFilesParam. Остальные параметры создаются по похожему принципу. 

Теперь создадим чекбоксы в UI.

widgets(    TextFieldWidget(classPrefix),    Separator,    CheckBoxWidget(withDiFilesParam),    CheckBoxWidget(withDiSharedModuleParam),    CheckBoxWidget(withFragmentFlow),    CheckBoxWidget(withMediatorParam),    CheckBoxWidget(withNavigation),    CheckBoxWidget(withDataLayer), )

Создание файла Recipe

Api создание шаблонов сильно ограничено и может не подходить для каких-то нетипичных задач. Например, не получится создать сразу два модуля. Можно добавить два отдельных шаблона для модуля api и impl, но это опять лишнее действие, от которого хочется избавить разработчиков. 

Ещё одна проблема связана с использованием version catalog в проекте. Зависимости вида implementation (libs.androidx.ktx) не получится добавить в проект, используя api темплейтов. Чтобы избежать этих проблем, можно воспользоваться api файловой системой, и создавать все файлы вручную.

fun RecipeExecutor.createModuleFiles(    moduleData: ModuleTemplateData,    withDiFiles: Boolean,    classPrefix: String,    withDiSharedModule: Boolean, ) {    val moduleName = moduleData.name.split(":").last()     val packageName = moduleData.packageName    val lastPackage = packageName.split(".").last()    val apiPackage = packageName.replace(lastPackage, "api.$lastPackage")     val apiRootPath = moduleData.rootDir.absolutePath        .replaceFirst(moduleName, "$moduleName-api")    val apiSrcPath = moduleData.srcDir.absolutePath.replaceFirst(moduleName, "$moduleName-api")        .replace("feature${File.separator}$lastPackage", "feature${File.separator}api${File.separator}$lastPackage")        .replace("java", "kotlin")     val implRootPath = moduleData.rootDir.absolutePath        .replaceFirst(moduleName, "$moduleName-impl")    val implSrcPath = moduleData.srcDir.absolutePath        .replace("java", "kotlin")        .replaceFirst(moduleName, "$moduleName-impl")     moduleData.removeFiles()     println(moduleData.projectTemplateData)     this.addIncludeToSettings("$moduleName-api")    this.addIncludeToSettings("$moduleName-impl")     createBuildGradle(        implRootPath = implRootPath,        apiRootPath = apiRootPath,        lastPackage = lastPackage,        hasDi = withDiFiles,        moduleName = moduleName,        packageName = packageName    )     if (withDiFiles) {        createDiFiles(            srcPath = implSrcPath,            packageName = moduleData.packageName,            classPrefix = classPrefix,            withDiSharedModule = withDiSharedModule,        )    } }

Чтобы не засорять статью лишним кодом, я описал создание только части кода, которая нам была необходима, это файлы гредл и di.

В этом коде находим все пути, по которым нужно будет создавать файлы для наших модулей и потом просто удаляем всё, что генерируется автоматически, в том числе файлы build.gradle, так как их будем создавать сами с нуля. Также добавляем в settings.gradle новые модули. Код удаления файлов выглядит так:

private fun ModuleTemplateData.removeFiles() {    resDir.deleteRecursivelyOrThrow()    manifestDir.deleteRecursivelyOrThrow()    rootDir.resolve("build.gradle").delete()    rootDir.resolve("build.gradle.kts").delete()    rootDir.resolve("proguard-rules.pro").delete()    rootDir.resolve("libs").delete()    rootDir.deleteRecursivelyOrThrow() }

createBuildGradle создаст файлы build.gradle.kts для api и impl модулей.

 fun RecipeExecutor.createBuildGradle(    implRootPath: String,    apiRootPath: String,    lastPackage: String,    moduleName: String,    hasDi: Boolean,    packageName: String ) {    saveFile(        absolutePath = implRootPath,        relative = "build.gradle.kts",        content = getBuildGradleTemplate(            lastPackage = lastPackage,            hasDi = hasDi,            moduleName = moduleName,            packageName = packageName        )    )     saveFile(        absolutePath = apiRootPath,        relative = "build.gradle.kts",        content = getApiBuildGradleTemplate(            packageName = packageName        )    ) }

saveFile — утилитарная функция-расширение, необходимая для более удобного сохранения файлов. Её реализацию можно посмотреть в репозитории.

getBuildGradleTemplate — функция, возвращающая шаблон, который мы хотим записать в build.gradle.kts. Его содержание будет сильно зависеть от плагинов и зависимостей, которые используются на проекте. Так как у нас упрощенный пример, я сделал простую версию этого файла.

fun getBuildGradleTemplate(     packageName: String,     lastPackage: String,     hasDi: Boolean,     moduleName: String ): String {     val prefix = lastPackage.lowercase()      return """         plugins {             id("com.android.application")             id("org.jetbrains.kotlin.android")             ${"id(\"kotlin-kapt\")".appendIf(hasDi)}         }          android {             compileSdk = 34              resourcePrefix = "${prefix}_"             namespace = "$packageName"                          defaultConfig {                 minSdk = 24                 targetSdk = 34             }              compileOptions {                 sourceCompatibility = JavaVersion.VERSION_1_8                 targetCompatibility = JavaVersion.VERSION_1_8             }             kotlinOptions {                 jvmTarget = "1.8"             }             buildFeatures {                 viewBinding = true             }         }          dependencies {             implementation(project(":${moduleName}-api"))             ${"implementation(\"com.google.dagger:dagger:2.45\")".appendIf(hasDi)}             ${"kapt(\"com.google.dagger:dagger-compiler:2.45\")".appendIf(hasDi)}             implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0")         }     """.trimIndent() }

Дальше создаём файлы для di. Нам понадобится значение чекбоксов, который мы задали при создании template.

if (withDiFiles) {    createDiFiles(        srcPath = implSrcPath,        packageName = moduleData.packageName,        classPrefix = classPrefix,        withDiSharedModule = withDiSharedModule,    ) }

Пример функции для создания компонента.

fun getDiComponentTemplate(    diPackageName: String,    classPrefix: String, ): String {    return """    package $diPackageName       import dagger.Component       @Component(        dependencies = [${classPrefix}ComponentDependencies::class],        modules = [${classPrefix}Module::class]    )    interface ${classPrefix}Component {               @Component.Factory        interface Factory {            fun create(                dependencies: ${classPrefix}ComponentDependencies,            ): ${classPrefix}Component        }           } """.trimIndent() }

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

Установка плагина в Android Studio

Для начала запустим таску jar. 

После этого в каталоге build/libs появится jar файл с собранным плагином. 

Чтобы добавить плагин в Android Studio, выбираем file -> settings -> Plugins и выбираем Install Plugin from Disk.

На этом всё, теперь шаблоны можно создавать в пару кликов.


Мы рассмотрели наглядно, как можно создать свой шаблон модулей используя api wizard template, проблемы с которыми придется столкнуться (в основном это ограниченный функционал api) и как можно их обойти. Я описал не весь функционал, который нам нужно было покрыть для нужд проекта Альфа-Бизнес: мы не добавляли создание шаблонов медиаторов, навигаторов, репозиториев, андроид манифестов, но этого хватит для понимания принципа создания плагина и любой читатель сможет его доработать под свои запросы. Репозиторий в котором можно посмотреть полностью код из статьи.


ссылка на оригинал статьи https://habr.com/ru/articles/799873/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *