- PVSM.RU - https://www.pvsm.ru -
Введение [1]
Подготовка [2]
Создание Action [4]
Заключение [8]
Всем привет. Работаю мобильным разработчиком в Narisuemvse [9]. В настоящий момент для разработки используем Flutter и в наших проектах стараемся придерживаться принципов чистой архитектуры типа feature-first. Из-за этого приходится создавать множество папок и файлов по одному и тому же шаблону, поэтому в целях ускорения разработки было принято решение по написанию простого плагина для Android Studio.
Хотелось бы предупредить, что это мой первый опыт в создании плагинов, и я не претендую на роль эксперта, но возможно кто-то находится в поисках простой реализации плагина, и сможет почерпнуть для себя что-то полезное.
Для разработки вам понадобится IntelliJ и Plugin DevKit [10].
Для начала создадим новый проект:
File > New > Project...
В списке "Generators" выберите IDE Plugin.
Введите название и расположение проекта.
Если вы используете версию IDE 2024.2+, вам потребуется произвести миграцию [11] на 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 можно почитать тут [12].
С помощью системы действий мы можем добавлять элементы своего плагина в IDE, например, в нашем случае будем добавлять действие в группу New, которое позволит создавать фичу внутри выбранной нами папки (скриншот ниже).
Создадим файл с нашим действием. Это будет класс с реализацией AnAction()
:
class FCAAction : AnAction() {
override fun actionPerformed(actionEvent: AnActionEvent) {
...
}
}
Нам нужно реализовать метод actionPerformed()
, код в данном методе выполняется при вызове действия. Метод содержит доступ к контекстным данным по типу, информации о проекте, файлам, выбранному элементу и т.д.
Для начала нужно зарегистрировать наше действие. Это можно сделать двумя способами:
С помощью IDE, выбрав нужное действие при наведении на название класса. В данном конструкторе можно легко найти нужную группу и действие. Более подробно можно почитать здесь [13]. После успешной регистрации действия оно появится в файле 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. Рекомендации по иконкам можно изучить здесь [14].
Пока наше действие не вызывает какой-либо интерфейс, будем реализовывать модальное окно для ввода названия фичи.
Для этого создадим отдельный файл с реализацией класса 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 [15], добавим в классе текстовое поле 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 проекте [16].
Во время написания плагина пришла идея о публикации его в стор. Но для этого нужна более гибкая настройка плагина, так как стили фич могут отличаются в разных проектах.
Начнем с создания модели данных состояния, поскольку мы будем использовать упрощенный подход к управлению состоянием, наследуем модель от класса 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"
...
}
}
Более подробно про реализацию состояния можно почитать здесь [17].
Теперь мы можем реализовать компонент, отвечающий за управление состоянием, он будет реализовывать класс 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, подробнее здесь [18].
Реализация выглядит так:
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. Буду рад выслушать конструктивную критику и предложения по улучшению плагина.
Спасибо за внимание!
Ссылка на проект [16]
Автор: Murlod1n
Источник [19]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/plugin/410001
Ссылки в тексте:
[1] Введение: #%D0%B2%D0%B2%D0%B5%D0%B4%D0%B5%D0%BD%D0%B8%D0%B5
[2] Подготовка: #%D0%BF%D0%BE%D0%B4%D0%B3%D0%BE%D1%82%D0%BE%D0%B2%D0%BA%D0%B0
[3] Настройка плагина: #%D0%BD%D0%B0%D1%81%D1%82%D1%80%D0%BE%D0%B9%D0%BA%D0%B0
[4] Создание Action: #action
[5] Создание пользовательского интерфейса : #ui
[6] Реализация логики генератора: #%D0%B3%D0%B5%D0%BD%D0%B5%D1%80%D0%B0%D1%82%D0%BE%D1%80
[7] Реализация настроек плагина: #%D0%BE%D0%BA%D0%BD%D0%BE%D0%BD%D0%B0%D1%81%D1%82%D1%80%D0%BE%D0%B5%D0%BA
[8] Заключение: #%D0%B7%D0%B0%D0%BA%D0%BB%D1%8E%D1%87%D0%B5%D0%BD%D0%B8%D0%B5
[9] Narisuemvse: https://narisuemvse.by/
[10] Plugin DevKit: https://plugins.jetbrains.com/plugin/22851-plugin-devkit
[11] миграцию: https://plugins.jetbrains.com/docs/intellij/tools-intellij-platform-gradle-plugin-migration.html
[12] тут: https://plugins.jetbrains.com/docs/intellij/android-studio.html#android-studio-plugin-setup
[13] здесь: https://plugins.jetbrains.com/docs/intellij/working-with-custom-actions.html
[14] здесь: https://plugins.jetbrains.com/docs/intellij/icons.html#svg-format
[15] Kotlin UI DSL Version 2: https://plugins.jetbrains.com/docs/intellij/kotlin-ui-dsl-version-2.html
[16] проекте: https://github.com/murlod1n/fca-plugin
[17] здесь: https://plugins.jetbrains.com/docs/intellij/persisting-state-of-components.html#implementing-the-persistentstatecomponent-interface
[18] здесь: https://plugins.jetbrains.com/docs/intellij/settings-guide.html#the-configurable-interface
[19] Источник: https://habr.com/ru/articles/880160/?utm_source=habrahabr&utm_medium=rss&utm_campaign=880160
Нажмите здесь для печати.