Всем привет! На связи Дима Котиков, и я все еще люблю разбираться в технологиях, разрабатывать под Android и KMP и пить латте на фундучном молоке :)
Рассказываю о генерации файлов с boilerplate-кодом с помощью удобного механизма задания File Templates в средах разработки Intellij. File Templates позволяет в пару кликов создавать несколько файлов с каким-либо boilerplate-кодом. Хоть статья приводит примеры создания File Templates для Android/Kotlin Multiplatform, она может быть полезна всем, кто работает в средах разработки от Intellij.
Шаблонный код
Каждый разработчик в повседневной работе сталкивается с созданием однотипных файлов и шаблонных конструкций, будь то файлы а-ля Controller/Service/Repository в backend-разработке или Fragment-, ViewModel-, State-, SideEffect-, Store-сущности в Android/KMP-разработке.
Файлы с таким кодом могут иметь схожие префиксы и суффиксы в названии, похожий или одинаковый код внутри них по типу инициализации сущностей, логирования, аналитики, вызовов методов жизненного цикла и тому подобное.
Каждый раз писать один и тот же код бывает довольно утомительно, неинтересно, затратно по времени, да и фокус внимания на бизнес-задаче или какой-то уникальной логике начинает уменьшаться.
Для примера приведу код одного шаблонного экрана мобильного приложения:
Шаблонный код экрана мобильного приложения
// file: SomeFeatureScreen
@Composable
fun SomeFeatureScreen(
viewModel: SomeFeatureViewModel,
router: SomeFeatureRouter,
) {
val lifecycle = LocalLifecycleOwner.current.lifecycle
LaunchedEffect(viewModel) {
viewModel.sideEffectFlow
.flowWithLifecycle(lifecycle)
.onEach { sideEffect: SomeFeatureSideEffect ->
handleSideEffect(sideEffect, router)
}
.launchIn(this)
}
val state: SomeFeatureState by viewModel.stateFlow.collectAsStateWithLifecycle()
SomeFeatureStateContent(
state = state,
onEvent = { uiEvent -> viewModel.dispatch(uiEvent) }
)
}
private fun handleSideEffect(sideEffect: SomeFeatureSideEffect, router: SomeFeatureRouter) {
// some feature-specific logic
}
@Composable
private fun SomeFeatureStateContent(
state: SomeFeatureState,
onEvent: (SomeFeatureUiEvent) -> Unit
) {
// some feature-specific logic
}
// file: SomeFeatureViewModel
class SomeFeatureViewModel : ViewModel() {
private val _stateFlow = MutableStateFlow<SomeFeatureState>(SomeFeatureState())
val stateFlow: StateFlow<SomeFeatureState>
get() = _stateFlow.asStateFlow()
private val sideEffectChannel = Channel<SomeFeatureSideEffect>()
val sideEffectFlow: Flow<SomeFeatureSideEffect>
get() = sideEffectChannel.receiveAsFlow().flowOn(Dispatchers.Main.immediate)
fun dispatch(uiEvent: SomeFeatureUiEvent) {
// some feature-specific logic
}
private fun reduceState(reducer: (SomeFeatureState) -> SomeFeatureState) {
_stateFlow.update(reducer)
}
private fun sendSideEffect(sideEffect: SomeFeatureSideEffect) {
viewModelScope.launch {
sideEffectChannel.send(sideEffect)
}
}
// other functions with some feature-specific logic
}
// file: SomeFeatureState
@Immutable
data class SomeFeatureState(
val isLoading: Boolean,
... // other some feature-specific fields
)
// file: SomeFeatureSideEffect
@Immutable
sealed interface SomeFeatureSideEffect {
data class ShowUnknownError(val error: Throwable) : SomeFeatureSideEffect
// other some feature-specific subclasses
}
// file: SomeFeatureUiEvent
@Immutable
sealed interface SomeFeatureUiEvent {
data object NavigateBack : SomeFeatureUiEvent
// other some feature-specific subclasses
}
// file: SomeFeatureRouter
interface SomeFeatureRouter {
fun navigateBack()
// other some feature-specific functions
}
// file: SomeFeatureRouterImpl
class SomeFeatureRouterImpl : SomeFeatureRouter {
override fun navigateBack() {
// some feature-specific logic
}
// other some feature-specific functions
}
Код немного утрирован для понимания того, что происходит в представленном примере, в production-коде некоторые конструкции могут быть вынесены в интерфейсы или абстрактные классы.
Если мы внимательно посмотрим на код, то легко сможем выделить некоторые конструкции, которые можно назвать шаблонными:
Названия сущностей именуются как "<feature name>" + "<suffix>", где "<feature name>" — специфичное название для фичи или экрана, а "<suffix>" — шаблонный суффикс, обозначающий, что это за сущность: экран, вьюмодель, роутер, модель состояния экрана и тому подобное.
У сущностей есть шаблонные вызовы, например в
SomeFeatureScreen
вызываются подписки на состояние (viewModel.stateFlow.collectAsStateWithLifecycle()
) и side effect-ы (код внутриLaunchedEffect
). В классеSomeFeatureViewModel
мы видим стандартный подход к хранению сущности состояния (StateFlow<SomeFeatureState>
) и его смены (функцияreduceState()
), а также хранение и отправкаsideEffect
-ов (one-time-events).
Если бы приложения состояли только из одного экрана и нам нужно было единожды написать шаблонный код, то особых проблем не возникло бы. На деле даже в небольших мобильных приложениях количество фич и экранов может достигать 50—100 штук. Представим, сколько времени суммарно может уйти, чтобы вручную 100 раз создать по семь файлов с шаблонным кодом, не говоря уже о том, что на каждом экране нужно прописать бизнес-логику, верстку, взаимодействие с сетью, локальным хранилищем, навигацию и так далее.
Как уменьшить боль от boilerplate-кода
Вариантов для решения проблемы несколько:
Сделать отдельную папку, в которой можно создать файлы с кодом, а потом копипастить в package
-ы. Наверное, это самый плохой способ решения проблемы, потому что каждый раз придется вручную переименовывать каждый файл, класс и так далее. Ускорение по времени от такого подхода сомнительное.
Написать annotation- или symbol-processor для генерации файлов или классов на базе аннотаций. Инструмент мощный, но не особо подходит для решения нашей проблемы. Все равно нужно руками создать как минимум один файл, написать какой-то базовый код, расставить аннотации, запустить компиляцию и только после этого пользоваться сгенерированными файлами и кодом в них.
А еще нужно помнить, что в сгенерированных файлах не получится дописать свой код, так как при следующей компиляции файлы запросто могут быть переписаны процессором аннотаций. Добавим к этому тот факт, что написание таких процессоров — штука нетривиальная и требует специфичного API, рефлексии и т. д. В общем, путь, который подходит скорее для создания библиотек и фреймворков, но не для решения нашей проблемы.
Написать плагин для Intellij — вариант рабочий, но потребует погружения в документацию по написанию плагинов и, по сути, тоже достаточно сложный процесс для решения нашей задачи.
Использовать встроенный в Intellij-среду механизм Live Templates — частично решает проблему с генерацией шаблонного кода, но не решает задачу создания и именования семи файлов.
Использовать встроенный в Intellij-среду механизм File Templates — наш выбор, так как умеет создавать файлы по шаблонам, причем сразу несколько. Плюс ко всему есть возможность для написания шаблонного кода по различным условиям.
А если вы знаете другие способы работы с бойлерплейтом — пишите в комментариях ?
В моих вариантах нет обобщения в базовые классы и интерфейсы. Этими механизмами можно и нужно пользоваться, но мы приняли за аксиому, что от boilerplate нельзя уйти. Мы все равно с ним столкнемся, но уже с использованием абстракций и обобщений.
Плюсы, возможности и синтаксис File Templates
Есть несколько весомых причин выбрать File Templates:
инструмент решает нашу проблему — генерирует файлы и контент в них по шаблонам;
генерирует несколько файлов в одно действие;
задает пользовательские переменные, которые можно использовать при генерации шаблонного кода;
для написания шаблонов не нужно глубоких и специфических знаний — достаточно изучить несколько базовых конструкций, чтобы решать большинство проблем с созданием шаблонов;
инструмент интегрирован в Intellij-подобные среды разработки, поэтому дополнительных действий, связанных с установкой и первоначальной настройкой (как это нужно для плагинов), не требуется.
Примеры покажу в Android Studio, но практически вся информация валидна и для остальных сред разработки на базе Intellij IDEA.
Чтобы добраться до инструмента File Templates, нужно открыть Settings → Editor → File and Code Templates:
В меню много заготовленных шаблонов, но большинство из них имеют пару строчек с базовыми конструкциями, поэтому будем писать свои.
Нас интересуют вкладки Files
и Includes
. На вкладке Files
располагаются коллекции с шаблонами, а во вкладке Includes
— шаблоны, предназначенные для переиспользования в шаблонах из вкладки Files.
Весь шаблонный код, который мы будем писать, по сути своей будет миксом из кода языка и из вспомогательного кода, состоящего из специальных переменных для File Templates и конструкций, написанных на Apache Velocity — языке, используемом для шаблонов.
Из стандартных переменных чаще всего приходится использовать ${NAME} и ${PACKAGE_NAME}. Можно создавать свои переменные, для этого их достаточно просто объявить в месте использования в шаблоне и тогда при создании шаблона появится отдельное поле для ввода кастомной переменной.
Посмотрим синтаксис Apache Velocity, так как на нем пишется логика для шаблонов.
Первое, что можно увидеть в стандартных шаблонах, — указание package name для файлов через конструкцию if
:
#if (${PACKAGE_NAME} && ${PACKAGE_NAME} != "")
package ${PACKAGE_NAME}
#end
Есть возможность писать конструкции if-else
:
#if (${NAME} != "")
class ${PACKAGE_NAME}
#else
class Unknown
#end
Или if-elseif-else
:
#if (${NAME} == "")
class Unknown
#elseif (${NAME} == "a")
class OneLetter
#else
class ${NAME}
#end
Все ключевые слова начинаются со знака #
, ссылки на переменные с $
. Как указано в документации: ссылки начинаются со знака $
и используются для получения чего-либо. Директивы начинаются со знака #
и используются, чтобы сделать что-либо (присвоить значение, обработать что-либо и т. п.).
Присвоение переменной и ее последующее применение выполняется так:
#set( $someVar = "Hello, World!" ) $someVar
Если мы хотим, чтобы переменная была задана путем пользовательского ввода, ее нужно сразу указывать в месте использования без set
-конструкции:
$USER_INPUT // or
${USER_INPUT}
А еще нам доступны циклы:
#set( $customerParts = ["John", "Doe"] )
#set( $customerFull = "" )
#set( $part = "" )
#foreach( $part in $customerParts )
#set( $customerFull = $customerFull + $part )
#end
$customerFull // prints `JohnDoe`
Ключевые слова #break
позволяют прервать цикл #foreach
, а #stop
прекращает выполнение кода в template-файле.
Для переиспользования скриптов между файлами шаблонов нужно задать шаблон во вкладке Includes
, а потом в целевых файлах вызвать с помощью конструкции #parse
— простейший пример есть в шаблонах для проставления хедера файла:
#parse("File Header.java")
Аналогично #parse
есть еще конструкции #include
:
#include("SharedData.java")
Ключевые отличия #parse
от #include
:
#parse
допускает, что в передаваемом в параметры файле может содержаться VLT-код, который будет выполнен в месте вызова.#include
же просто добавит контент из передаваемого в параметрах файла прямиком как есть в место вызова, без обработки VLT-кода.#parse
может принимать в себя только один аргумент, а#include
— несколько:
#parse("File1.java", "File2.java") // ERROR, 2 parameters passed. Must be 1 parameter
#include("File1.java", "File2.java") // CORRECT, more than 1 parameters available
Еще есть директива #define
, которая позволяет задать какой-то шаблон с переменными, которые должны вычислиться позже, к примеру:
#define( $block )
Hello $who
#end
#set( $who = 'World!' )
$block // "Hello World!" will be print
Есть возможность задать переиспользуемые части с помощью конструкции #macro:
#macro( cell )
<tr><td></td></tr>
#end
Заданную в #macro
конструкцию дальше в шаблоне можно вызвать так:
#cell // "<tr><td></td></tr>" will be print
#cell // "<tr><td></td></tr>" will be print
В VLT-коде нам доступны функции из Java-мира. Особенно полезны функции работы со строками, например:
$NAME.replaceAll("([a-z])([A-Z])", "$1_$2").toLowerCase() // replaces "SomeString" camel-case string to "some_string" snake-case string
Указанных конструкций в большинстве случаев должно хватить при написании шаблонов. Больше информации можно найти в документации по ссылкам: User Guide, VLT Reference, Glossary.
Итоги первой части
В этой части мы выделили проблему с необходимостью написания шаблонного кода и рассмотрели возможные варианты уменьшения боли. А еще познакомились со встроенным инструментом File Templates и основным синтаксисом.
В следующей статье рассмотрим примеры использования File Templates для решения типовых сценариев, где требуется шаблонный код. Не переключайтесь :)