Введение
Приходилось ли вам работать над проектом со сложной архитектурой, в котором для создания нового экрана нужно добавить несколько новых файлов с определенным содержимым? Например, при работе с Model-View-Presenter и Dagger вы хотите добавить экран Main
. Возможно, также потребуется добавить такие файлы, как MainActivity.kt
, MainView.kt
, MainPresenter.kt
, MainPresenterImpl.kt
, MainModule.kt
, MainComponent.kt
, activity_main.xml
и т. д. Слишком много, не так ли? Мне часто приходилось копировать эти файлы из других экранов и переименовывать. Но должен же быть способ получше!
Я решил создать собственный плагин для Android Studio для автоматизации работы. В этой статье я расскажу о трудностях, с которыми пришлось столкнуться при написании плагина, а также о способах их решения.
Требования для плагина
Плагин должен соответствовать следующим параметрам:
- Пользователь может устанавливать файлы, которые нужно сгенерировать для каждого экрана. Возьмем для примера два типа файлов: Kotlin и layout XML.
- Эти установки должны соответствовать проекту.
- Пользователь может создавать шаблон содержимого для каждого файла, в котором можно использовать такие переменные, как Название экрана (Screen Name), Название элемента экрана (Screen Element’s Name, т.е. Presenter), базовый класс Component Android (т.е. Activity Фрагмента) и т. д.
- Пользователь может генерировать эти файлы из контекстного меню в структуре проекта. Ему будет предложено ввести название пакета, название экрана, а также выбрать компонент Android (Activity Фрагмента).
- Название пакета заполняется автоматически в соответствии с пакетом, из которого вызвано действие. Название можно изменить во всплывающем диалоговом окне.
Экран параметров
Для создания экрана параметров, доступного из Preferences в Android Studio, нужно выполнить следующее:
К счастью, работать с пользовательским интерфейсом достаточно легко. Добавляем новые файлы в список элементов экрана. Его название, шаблон названия файла и тип файла можно изменить в панели деталей и свойств экрана. В панели code template напишите код, который нужно сгенерировать в файле. Сгенерированный код со всеми переменными, замененными на демо данные, отобразится в панели sample code. Все переменные имеют формат %variableName%
. Список доступных переменных с описаниями можно найти, нажав на Help. Activity и базовый класс Fragment можно настроить в панели компонентов android.
Для начала нужно создать класс, реализующий интерфейс Configurable
, а затем зарегистрировать его в plugin.xml
. Я использовал следующий код:
<extensions defaultExtensionNs="com.intellij">
<defaultProjectTypeProvider type="Android"/>
<projectConfigurable
instance="ui.settings.SettingsViewImpl">
</projectConfigurable>
</extensions>
Полную версию исходного кода для этого экрана можно найти здесь.
Я стремился создать гибкий пользовательский интерфейс, основанный на библиотеке Java Swing. UI можно создать с помощью встроенного инструмента IntelliJ. Зайдите в Eclipse и создайте там UI, а затем просто скопируйте код или напишите его сами. Я выбрал последний вариант, но не уверен, что он был из простых. API IntelliJ также предоставляет несколько компонентов, таких как JBList
, ToolbarDecorator
или JBSplitter
. Они более предпочтительны для использования, чем стандартные компоненты Swing. Я использовал Kotlin DSL, который облегчает построение UI. Создание простой панели выглядит следующим образом:
panel(LCFlags.fillX, title = "Android Components") {
row(label = "Activity Base Class:") { activityTextField() }
row(label = "Fragment Base Class:") { fragmentTextField() }
}
В данном примере activityTextField
и fragmentTextField
являются созданными мной ранее JTextField
.
Сохранение состояния
Согласно второму требованию для плагина, он должен сохранять состояние параметров проекта. Сделать это достаточно просто. Нужно создать класс с аннотацией @State
, реализующий PersistentStateComponent
, и зарегистрировать его в plugin.xml
:
<extensions defaultExtensionNs="com.intellij">
<!-- ... -->
<projectService serviceInterface="data.ScreenGeneratorComponent" serviceImplementation="data.ScreenGeneratorComponent"/>
</extensions>
Мой компонент:
@State(name = "ScreenGeneratorConfiguration",
storages = [Storage(value = "screenGeneratorConfiguration.xml")])
class ScreenGeneratorComponent : Serializable, PersistentStateComponent<ScreenGeneratorComponent> {
companion object {
fun getInstance(project: Project) = ServiceManager.getService(project, ScreenGeneratorComponent::class.java)
}
var settings: Settings = Settings()
override fun getState(): ScreenGeneratorComponent = this
override fun loadState(state: ScreenGeneratorComponent) {
XmlSerializerUtil.copyBean(state, this)
}
}
Не упустите класс, который вы хотите сериализовать! В моем случае, это Settings
. Необходимо, чтобы все поля были помечены как изменяемые переменные var
, а список являлся экземпляром MutableList
. В противном случае, сериализация будет выполнена неправильно.
Диалоговое окно нового экрана
Шаблоны, созданные в экране параметров, в дальнейшем используются для генерирования файлов. Новый экран можно добавить, нажав на File -> New -> Screen в верхнем меню или просто нажав правой кнопкой мыши на любой пакет в структуре проекта и выбрав New -> Screen. Затем появится следующее диалоговое окно:
Здесь нужно выбрать пакет, название экрана и компонент Android для расширения. Эти входные данные заменят определенные переменные в шаблонах и названиях файлов.
Теперь разберемся, как добавить действие в меню File -> New. Нужно создать пользовательский класс, расширяющий AnAction
, а затем зарегистрировать его в plugin.xml
. Код XML:
<actions>
<group id="ScreenGenerator.FileMenu"
text="Screen"
description="Screen Generator Plugin">
<add-to-group group-id="NewGroup" anchor="last"/>
<separator />
<action id="NewScreenAction"
class="ui.newscreen.NewScreenAction"
text="Screen"
description="Screen Generator Plugin"/>
</group>
</actions>
Важно найти правильный group-id
для добавления действия в группе меню. Я нашел его, заглянув в файл LangActions.xml
. Его можно открыть в IDE с помощью комбинации клавиш для поиска символа Cmd+Shift+O
и ввода его названия.
Теперь переопределяем функцию actionPerformed
в классе Action
и отображаем в ней диалоговое окно. Для создания простого диалогового окна нужно создать расширение из класса DialogWrapper
. Здесь можно посмотреть мой код для NewScreenAction
.
Генерация файлов из шаблонов
Самая сложная часть написания плагина — создание файлов. В IntelliJ Platform SDK есть специальный API для работы с файлами. Базовые компоненты: VirtualFile
для представления одного файла из VirtualFileSystem
, PsiFile
для представления файла для определенного языка и PsiDirectory
для представления каталога файловой системы.
Нам предстоят четыре этапа. Первый — получить каталог исходных файлов (т. е. app/src/main/java) или файл ресурсов (т. е. app/src/main/res). Затем нужно найти или создать подкаталог в соответствии с названием пакета для файла Kotlin или просто подкаталог layout для XML. Третий этап — создание PsiFile
, а четвертый — добавление его в подкаталог.
Чтобы получить все исходные roots, я использовал метод:
val virtualFiles: Array<VirtualFile> =
ProjectRootManager.getInstance(project).contentSourceRoots
Он возвращает массив VirtualFile
, содержащий все исходные каталоги, такие как сгенерированный код, тестирование и т. д. Я создавал плагин для проекта с одним модулем, поэтому отфильтровал объекты VirtualFile
, путь которых содержит такие слова, как build
, test
и res
, и получил соответствующий исходный root. Для получения исходного root ресурсов, нужно отфильтровать путь, содержащий res
. В этом случае параметром проекта является объект класса Project
. Его можно получить в функции actionPerformed
из пользовательского AnAction
, поскольку он обладает объектом AnActionEvent
, переданным в качестве параметра, и ссылается на Project
.
Чтобы конвертировать VirtualFile
из PsiDirectory
, используем следующий метод:
val directory: PsiDirectory =
PsiManager.getInstance(project).findDirectory(virtualFile)
В PsiDirectory
нам понадобятся два метода: findSubdirectory(name)
, чтобы получить объект PsiDirectory
из подкаталога, и createSubdirectory(name)
в случае отсутствия каталога.
val subdirectory: PsiDirectory =
psiDirectory.findSubdirectory(name) ?: psiDirectory.createSubdirectory(name)
Теперь создаем PsiFile
и добавляем его в подкаталог, используя следующий фрагмент кода:
val language =
when(fileType) {
"kotlin" -> KotlinLanguage.INSTANCE
"xml" -> XMLLanguage.INSTANCE
}
val psiFile =
PsiFileFactory
.getInstance(project)
.createFileFromText(fileName, language, fileContent)
psiDirectory.add(psiFile)
Поддержка языка Kotlin
Нет необходимости писать плагин на Kotlin. Просто создайте PsiFile
, обладающий типом языка KotlinLanguage
. Для получения доступа к классам Kotlin Language, нужно добавить следующий код в plugin.xml
:
<depends>org.jetbrains.kotlin</depends>
А этот фрагмент добавьте в build.gradle
:
intellij {
//...
plugins 'kotlin'
}
Автозаполнение названия пакета
Еще одна трудность, с которой я столкнулся при написании плагина — получение текущего названия файла и автоматическое добавление его в текстовое поле package name в диалоговом окне нового экрана. Как оказалось, сделать это легко. Объект VirtualFile
можно получить из AnActionEvent
:
override fun actionPerformed(event: AnActionEvent) {
val currentFile = event.getData(DataKeys.VIRTUAL_FILE)
}
Теперь у нас есть текущий файл, а с помощью парсинга его пути можно получить название пакета.
Заключение
Исходный код можно найти здесь:gmatyszczak/screen-generator-plugin
Contribute to gmatyszczak/screen-generator-plugin development by creating an account on GitHub.github.com
Там также есть инструкция по установке и использованию. Наслаждайтесь! 🙂
Перевод статьи Grzegorz Matyszczak: How I automated creating files for a new screen with my own Android Studio Plugin