Всем привет! На связи Дима Котиков, и я все еще люблю разбираться в технологиях, разрабатывать под 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 для решения типовых сценариев, где требуется шаблонный код. Не переключайтесь 🙂
ссылка на оригинал статьи https://habr.com/ru/articles/872684/
Добавить комментарий