
Всем привет! На связи Дима Котиков, и это вторая статья из цикла про 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Шаблон готов — выглядит легче, чем нарисовать сову
