Привет, Хабр! Меня зовут Алексей, я ведущий разработчик платформенной команды и по совместительству лид архитектурной компетенции в проекте Альфа-Бизнес. Сегодня я расскажу, как можно автоматизировать повторяющуюся работу в андроид-разработке при помощи плагина для 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/
Добавить комментарий