Android

Введение

Приходилось ли вам работать над проектом со сложной архитектурой, в котором для создания нового экрана нужно добавить несколько новых файлов с определенным содержимым? Например, при работе с Model-View-Presenter и Dagger вы хотите добавить экран Main. Возможно, также потребуется добавить такие файлы, как MainActivity.kt, MainView.kt, MainPresenter.kt, MainPresenterImpl.kt, MainModule.kt, MainComponent.kt, activity_main.xml и т. д. Слишком много, не так ли? Мне часто приходилось копировать эти файлы из других экранов и переименовывать. Но должен же быть способ получше!

Я решил создать собственный плагин для Android Studio для автоматизации работы. В этой статье я расскажу о трудностях, с которыми пришлось столкнуться при написании плагина, а также о способах их решения.

Требования для плагина

Плагин должен соответствовать следующим параметрам:

  1. Пользователь может устанавливать файлы, которые нужно сгенерировать для каждого экрана. Возьмем для примера два типа файлов: Kotlin и layout XML.
  2. Эти установки должны соответствовать проекту.
  3. Пользователь может создавать шаблон содержимого для каждого файла, в котором можно использовать такие переменные, как Название экрана (Screen Name), Название элемента экрана (Screen Element’s Name, т.е. Presenter), базовый класс Component Android (т.е. Activity Фрагмента) и т. д.
  4. Пользователь может генерировать эти файлы из контекстного меню в структуре проекта. Ему будет предложено ввести название пакета, название экрана, а также выбрать компонент Android (Activity Фрагмента).
  5. Название пакета заполняется автоматически в соответствии с пакетом, из которого вызвано действие. Название можно изменить во всплывающем диалоговом окне.

Экран параметров

Для создания экрана параметров, доступного из Preferences в Android Studio, нужно выполнить следующее:

Настройки генератора экрана в Preferences

К счастью, работать с пользовательским интерфейсом достаточно легко. Добавляем новые файлы в список элементов экрана. Его название, шаблон названия файла и тип файла можно изменить в панели деталей и свойств экрана. В панели 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