
Всем привет! На связи Дима Котиков, и это вторая статья из цикла про File Templates. В предыдущей части мы описали проблему с необходимостью написания шаблонного кода и вариантами решения, познакомились с инструментом шаблонов File Templates и разобрали синтаксис написания шаблонов.
В этой статье перейдем к практике — напишем шаблоны для кода типового экрана SomeFeature мобильного приложения.
Подготовка условия для создания шаблона
Структура файлов и папок, которые мы хотим видеть:

Определяемся с тем, что нам нужно:
1. Возможность задать имя root folder.
2. Возможность задать название фичи, которое будет являться префиксом в именах файлов — будет задаваться переменной по умолчанию $NAME
.
3. После ввода имени рутовой папки и названия фичи должна быть сгенерирована структура файлов и папок, в файлах — сгенерирован шаблонный код со всеми корректными импортами между файлами.
А еще нам нужно определить файл, который будет являться точкой входа для нашего шаблона и относительно которого мы будем формировать дочерние файлы и папки в нашей структуре. Для этого выбираем самый верхнеуровневый по вложенности файл, например SomeFeatureViewModel.kt
.
Возможность задать root folder
Для указания имени root folder идем в меню создания шаблонов, жмем на вкладке Files
на значок +
и указываем в форме информацию:

ROOT_PACKAGE
В меню создания шаблонов мы указали:
название нашего набора шаблонов как
Compose feature
;расширение файла
kt
, так как нам нужен kotlin-файл;в поле
File name
новую переменную${ROOT_PACKAGE}
и через слеш название файла, состоящее из${NAME}
и суффиксаViewModel
. Содержимое файла заполним позже.
Нажимаем кнопку Apply
для сохранения изменений или кнопку OK
для сохранения изменений и выхода из меню.
Пробуем наш шаблон в деле. Для этого в папке в дереве проекта среды разработки жмем правую кнопку мыши и в контекстном меню выбираем New → Compose feature
:

Видим диалоговое окно, в котором отображается поле File name
, за которое отвечает переменная $NAME
, а еще поле ROOT PACKAGE
для нашей кастомной переменной $ROOT_PACKAGE
:

Заполняем поля, нажимаем на OK
и видим, что структура из файла и папки создалась:

Первый, а заодно и второй пункты нашего плана выполнены: корневая папка создается с именем, которое мы задали, префикс названия фичи для файлов также можно задать и файл генерируется с корректным именем.
Генерация структуры файлов и папок
После ввода имени рутовой папки и названия фичи должна быть сгенерирована структура файлов и папок в соответствии с нужной нам структурой и в файлах должен быть сгенерирован шаблонный код со всеми корректными импортами между файлами.
Для начала заполним шаблонным кодом наш уже созданный шаблон для ViewModel-и.
Шаблон для ViewModel
#if ($PACKAGE_NAME && $PACKAGE_NAME != "" && $ROOT_PACKAGE && $ROOT_PACKAGE != "")
#set($OUT_PACKAGE = "$PACKAGE_NAME.$ROOT_PACKAGE")
#elseif ($PACKAGE_NAME && $PACKAGE_NAME != "")
#set($OUT_PACKAGE = $PACKAGE_NAME)
#elseif ($ROOT_PACKAGE && $ROOT_PACKAGE != "")
#set($OUT_PACKAGE = $ROOT_PACKAGE)
#else
#set($OUT_PACKAGE = "")
#end
#if ($OUT_PACKAGE != "")package ${OUT_PACKAGE}#end
#if ($OUT_PACKAGE != "")
#set($IMPORT_PREFIX = "import ${OUT_PACKAGE}.")
#else
#set($IMPORT_PREFIX = "import ")
#end
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.flow.flowOn
${IMPORT_PREFIX}model.${NAME}State
${IMPORT_PREFIX}model.${NAME}SideEffect
${IMPORT_PREFIX}model.${NAME}UiEvent
class ${NAME}ViewModel : ViewModel() {
private val _stateFlow = MutableStateFlow<${NAME}State>(${NAME}State())
val stateFlow: StateFlow<${NAME}State>
get() = _stateFlow.asStateFlow()
private val sideEffectChannel = Channel<${NAME}SideEffect>()
val sideEffectFlow: Flow<${NAME}SideEffect>
get() = sideEffectChannel.receiveAsFlow().flowOn(Dispatchers.Main.immediate)
fun dispatch(uiEvent: ${NAME}UiEvent) {
// some feature-specific logic
}
private fun reduceState(reducer: (${NAME}State) -> ${NAME}State) {
_stateFlow.update(reducer)
}
private fun sendSideEffect(sideEffect: ${NAME}SideEffect) {
viewModelScope.launch {
sideEffectChannel.send(sideEffect)
}
}
}
В блоке if-else-ов мы проверяем, заданы ли переменные $PACKAGE_NAME
и $ROOT_PACKAGE
и не пустые ли они. В зависимости от фактических значений переменных у нас конфигурируется итоговая строка, которую мы присваиваем переменной $OUT_PACKAGE
.
#if ($PACKAGE_NAME && $PACKAGE_NAME != "" && $ROOT_PACKAGE && $ROOT_PACKAGE != "")
#set($OUT_PACKAGE = "$PACKAGE_NAME.$ROOT_PACKAGE")
#elseif ($PACKAGE_NAME && $PACKAGE_NAME != "")
#set($OUT_PACKAGE = $PACKAGE_NAME)
#elseif ($ROOT_PACKAGE && $ROOT_PACKAGE != "")
#set($OUT_PACKAGE = $ROOT_PACKAGE)
#else
#set($OUT_PACKAGE = "")
#end
Проверяем переменную $OUT_PACKAGE
, которую создавали выше, и, если она не пустая, указываем ее вместе с ключевым kotlin-словом package
.
#if ($OUT_PACKAGE != "")package ${OUT_PACKAGE}#end
Конфигурируем строку для последующего применения в импортах сущностей State
, SideEffect
и UiEvent
(они будут созданы чуть ниже).
Далее по коду мы применяем переменную $IMPORT_PREFIX
, а также задаем префикс ${NAME}
для названий сущностей.
#if ($OUT_PACKAGE != "")
#set($IMPORT_PREFIX = "import ${OUT_PACKAGE}.")
#else
#set($IMPORT_PREFIX = "import ")
#end
Шаблон готов — выглядит легче, чем нарисовать сову ?

Проверяем, чтобы написанный шаблон генерировал тот код, что ожидается.
Переиспользование VTL-кода в шаблонах
Вышеупомянутые блоки VTL-кода нам нужно будет переиспользовать в других дочерних файлах. Варианты переиспользования VTL-кода:
копипастить из файла в файл, но, как мы понимаем, это не путь самурая;
обобщить код в отдельных файлах шаблонов и переиспользовать — наш путь.
Для того чтобы переиспользовать код, нам потребуется выделить его в отдельные файлы. Переходим в Settings → Editor → File and Code Templates → вкладка Includes
, жмем на иконку +
для создания нового файла и копируем в новый файл VTL-код для конфигурирования переменной $OUT_PACKAGE
. А еще копируем строку с VTL-кодом, в которой задается package
.

$OUT_PACKAGE
и применения packageВынесем в отдельный файл и блок с VTL-кодом расчета $IMPORT_PREFIX
:

$IMPORT_PREFIX
Теперь обобщенный код можно использовать в шаблонах через вызов конструкции #parse
. Расчет строки package
и импорты в итоговом варианте шаблона в файле ViewModel-и будут выглядеть так:
#parse("CalculatePackage.kt")
#parse("CalculateImportPrefix.kt")
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
...
import kotlinx.coroutines.flow.flowOn
${IMPORT_PREFIX}model.${NAME}State
${IMPORT_PREFIX}model.${NAME}SideEffect
${IMPORT_PREFIX}model.${NAME}UiEvent
class ${NAME}ViewModel : ViewModel() {
...
Создание дочерних файлов с шаблонами
Теперь в соответствии с нашей структурой файлов и папок нужно добавить шаблоны классов, находящихся в папке (пакете) model. Чтобы файлы создавались автоматически при создании шаблона ViewModel-и, их нужно создавать не как отдельные шаблоны, а как Child Template File-ы:

Задаем имя файлу как ${ROOT_PACKAGE}/model/${NAME}State
, меняем extension на kt и в окне создания шаблона прописываем:
#parse("CalculatePackage.kt")
import androidx.compose.runtime.Immutable
@Immutable
data class ${NAME}State(
val isLoading: Boolean,
)
Жмем OK, пробуем создать шаблон и видим, что импорт в файле со State-классом некорректный:

Мы рассчитали package
одинаково для всех сущностей — и для корневого файла, и для дочернего, который лежит во вложенном пакете model
. К сожалению, конструкция #parse
не может принимать в себя какие-либо параметры. Есть несколько решений ошибки неправильного package name:
В вызывающем конструкцию #parse шаблоне можно задать новую переменную $PACKAGE_SUFFIX
, а в обобщенном шаблоне CalculatePackage.kt
использовать ее. В этом случае обобщенный шаблон CalculatePackage.kt
будет выглядеть так:
#if ($PACKAGE_NAME && $PACKAGE_NAME != "" && $ROOT_PACKAGE && $ROOT_PACKAGE != "")
#set($OUT_PACKAGE = "$PACKAGE_NAME.$ROOT_PACKAGE")
#elseif ($PACKAGE_NAME && $PACKAGE_NAME != "")
#set($OUT_PACKAGE = $PACKAGE_NAME)
#elseif ($ROOT_PACKAGE && $ROOT_PACKAGE != "")
#set($OUT_PACKAGE = $ROOT_PACKAGE)
#else
#set($OUT_PACKAGE = "")
#end
#if ($OUT_PACKAGE != "" && $PACKAGE_SUFFIX && $PACKAGE_SUFFIX != "")
package ${OUT_PACKAGE}.${PACKAGE_SUFFIX}
#elseif ($OUT_PACKAGE != "")
package ${OUT_PACKAGE}
#elseif ($PACKAGE_SUFFIX && $PACKAGE_SUFFIX != "")
package $PACKAGE_SUFFIX
#end
Расчет package name в рутовом файле шаблона Compose feature
будет выглядеть так:
#set($PACKAGE_SUFFIX = "")
#parse("CalculatePackage.kt")
#parse("CalculateImportPrefix.kt")
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.Channel
...
В файле с шаблоном для State код будет выглядеть вот так:
#set($PACKAGE_SUFFIX = "model")
#parse("CalculatePackage.kt")
import androidx.compose.runtime.Immutable
@Immutable
data class ${NAME}State(
val isLoading: Boolean,
)
Из обобщенного шаблона можно убрать конструкцию #if ($OUT_PACKAGE != "")package ${OUT_PACKAGE}#end
, вставить ее в вызывающем классе, но перед этим переприсвоить переменную $OUT_PACKAGE
, примерно так:
#parse("CalculatePackage.kt")
#if ($OUT_PACKAGE != "")
package ${OUT_PACKAGE}.model
#else
package model
#end
import androidx.compose.runtime.Immutable
@Immutable
data class ${NAME}State(
val isLoading: Boolean,
)
У каждого способа есть свои плюсы и минусы. В первом случае мы заводим новую и не самую очевидную для использования переменную $PACKAGE_SUFFIX
, но избавляем себя от копипасты if-else
-блока из второго варианта в местах использования.
Во втором варианте мы обходимся без необходимости задавать переменную $PACKAGE_SUFFIX
, но вынуждены добавить if-else
-блок для корректной обработки полученного результата в переменной $OUT_PACKAGE
.
Я выберу первый способ, так как с ним придется писать чуть меньше кода в других шаблонах. Если у вас есть другие способы решения данной проблемы — делитесь в комментариях.
После применения шаблона с нашими доработками получаем корректный результат:

Далее нужно создать шаблоны для классов SideEffect
, UiEvent
, Screen
, Router
и RouterImpl
. Принцип создания у них ровно такой же, как и у шаблонов для ViewModel
и State
. Подробный код в статье приводить не буду, но итоговые шаблоны можно посмотреть на GitHub.
В итоге получаем алгоритм генерации шаблона:
-
Жмем на создание шаблона, вводим данные в два поля:
Заполнение формы для генерации шаблонов -
Любуемся результатом генерации семи файлов:
Сгенерированные файлы на основе шаблонов
Так всего в пару кликов и заполненных полей мы можем генерировать пачку файлов с шаблонным кодом.
Выводы по второй части
В этой части мы задали входящие переменные на примере root folder, разобрали, как генерировать структуру из файлов и папок и как выносить обобщенный VTL-код для переиспользования в шаблонах. А еще разобрались в том, как генерировать дочерние файлы в подпапках и как решать проблему с некорректным package name.
Выводы после использования File Templates:
Инструмент позволяет гибко настраивать генерацию как одного файла, так и нескольких.
Есть возможность настроить генерацию файлов в папках и подпапках.
Есть возможность задать пользовательские переменные, по которым будет происходить генерация шаблонов.
Можно выделить обобщенные шаблоны для использования в других шаблонах.
В следующей части разберем, как экспортировать и импортировать шаблоны в Intellij- IDE, и подведем общие итоги, не переключайтесь :)