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

В этой статье перейдем к практике — напишем шаблоны для кода типового экрана SomeFeature мобильного приложения.

Подготовка условия для создания шаблона

Структура файлов и папок, которые мы хотим видеть:

Структура файлов и папок. Желтыми блоками обозначены папки (пакеты), зеленым — файлы 
Структура файлов и папок. Желтыми блоками обозначены папки (пакеты), зеленым — файлы 

Определяемся с тем, что нам нужно:

1. Возможность задать имя root folder.

2. Возможность задать название фичи, которое будет являться префиксом в именах файлов — будет задаваться переменной по умолчанию $NAME.

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


А еще нам нужно определить файл, который будет являться точкой входа для нашего шаблона и относительно которого мы будем формировать дочерние файлы и папки в нашей структуре. Для этого выбираем самый верхнеуровневый по вложенности файл, например SomeFeatureViewModel.kt.

Возможность задать root folder

Для указания имени root folder идем в меню создания шаблонов, жмем на вкладке Files на значок + и указываем в форме информацию:

Создание входного файла для генерации шаблонов и помещение в ROOT_PACKAGE
Создание входного файла для генерации шаблонов и помещение в 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
Обобщенный код для расчета $OUT_PACKAGE и применения package

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

Обобщенный код для расчета $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-ы:

Создание child template — сначала выделяем parent template, затем жмем на иконку создания
Создание child template — сначала выделяем parent template, затем жмем на иконку создания

Задаем имя файлу как ${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.

В итоге получаем алгоритм генерации шаблона:

  1. Жмем на создание шаблона, вводим данные в два поля:

    Заполнение формы для генерации шаблонов
    Заполнение формы для генерации шаблонов
  2. Любуемся результатом генерации семи файлов:

    Сгенерированные файлы на основе шаблонов
    Сгенерированные файлы на основе шаблонов

Так всего в пару кликов и заполненных полей мы можем генерировать пачку файлов с шаблонным кодом.

Выводы по второй части

В этой части мы задали входящие переменные на примере root folder, разобрали, как генерировать структуру из файлов и папок и как выносить обобщенный VTL-код для переиспользования в шаблонах. А еще разобрались в том, как генерировать дочерние файлы в подпапках и как решать проблему с некорректным package name.

Выводы после использования File Templates:

  • Инструмент позволяет гибко настраивать генерацию как одного файла, так и нескольких.

  • Есть возможность настроить генерацию файлов в папках и подпапках.

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

  • Можно выделить обобщенные шаблоны для использования в других шаблонах.

В следующей части разберем, как экспортировать и импортировать шаблоны в Intellij- IDE, и подведем общие итоги, не переключайтесь :)

Комментарии (0)