Жизнь с многомодульным проектом не так уж проста. Чтобы избежать рутины создания нового модуля мы создали собственный плагин для Android Studio. В процессе реализации мы столкнулись с отсутствием практической документации, перепробовали несколько подходов и откопали множество подводных камней. Получилось две статьи: “Теория” и “Практика”. Встречайте!
О чем буду говорить?
- Зачем плагин? Почему плагин?
- Составление чек-листа
- Варианты автоматизации чек-листа
- Основы разработки плагинов
- Actions
- Разработка UI в плагинах
- Выводы
- Внутренности IDEA: компоненты, PSI
- Внутреннее устройство IDEA
- PSI
- Выводы
Зачем плагин? Почему плагин?
Если вы разрабатываете многомодульный Android-проект, то знаете, какая это рутина – каждый раз создавать новый модуль. Вам нужно создать модуль, настроить в нем Gradle, добавить зависимости, дождаться синхронизации, не забыть что-то исправить в application-модуле – на все это уходит очень много времени. Нам захотелось автоматизировать рутину, и мы начали с составления чек-листа того, что делаем каждый раз, когда создаем новый модуль.
1. Во-первых, мы создаем сам модуль через меню File -> New -> New module -> Android library.
2. Прописываем пути к модулю в файле settings.gradle, потому что у нас есть несколько видов модулей – core-модули и feature-модули, которые лежат в разных папках.
// settings.gradle
include ':analytics
project(':analytics').projectDir = new File(settingsDir, 'core/framework-metrics/analytics)
...
include ':feature-worknear'
project(':feature-worknear').projectDir = new File(settingsDir, 'feature/feature-worknear')
3. Меняем константы compileSdk, minSdk, targetSdk в сгенерированном build.gradle: заменяем их на константы, которые определены в рутовом build.gradle.
// Feature module build.gradle
…
android {
compileSdkVersion rootProject.ext.targetSdkVersion
defaultConfig {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
...
}
}
Примечание: эту часть работы мы недавно вынесли в наш Gradle-плагин, который помогает в несколько строк настроить все необходимые параметры build.gradle файла.
4. Поскольку весь новый код пишем на Kotlin, стандартно подключаем два плагина: kotlin-android и kotlin-kapt. Если модуль каким-то образом связан с UI, дополнительно подключаем модуль kotlin-android-extensions.
// Feature module build.gradle
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'
5. Подключаем общие библиотеки и core-модули. Core-модули – это, например, logger, аналитика, какие-то общие утилиты, а библиотеки – RxJava, Moxy и многие другие.
// Feature module build.gradle
dependencies {
def libraries = rootProject.ext.deps
compileOnly project(':logger')
compileOnly project(':analytics')
…
// Kotlin
compileOnly libraries.kotlin
// DI
compileOnly libraries.toothpick
kapt libraries.toothpickCompiler
}
6. Настраиваем kapt для Toothpick-а. Toothpick – это наш основной DI-фреймворк. Скорее всего вам известно: чтобы в релизной сборке использовать не рефлексию, а кодогенерацию, вам нужно донастроить annotation processor, чтобы он понимал, где брать фабрики для создаваемых объектов:
// Feature module build.gradle
defaultConfig {
...
javaCompileOptions {
annotationProcessorOptions {
arguments = [
toothpick_registry_package_name: "ru.hh.feature_worknear"
]
}
}
...
Примечание: в hh.ru мы используем первую версию Toothpick, во второй убрали возможность использовать кодогенерацию.
7. Донастраиваем kapt для Moxy внутри созданного модуля. Moxy – наш основной фреймворк для создания MVP в приложении, и его нужно немножко докручивать, чтобы он мог работать в многомодульном проекте. В частности, прописать пакет созданного модуля в аргументы kapt-а:
// Feature module build.gradle
android {
...
kapt {
arguments {
arg("moxyReflectorPackage", "ru.hh.feature_worknear")
}
}
...
Примечание: мы уже перешли на новую версию Moxy, и эта часть кодогенерации потеряла свою актуальность.
8. Генерируем кучу новых файлов. Имею ввиду не те файлы, которые создаются автоматически (AndroidManifest.xml, build.gradle, .gitignore), а общий каркас нового модуля: интеракторы, репозитории, DI-модули, презентеры, фрагменты. Этих файлов очень много, они в начале имеют одну и ту же структуру, и создавать их – рутина.
9. Подключаем наш созданный модуль к application-модулю. В этом шаге нужно не забыть донастроить Toothpick в build.gradle файле application-модуля. Для этого мы дописываем пакет созданного модуля в специальный аргумент annotation processor-а – toothpick_registry_children_package_names.
// App module build.gradle
defaultConfig {
…
javaCompileOptions {
annotationProcessorOptions {
arguments = [
toothpick_registry_package_name: "ru.hh.android",
toothpick_registry_children_package_names: [
"ru.hh.analytics",
"ru.hh.feature_worknear",
...
].join(",")
]
}
}
…
После этого донастраиваем Moxy в application-модуле. У нас есть класс, который отмечен аннотацией @RegisterMoxyReflectorPackages – туда мы добавляем название пакета созданного модуля:
// App module file
@RegisterMoxyReflectorPackages(
"ru.hh.feature_force_update",
"ru.hh.feature_profile",
"ru.hh.feature_worknear"
...
) class MoxyReflectorStub
И, в конце концов, не забываем подключить созданный модуль в блок dependences application-модуля:
// Application module build.gradle
dependencies {
def libraries = rootProject.ext.deps
implementation project(':logger')
implementation project(':dependency-handler')
implementation project(':common')
implementation project(':analytics')
implementation project(':feature_worknear')
...
У нас получился чек-лист из девяти пунктов.
Поскольку пунктов много, велика вероятность что-то забыть. А после этого часами гадать, что же произошло и почему проект не собирается.
Мы решили, что так жить нельзя и нужно что-то менять.
Варианты автоматизации чек-листа
После составления чек-листа мы начали искать варианты автоматизации его пунктов.
Первым вариантом стала попытка сделать “Ctrl+C, Ctrl+V”. Мы попробовали найти реализацию создания модуля Android Library, которая доступна нам “из коробки”. В папке с Android Studio (для MacOs: /Applications/Android\ Studio.app/Contents/plugins/android/lib/templates/gradle-projects/) можно отыскать специальную папку с шаблонами тех проектов, которые вы видите при выборе пункта File -> New -> New Module. Мы попробовали скопировать шаблон NewAndroidModule, поменяв id внутри файла template.xml.ftl. После чего запустили IDE, начали создавать новый модуль, и… Android Studio крашнулась, потому что список модулей, которые вы видите в меню создания нового модуля, жестко захардкожен, примитивным копи-пастом его изменить нельзя. При попытке взять и добавить, удалить или изменить какой-то элемент, Android Studio просто крашится.
Вторым вариантом автоматизации чек-листа мы рассматривали движок шаблонов FreeMarker. После неудачной попытки копипаста мы решили посмотреть на шаблоны модулей повнимательнее и обнаружили под капотом FreeMarker-овские шаблоны.
Что такое FreeMarker подробно рассказывать не буду – есть хорошая статья от RedMadRobot и видео с MosDroid от Леши Быкова. Но если кратко – это движок для генерации файлов при помощи шаблонов и специальной Map-ки java-объектов. Вы подаете на вход шаблоны, объекты, и FreeMarker на выходе генерирует код.
Но посмотрим еще раз на чек-лист:
Если приглядеться, то можно заметить, что он делится на две большие группы задач:
- Задачи генерации нового кода (1, 3, 4, 5, 6, 7, 8) и
- Задачи модификации существующего кода (2, 7, 8, 9)
И если с задачами из первой группы FreeMarker справляется на ура, то со второй не справляется совсем. В качестве небольшого примера: в текущей реализации интеграции FreeMarker в Android Studio при попытке вставить в файл settings.gradle строчку, которая не начинается со слова 'include', студия будет крашиться. Тут мы поймали грустного, и решили отказаться от использования FreeMarker.
После неудачи с FreeMarker-ом возникла идея написать свою консольную утилиту для выполнения чек-листа. Внутри Intellij IDEA есть возможность использовать терминал, так почему бы и нет? Напишем скрипт на баше, всего делов:
Но коль скоро мы хотим иметь возможность гибко настраивать создаваемый модуль, нам придется вводить множество различных флажков, которые будет не очень удобно печатать в консоли.
После этого мы сделали как бы шаг назад и вспомнили, что мы работаем внутри Intellij IDEA. А как она устроена? Есть некоторое ядро классов, движок, к которому прикреплено множество плагинов, которые добавляют нужную нам функциональность.
Кто из вас видит на скриншоте больше, чем два подключенных плагина?
Здесь их подключено как минимум три. Если вы работаете с Kotlin, то у вас включен Kotlin-плагин. Если работаете в проекте с Gradle, то включен и Gradle-плагин. Если работаете в проекте с системой контроля версий – Git, SVN или еще что-нибудь, – у вас включен соответствующий плагин для интеграции этой VCS.
Мы посмотрели в официальный репозиторий плагинов JetBrains, и оказалось, что официально зарегистрированных плагинов уже более 4000! Почти весь мир пишет плагины, и эти плагины могут делать все, что угодно: начиная от интеграции в IDEA какого-либо языка программирования и заканчивая специфичными тулзами, которые могут быть запущены изнутри IDEA.
Короче, мы решили написать свой плагин.
Основы разработки плагинов
Переходим к основам разработки плагинов. Для начала вам потребуются всего три вещи:
- IntelliJ IDEA, минимум Community Edition (Можно работать и в Ultimate-версии, но особых преимуществ при разработке плагинов, это не даст);
- Подключенный к ней Plugin DevKit – это специальный плагин, который добавляет возможность писать другие плагины;
- И любой JVM-ный язык, на котором вы хотите писать плагин. Это может быть Kotlin, Java, Groovy – что угодно.
Начинаем с создания проекта плагина. Выбираем New project, пункт Gradle, отмечаем галочкой IntelliJ Platform Plugin и создаем проект.
Примечание: Если вы не видите галочки IntelliJ Platform Plugin – это означает, что у вас не установлен Plugin DevKit.
После заполнения необходимых полей мы увидим пустую структуру плагина.
Давайте посмотрим на него внимательней. Он состоит из:
- Папок, в которых вы будете писать код будущего проекта; (main/java, main/kotlin, etc);
- build.gradle файла, в котором вы будете объявлять зависимости вашего плагина от каких-то библиотек, а также будете настраивать такую вещь, как gradle-intellij-plugin.
gradle-intellij-plugin – Gradle-овый плагин, который позволяет использовать Gradle в качестве системы сборки плагина. Это удобно, потому что почти каждый Android-разработчик знаком с Gradle и умеет с ним работать. Кроме того, gradle-intellij-plugin добавляет в ваш проект полезные gradle-таски, в частности:
- runIde – эта задача запускает отдельный инстанс IDEA с плагином, который вы разрабатываете, чтобы вы могли его отлаживать;
- buildPlugin – собирает zip-архив вашего плагина, чтобы вы могли его распространять либо локально, либо через официальный репозиторий IDEA;
- verifyPlugin – эта задача проверяет ваш плагин на наличие грубых ошибок, которые могут не позволить ему интегрироваться в Android Studio или еще какую-то IDEA.
Что еще полезного дает gradle-intellij-plugin? С его помощью становится проще добавлять зависимости от других плагинов, но про это мы поговорим чуть позже, а пока я могу сказать, что gradle-intellij-plugin – ваш бро, пользуйтесь им.
Вернемся к структуре плагина. Самый важный файл любого плагина – plugin.xml.
<idea-plugin>
<id>com.experiment.simple.plugin</id>
<name>Hello, world</name>
<vendor
email="myemail@yourcompany.com"
url="http://www.mycompany.com">
My company
</vendor>
<description><![CDATA[
My first ever plugin - try to open Hello world dialog<br>
]]></description>
<depends>com.intellij.modules.lang</depends>
<depends>org.jetbrains.kotlin</depends>
<depends>org.intellij.groovy</depends>
<idea-version since-build="163"/>
<actions>
<group description="My actions" id="MyActionGroup" text="My actions">
<separator/>
<action id="com.experiment.actions.OpenHelloWorldAction"
class="com.experiment.actions.OpenHelloWorldAction"
text="Show Hello world" description="Open dialog">
<add-to-group group-id="NewGroup" anchor="last"/>
</action>
</group>
</actions>
<idea-plugin>
Это файл, который содержит в себе:
- Метаданные вашего плагина: идентификатор, название, описание, информацию о вендоре, change log
- Описание зависимостей от других плагинов;
- Еще здесь можно указывать версию IDEA, с которой ваш плагин будет корректно работать
- Также здесь описывают Actions.
Actions
Что такое Actions? Предположим, вы открыли меню для создания нового файла. На самом деле, каждый элемент этого меню был добавлен каким-то плагином:
Actions – это точки входа в ваш плагин для пользователей. Каждый раз, когда пользователь нажимает на пункт меню, вы получаете управление внутри плагина, можете отреагировать на это нажатие и сделать то, что необходимо.
Как создаются Actions? Давайте напишем простой Action, который будет показывать диалог с сообщением "Hello, World".
class OpenHelloWorldAction : AnAction() {
override fun actionPerformed(actionEvent: AnActionEvent) {
val project = actionEvent.project
Messages.showMessageDialog(
project,
"Hello world!",
"Greeting",
Messages.getInformationIcon()
)
}
override fun update(e: AnActionEvent) {
super.update(e)
// TODO - Here we can update our action (for example, disable it)
}
override fun beforeActionPerformedUpdate(e: AnActionEvent) {
super.beforeActionPerformedUpdate(e)
// TODO - This method calls right before 'actionPerformed'
}
}
Для создания Action-а мы, во-первых, создаем класс, который наследуется от класса AnAction. Во-вторых, мы должны переопределить метод actionPerformed, куда приходит специальный параметр класса AnActionEvent. Этот параметр содержит в себе данные о контексте выполнения вашего Action-а. Под контекстом понимается проект, в котором вы работаете, файл, который сейчас открыт у пользователя в редакторе кода, выбранные в дереве проекта элементы и другие данные, которые могут помочь в обработке ваших задач.
Чтобы показать "Hello, world"-диалог, мы сначала получаем проект (как раз из параметра AnActionEvent), а затем используем утилитный класс Messages для показа диалогового окна.
Какие дополнительные возможности внутри Action-а у нас есть? Мы можем переопределить два метода: update и beforeActionPerformedUpdate.
Метод update вызывается каждый раз, когда меняется контекст выполнения вашего Action-а. Почему он может вам пригодиться: например, для обновления пункта меню, который был добавлен вашим плагином. Пусть вы написали Action, который может работать только с Kotlin-овскими файлами, а пользователь сейчас открыл Groovy-файл. Тогда в методе update вы можете сделать ваш action недоступным.
Метод beforeActionPerformedUpdate похож на метод update, но вызывается прямо перед actionPerformed. Это последняя возможность повлиять на ваш Action. Документация рекомендует не выполнять в этом методе ничего “тяжелого”, чтобы он выполнялся, как можно скорее.
Еще Actions можно привязывать к определенным элементам интерфейса IDEA и задавать им дефолтные комбинации клавиш для вызова – подробнее про это рекомендую почитать вот тут.
Разработка UI в плагинах
Если вам нужен свой собственный дизайн диалогов, придется потрудиться. Мы разрабатывали свой UI, потому что хотели иметь удобный графический интерфейс, в котором можно было бы отметить несколько галочек, иметь селектор для значений enum-а и прочее.
Plugin DevKit для разработки UI добавляет несколько action-ов, таких как GUI form и Dialog. Первый создает нам пустую форму, второй – форму с двумя кнопками: Ok и Cancel.
Окей, дизайнер форм есть, но он… так себе. По сравнению с ним даже Layout designer в Android Studio выглядит удобным и хорошим. Весь UI разрабатывается на такой библиотеке, как Java Swing. Этот дизайнер форм генерирует человекочитаемый XML-файл. Если у вас не получается что-то сделать в дизайнере форм (пример: вставить несколько контролов в одну и ту же ячейку сетки и скрыть все контролы, кроме одного), нужно идти в этот файл и менять его – IDEA подцепит эти изменения.
Почти каждая форма состоит из двух файлов: первый имеет расширение .form, это как раз и есть XML-файл, второй – это так называемый Bound class, который можно писать на Java, Kotlin, да на чем хотите. Он выступает в роли контроллера формы. Неожиданно, но писать на Java его гораздо проще, чем на других языках. Потому что, например, тулинг для Kotlin-а пока не такой совершенный. При добавлении новых компонентов в работе с Java-классом эти компоненты автоматически добавляются в класс, а при изменении имени компонента в дизайнере, оно подтягивается автоматом. Зато в случае с Kotlin компоненты не добавляются – никакой интеграции не происходит, можно что-нибудь забыть и не понимать, почему ничего не работает.
Резюмируем основы
- Для создания плагина потребуется: IDEA Community Edition, подключенный к ней Plugin DevKit и Java.
- gradle-intellij-plugin – ваш бро, он сильно упростит вам жизнь, рекомендую его использовать.
- Не пишите собственный UI, если нет необходимости. В IDEA есть много утилитных классов, которые позволяют из коробки создать свой собственный UI. Если вам нужно что-то сложное – готовьтесь потрудиться.
- В плагине может быть сколько угодно Action-ов. Один и тот же плагин может добавлять много функциональности вашей IDEA.
Внутренности IDEA: компоненты, PSI
Поговорим про кишочки IDEA, про то, как она устроена внутри. Рассказываю для того, чтобы у вас в голове ничего не рассыпалось, когда я буду объяснять практическую часть, и чтобы вы понимали, откуда что берется.
Как устроена IDEA? На первом уровне иерархии стоит такой класс, как Application. Это отдельный инстанс IDEA. На каждый инстанс IDEA создается один объект класса Application. Например, если вы запускаете одновременно AppCode, Intellij IDEA, Android Studio, у вас получатся три отдельных инстанса класса Application. Этот класс предназначен для обработки потока ввода-вывода.
Следующий уровень – класс Project. Это наиболее близкое понятие к тому, что вы видите, открывая новый проект в IDEA. Обычно Project нужен для получения других компонентов внутри IDEA: утилитных классов, менеджеров и многого другого.
Следующий уровень детализации – класс Module. В общем случае, модуль – это иерархия классов, сгруппированных в одну папку. Но здесь под модулями мы понимаем Maven-модули, Gradle-модули. Этот класс нужен, во-первых, для определения зависимостей между модулями, во-вторых – для поиска классов внутри этих модулей.
Следующий уровень детализации – класс VirtualFile. Это абстракция над реальным файлом, который лежит у вас на диске. Каждому реальному файлу может соответствовать несколько инстансов VirtualFile, но все они равны между собой. При этом, если реальный файл удален, то VirtualFile не удалится самостоятельно, а просто станет невалидным.
С каждым VirtualFile связана такая сущность, как Document. Она представляет собой абстракцию над текстом вашего файла. Document нужен, чтобы вы могли отслеживать события, связанные с изменением текста файла: пользователь вставил строчку, удалил строчку, и т.д., и т.п.
Чуть сбоку от этой иерархии стоит класс Editor – это редактор кода. У каждого проекта может быть один Editor. Он нужен, чтобы вы могли отслеживать события, связанные с редактором кода: пользователь выделил строчку, где стоит каретка, и так далее.
Последняя вещь, о которой я хотел поговорить – это PsiFile. Это тоже абстракция над реальными файлами, но с точки зрения представления элементов кода. PSI расшифровывается как Program Structure Interface – интерфейс структуры программы.
А из чего состоит каждая программа? Рассмотрим обычный Java-класс.
package com.experiment;
import javax.inject.Inject;
class SomeClass {
@Inject
String injectedString;
public void someMethod() {
System.out.println(injectedString);
}
}
Он состоит из указания пакета, импортов, классов, полей, методов, аннотаций, ключевых слов, типов данных, модификаторов, идентификаторов, ссылок на методы, выражений и токенов. И для каждого элемента существует своя абстракция PsiElement-ов. То есть каждая ваша программа состоит из PsiElement-ов.
В свою очередь, PsiFile представляет собой древовидную структуру, где у каждого элемента может быть родитель и множество потомков.
Хочется упомянуть, что PSI не равняется абстрактному синтаксическому дереву. Абстрактное синтаксическое дерево – это дерево представления вашей программы после прохождения парсером вашей программы, и оно отвязано от какого-либо языка программирования. PSI же, наоборот, привязан к конкретному языку программирования. Когда вы работаете с Java-классом, то имеете дело с джавовыми PsiElement-ами. Когда работаете с Groovy-классом — с PsiElement-ами языка Groovy, и так далее. Это важно учитывать, потому что если вы попытаетесь работать с неправильными PSI-элементами внутри какого-то языка программирования, то плагин будет работать не так, как вы хотите, – будет стрелять и крашиться.
Почему еще важно знать про PSI – потому что PSI-структурой пронизана вся IDEA. Если при работе с кодом существующих программ вы не учитываете эту структуру, попробуете работать в обход, то ваш плагин будет работать совсем не так, как вы ожидаете. Про это я еще отдельно упомяну в практической части.
Резюмируем часть про внутренности IDEA
- PSI нужен для представления программы внутри IDEA;
- PSI-структурой пронизана вся IDEA, о ней знают все плагины и вы тоже должны про нее знать;
- Для каждого языка программирования существует своя собственная коллекция PsiElement-ов.
На этом с теорией я закончу, впереди вас ждет практическая часть. Чуть ниже я прикрепил список полезных и интересных ссылок на тему плагиностроения.
Дополнительные материалы по плагиностроению
- Про создание плагина для учета времени
- Начало большого цикла статей, дополняющего документацию по плагинам
- Создание собственного Tool window
- Про расширение функциональности редактора кода
- Маленький пример создания собственного плагина
- Про добавление нового типа файла в IntelliJ IDEA
- Про добавление возможности навигации по файлам внутри IDEA
- Разработка своей ленты с картинками внутри IntelliJ IDEA
- С помощью плагинов, кстати, можно встроиться в меню создания новых модулей. Советую просмотреть всю презентацию, там показывается много интересных вещей.
- Видео с доклада на Droidcon Italy 2017, в котором, помимо всего прочего, рассказывают, как можно из плагина создать диалог из FreeMarker-ного шаблона. У меня не получилось сходу повторить опыт докладчиков, но может быть вам повезет больше.
- Недавний доклад с KotlinConf, в котором ребята из Square рассказывают о доработках своего плагина для SQLDelight, которые позволили им поддержать работу и с Java, и с Kotlin.