Автоматизируем рутину в 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
можно прочитать в документации.
После того как уберём лишние строки и заполним необходимые на данном этапе поля, в файле останется такая структура.
com.example.plugin
TestPlugin
TestPluginVendor
com.intellij.modules.platform
org.jetbrains.android
После откроем файл 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 {
return listOf(
moduleTemplate
)
}
}
Теперь добавим этот провайдер в plugin.xml
.
Создание файла 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) и как можно их обойти. Я описал не весь функционал, который нам нужно было покрыть для нужд проекта Альфа-Бизнес: мы не добавляли создание шаблонов медиаторов, навигаторов, репозиториев, андроид манифестов, но этого хватит для понимания принципа создания плагина и любой читатель сможет его доработать под свои запросы. Репозиторий в котором можно посмотреть полностью код из статьи.