Содержание
Введение
Всем привет. Работаю мобильным разработчиком в Narisuemvse. В настоящий момент для разработки используем Flutter и в наших проектах стараемся придерживаться принципов чистой архитектуры типа feature-first. Из-за этого приходится создавать множество папок и файлов по одному и тому же шаблону, поэтому в целях ускорения разработки было принято решение по написанию простого плагина для Android Studio.
Хотелось бы предупредить, что это мой первый опыт в создании плагинов, и я не претендую на роль эксперта, но возможно кто-то находится в поисках простой реализации плагина, и сможет почерпнуть для себя что-то полезное.
Подготовка
Для разработки вам понадобится IntelliJ и Plugin DevKit.
Для начала создадим новый проект:
-
File > New > Project…
-
В списке «Generators» выберите IDE Plugin.
-
Введите название и расположение проекта.
Настройка плагина
Если вы используете версию IDE 2024.2+, вам потребуется произвести миграцию на Gradle Plugin (2.x)
Поскольку для разработки я использую локальную версию Android Studio, минимальная настройка build.gradle.kts выглядит так:
plugins { id("java") id("org.jetbrains.kotlin.jvm") version "1.9.25" id("org.jetbrains.intellij.platform") version "2.2.1" } group = "com.murlodin" version = "1.0.0" repositories { mavenCentral() intellijPlatform { defaultRepositories() } } intellijPlatform { pluginConfiguration { name = "FCA" id="com.murlodin.fca-plugin" } } dependencies { intellijPlatform { local("/Applications/Android Studio.app/Contents") } }
Подробнее про настройку для Android Studio можно почитать тут.
Создание Action
С помощью системы действий мы можем добавлять элементы своего плагина в IDE, например, в нашем случае будем добавлять действие в группу New, которое позволит создавать фичу внутри выбранной нами папки (скриншот ниже).
Создадим файл с нашим действием. Это будет класс с реализацией AnAction():
class FCAAction : AnAction() { override fun actionPerformed(actionEvent: AnActionEvent) { ... } }
Нам нужно реализовать метод actionPerformed(), код в данном методе выполняется при вызове действия. Метод содержит доступ к контекстным данным по типу, информации о проекте, файлам, выбранному элементу и т.д.
Для начала нужно зарегистрировать наше действие. Это можно сделать двумя способами:
-
С помощью IDE, выбрав нужное действие при наведении на название класса. В данном конструкторе можно легко найти нужную группу и действие. Более подробно можно почитать здесь. После успешной регистрации действия оно появится в файле plugin.xml (пункт 2).


-
Также действие можно зарегистрировать вручную, открыв файл resources/META-INF/plugin.xml, зарегистрированное действие выглядит так:
<actions> <action id="com.murlodin.fcaplugin.actions.FCAAction" class="com.murlodin.fcaplugin.actions.FCAAction" text="Add FCA Feature" description="Action for create fca feature" icon="icons/action_icon.svg" > <add-to-group group-id="NewGroup" anchor="last"/> </action> </actions>Для того чтобы добавить свою иконку к действиям, создайте папку icons внутри папки resources. Рекомендации по иконкам можно изучить здесь.
Создание пользовательского интерфейса
Пока наше действие не вызывает какой-либо интерфейс, будем реализовывать модальное окно для ввода названия фичи.
Для этого создадим отдельный файл с реализацией класса DialogWrapper() . Для этого нужно переопределить метод createCenterPanel() :
class FCADialogWrapper(private val action: AnActionEvent) : DialogWrapper(action.project) { override fun createCenterPanel(): JComponent { ... } }
В приведенном выше коде добавим для класса конструктор с параметром action: AnActionEvent, с помощью него сможем получить данные о проекте и выбранной папке. Вызовем конструктор базового класса с передачей проекта DialogWrapper(action.project), в окне которого будет отображаться наше окно.
Добавим метод инициализации диалога, в котором зададим заголовок:
class FCADialogWrapper(private val action: AnActionEvent) : DialogWrapper(action.project) { init { title = "Create FCA Feature" super.init() } ... }
Приступим к написанию UI, для этого будем использовать Kotlin UI DSL Version 2, добавим в классе текстовое поле private lateinit var nameTextField: Cell<JBTextField>, который инициализируем позже, и метод для получения значения этого поля getNameTextFieldValue(), он понадобится для получения значения после нажатия кнопки OK. Реализуем нужный нам интерфейс в createCenterPanel(). Обновленный класс будет выглядеть так:
class FCADialogWrapper(private val action: AnActionEvent) : DialogWrapper(action.project) { // Инициализируем в методе createCenterPanel() private lateinit var nameTextField: Cell<JBTextField> ... fun getNameTextFieldValue() : String = nameTextField.component.text override fun createCenterPanel(): JComponent { return panel { row { label("Feature name") } row { nameTextField = textField() //добавим автоматический фокус .focused() //валидация по нажатию кнопки OK .validationOnApply(nameValidator) } } } }
Во время валидации мы должны проверить значение на соответствие двум условиям: поле не является пустым и отсутствие в родительской папке папок с таким же названием (для этого нам и понадобится параметр action). Реализация валидации выглядит следующим образом:
override fun createCenterPanel(): JComponent { //получаем выбранный с помощью Action объект val selectedFolder = PlatformDataKeys.VIRTUAL_FILE.getData(action.dataContext) val nameValidator: ValidationInfoBuilder.(JBTextField) -> ValidationInfo? = { var isSameName = false // проверяем, есть ли среди дочерних папок папка с таким же названием if (selectedFolder != null) { for(child in selectedFolder.children) { if (it.text == child.name && child.isDirectory) { isSameName = true break } } } // Возвращаем ответ относительно выполненых условий, null уберает ошибку when { isSameName -> ValidationInfo("A folder with that name already exist") it.text.isNullOrBlank() -> ValidationInfo("Please enter a name") else -> null } } ... }
Реализация логики генератора
Ранее мы реализовывали класс AnAction , теперь нужно добавить вывод диалога в метод actionPerformed() , для этого напишем следующее:
override fun actionPerformed(actionEvent: AnActionEvent) { //Создаем экземпляр нашего диалога val dialog = FCADialogWrapper(actionEvent) // Вызываем его if (dialog.showAndGet()) { //После нажатия кнопки OK мы можем получить нужные нам данные val featureName = dialog.getNameTextFieldValue() //реализацию метода генерации можно посмотреть на github generateFeature(actionEvent.dataContext, featureName) } }
Поскольку основная цель плагина — это генерация файлов и папок, нам понадобится класс WriteCommandAction , позволяющий изменять структуру проекта. Важно выполнять операции записи внутри WriteCommandAction.runWriteCommandAction , чтобы обеспечить целостность данных и отсутствие конфликтов.
Реализуем простую генерацию папок:
private fun generateFeature(dataContext: DataContext, featureName: String) { //получаем проект из контекста val project = CommonDataKeys.PROJECT.getData(dataContext) ?: return //получаем выбранную папку val selected = PlatformDataKeys.VIRTUAL_FILE.getData(dataContext) ?: return WriteCommandAction.runWriteCommandAction(project) { val featureFolder = selected.createChildDirectory(this, featureName) val featureFile = featureFolder.createChildData(this, "feature_file.txt") featureFile.writeText("Hello World!") } }
Полную реализацию для генератора фичи можно посмотреть на моем GitHub проекте.
Реализация настроек плагина
Во время написания плагина пришла идея о публикации его в стор. Но для этого нужна более гибкая настройка плагина, так как стили фич могут отличаются в разных проектах.
Начнем с создания модели данных состояния, поскольку мы будем использовать упрощенный подход к управлению состоянием, наследуем модель от класса BaseState , это позволит оперировать данными без дополнительных усилий:
class FCASettingsState : BaseState() { var isCreateDataMapperTemplates by property(IS_CREATE_DATA_MAPPER_FOLDER) var dataMappersFolderName by string(DATA_MAPPER_FOLDER_NAME) ... companion object DefaultFCASettingsProperties { const val IS_CREATE_DATA_MAPPER_FOLDER = false const val DATA_MAPPER_FOLDER_NAME = "mapper" ... } }
Более подробно про реализацию состояния можно почитать здесь.
Теперь мы можем реализовать компонент, отвечающий за управление состоянием, он будет реализовывать класс SimplePersistentStateComponent, простая реализация выглядит так:
@Service(Service.Level.PROJECT) @State( name = "com.murlodin.fcapluginFCASettings", storages = [Storage("FCASettingsPlugin.xml")], ) class FCASettings : SimplePersistentStateComponent<FCASettingsState>(FCASettingsState()) { override fun noStateLoaded() { loadState(FCASettingsState()) } }
Разберем вышенаписанный код.
В первую очередь объявляем аннотации. Аннотация @Service используется для регистрации сервиса, мы можем зарегистрировать сервис на двух уровнях:
-
@Service(Service.Level.PROJECT)— Сервис создается для каждого проекта отдельно. Если в IDE открыто несколько проектов, каждый из них будет иметь свой экземпляр этого сервиса. -
@Service(Service.Level.APP)— Cоздает сервис на уровне всей IDE (глобальный для всех проектов).
Далее объявляем аннотацию @State . Указываем, что данный класс явяляется компонентом состояния. Здесь мы указываем уникальное имя для состояния и файл, в котором будет хранится состояние.
Поскольку мы используем SimplePersistentStateComponent , нам нужно только указать тип модели данных и передать экземпляр в конструктор. Единственный метод, который нам надо реализовать, это noStateLoaded() , тут мы просто указываем поведение в том случае, если состояние не загрузилось.
Если вам нужно больше контроля в управлении состоянием, вы можете реализовать класс PersistentStateComponent , в котором нужно реализовать методы getState() и loadState() самостоятельно.
Теперь нам нужно создать окно для настроек, для этого будем использовать класс BoundConfigurable. Он автоматически связывает элементы UI с моделью данных, а также автоматически отслеживает обновление UI и сохраняет данные при нажатии Apply и OK. Если вам нужно больше контроля, вы можете заменить BoundConfigurable на другой тип Configurable, подробнее здесь.
Реализация выглядит так:
internal class FCASettingsConfigurable(project: Project) : BoundConfigurable(displayName = "FCASettings") { //получаем состояние private val fcaSettings = project.service<FCASettings>() private lateinit var dataSourcesFolderNameTextField: Cell<JBTextField> override fun createPanel(): DialogPanel { val textValidator: ValidationInfoBuilder.(JBTextField) -> ValidationInfo? = { textField -> if (textField.text.isNullOrBlank()) { error("Поле не может быть пустым") } else { null } } return panel { group("FCA Settings") { group("Folder Names") { row { label("data") } row { dataSourcesFolderNameTextField = textField() //данные будут автоматически привязаны к состоянию .bindText( { fcaSettings.state.dataSourcesFolderName ?: "" }, { value -> fcaSettings.state.dataSourcesFolderName = value } ) //проверим поле во время ввода .validationOnInput(textValidator) } } } } } //если вам не нужна проверка на пустые поля, можно удалить эту реализацию override fun apply() { if(dataSourcesFolderNameTextField.component.text.isNullOrBlank()) { //отобразит ошибку в окне настроек throw ConfigurationException("Fields cannot be empty") } super.apply() } }
Последнее, что нам осталось, это зарегистрировать Configurable, для этого нужно добавить новый атрибут в файл plugin.xml внутри тега <idea-plugin>:
<extensions defaultExtensionNs="com.intellij"> <projectConfigurable parentId="tools" instance="com.murlodin.fcaplugin.settings.FCASettingsConfigurable" id="com.murlodin.fcaplugin.settings.FCASettingsConfigurable" displayName="FCA Settings " /> </extensions>
Теперь можем проверять результат. У меня вышел такой плагин:
Заключение
В ближайшее время опубликую плагин в сторе. Название плагина FCA. Буду рад выслушать конструктивную критику и предложения по улучшению плагина.
Спасибо за внимание!
ссылка на оригинал статьи https://habr.com/ru/articles/880160/


Добавить комментарий