Жизнь с многомодульным проектом не так уж проста. Чтобы избежать рутины создания нового модуля мы создали собственный плагин для Android Studio. В процессе реализации мы столкнулись с отсутствием практической документации, перепробовали несколько подходов и откопали множество подводных камней. Получилось две статьи: “Теория” и “Практика”. Встречайте!


image


О чем буду говорить?


  • Зачем плагин? Почему плагин?
    • Составление чек-листа
    • Варианты автоматизации чек-листа
  • Основы разработки плагинов
    • Actions
    • Разработка UI в плагинах
    • Выводы
  • Внутренности IDEA: компоненты, PSI
    • Внутреннее устройство IDEA
    • PSI
    • Выводы

Зачем плагин? Почему плагин?


Если вы разрабатываете многомодульный Android-проект, то знаете, какая это рутина – каждый раз создавать новый модуль. Вам нужно создать модуль, настроить в нем Gradle, добавить зависимости, дождаться синхронизации, не забыть что-то исправить в application-модуле – на все это уходит очень много времени. Нам захотелось автоматизировать рутину, и мы начали с составления чек-листа того, что делаем каждый раз, когда создаем новый модуль.


1. Во-первых, мы создаем сам модуль через меню File -> New -> New module -> Android library.


image


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.


Меняем константы в 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.


Подключаем плагины Kotlin-а
// 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, чтобы он понимал, где брать фабрики для создаваемых объектов:


Настраиваем annotation processor для Toothpick
// 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-а:


Настраиваем kapt для Moxy
// Feature module build.gradle

android {
    ...

    kapt {
        arguments {
            arg("moxyReflectorPackage", "ru.hh.feature_worknear")
        }
    }

    ...

Примечание: мы уже перешли на новую версию Moxy, и эта часть кодогенерации потеряла свою актуальность.


8. Генерируем кучу новых файлов. Имею ввиду не те файлы, которые создаются автоматически (AndroidManifest.xml, build.gradle, .gitignore), а общий каркас нового модуля: интеракторы, репозитории, DI-модули, презентеры, фрагменты. Этих файлов очень много, они в начале имеют одну и ту же структуру, и создавать их – рутина.


image


9. Подключаем наш созданный модуль к application-модулю. В этом шаге нужно не забыть донастроить Toothpick в build.gradle файле application-модуля. Для этого мы дописываем пакет созданного модуля в специальный аргумент annotation processor-а – toothpick_registry_children_package_names.


Донастраиваем Toothpick
// 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 – туда мы добавляем название пакета созданного модуля:


Настраиваем MoxyReflectorStub
// App module file

@RegisterMoxyReflectorPackages(
        "ru.hh.feature_force_update",
        "ru.hh.feature_profile",
        "ru.hh.feature_worknear"
        ...
) class MoxyReflectorStub

И, в конце концов, не забываем подключить созданный модуль в блок dependences application-модуля:


Добавление dependencies
// 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 просто крашится.


image


Вторым вариантом автоматизации чек-листа мы рассматривали движок шаблонов FreeMarker. После неудачной попытки копипаста мы решили посмотреть на шаблоны модулей повнимательнее и обнаружили под капотом FreeMarker-овские шаблоны.


Что такое FreeMarker подробно рассказывать не буду – есть хорошая статья от RedMadRobot и видео с MosDroid от Леши Быкова. Но если кратко – это движок для генерации файлов при помощи шаблонов и специальной Map-ки java-объектов. Вы подаете на вход шаблоны, объекты, и FreeMarker на выходе генерирует код.


Но посмотрим еще раз на чек-лист:


image


Если приглядеться, то можно заметить, что он делится на две большие группы задач:


  • Задачи генерации нового кода (1, 3, 4, 5, 6, 7, 8) и
  • Задачи модификации существующего кода (2, 7, 8, 9)

И если с задачами из первой группы FreeMarker справляется на ура, то со второй не справляется совсем. В качестве небольшого примера: в текущей реализации интеграции FreeMarker в Android Studio при попытке вставить в файл settings.gradle строчку, которая не начинается со слова 'include', студия будет крашиться. Тут мы поймали грустного, и решили отказаться от использования FreeMarker.


После неудачи с FreeMarker-ом возникла идея написать свою консольную утилиту для выполнения чек-листа. Внутри Intellij IDEA есть возможность использовать терминал, так почему бы и нет? Напишем скрипт на баше, всего делов:


image


Но коль скоро мы хотим иметь возможность гибко настраивать создаваемый модуль, нам придется вводить множество различных флажков, которые будет не очень удобно печатать в консоли.


После этого мы сделали как бы шаг назад и вспомнили, что мы работаем внутри Intellij IDEA. А как она устроена? Есть некоторое ядро классов, движок, к которому прикреплено множество плагинов, которые добавляют нужную нам функциональность.


Кто из вас видит на скриншоте больше, чем два подключенных плагина?


image


Куда смотреть-то?..

image


Здесь их подключено как минимум три. Если вы работаете с Kotlin, то у вас включен Kotlin-плагин. Если работаете в проекте с Gradle, то включен и Gradle-плагин. Если работаете в проекте с системой контроля версий – Git, SVN или еще что-нибудь, – у вас включен соответствующий плагин для интеграции этой VCS.


Мы посмотрели в официальный репозиторий плагинов JetBrains, и оказалось, что официально зарегистрированных плагинов уже более 4000! Почти весь мир пишет плагины, и эти плагины могут делать все, что угодно: начиная от интеграции в IDEA какого-либо языка программирования и заканчивая специфичными тулзами, которые могут быть запущены изнутри IDEA.


Короче, мы решили написать свой плагин.


Основы разработки плагинов


Переходим к основам разработки плагинов. Для начала вам потребуются всего три вещи:


  1. IntelliJ IDEA, минимум Community Edition (Можно работать и в Ultimate-версии, но особых преимуществ при разработке плагинов, это не даст);
  2. Подключенный к ней Plugin DevKit – это специальный плагин, который добавляет возможность писать другие плагины;
  3. И любой JVM-ный язык, на котором вы хотите писать плагин. Это может быть Kotlin, Java, Groovy – что угодно.

Начинаем с создания проекта плагина. Выбираем New project, пункт Gradle, отмечаем галочкой IntelliJ Platform Plugin и создаем проект.


image


Примечание: Если вы не видите галочки IntelliJ Platform Plugin – это означает, что у вас не установлен Plugin DevKit.


После заполнения необходимых полей мы увидим пустую структуру плагина.


image


Давайте посмотрим на него внимательней. Он состоит из:


  • Папок, в которых вы будете писать код будущего проекта; (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.


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? Предположим, вы открыли меню для создания нового файла. На самом деле, каждый элемент этого меню был добавлен каким-то плагином:


image


Actions – это точки входа в ваш плагин для пользователей. Каждый раз, когда пользователь нажимает на пункт меню, вы получаете управление внутри плагина, можете отреагировать на это нажатие и сделать то, что необходимо.
Как создаются Actions? Давайте напишем простой Action, который будет показывать диалог с сообщением "Hello, World".


OpenHelloWorldAction
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.


image


Окей, дизайнер форм есть, но он… так себе. По сравнению с ним даже 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. Этот класс предназначен для обработки потока ввода-вывода.


Место Application в иерархии

image


Следующий уровень – класс Project. Это наиболее близкое понятие к тому, что вы видите, открывая новый проект в IDEA. Обычно Project нужен для получения других компонентов внутри IDEA: утилитных классов, менеджеров и многого другого.


Место Project в иерархии

image


Следующий уровень детализации – класс Module. В общем случае, модуль – это иерархия классов, сгруппированных в одну папку. Но здесь под модулями мы понимаем Maven-модули, Gradle-модули. Этот класс нужен, во-первых, для определения зависимостей между модулями, во-вторых – для поиска классов внутри этих модулей.


Место Module в иерархии

image


Следующий уровень детализации – класс VirtualFile. Это абстракция над реальным файлом, который лежит у вас на диске. Каждому реальному файлу может соответствовать несколько инстансов VirtualFile, но все они равны между собой. При этом, если реальный файл удален, то VirtualFile не удалится самостоятельно, а просто станет невалидным.


Место VirtualFile в иерархии

image


С каждым VirtualFile связана такая сущность, как Document. Она представляет собой абстракцию над текстом вашего файла. Document нужен, чтобы вы могли отслеживать события, связанные с изменением текста файла: пользователь вставил строчку, удалил строчку, и т.д., и т.п.


Место Document в иерархии

image


Чуть сбоку от этой иерархии стоит класс Editor – это редактор кода. У каждого проекта может быть один Editor. Он нужен, чтобы вы могли отслеживать события, связанные с редактором кода: пользователь выделил строчку, где стоит каретка, и так далее.


Место Editor в иерархии

image


Последняя вещь, о которой я хотел поговорить – это PsiFile. Это тоже абстракция над реальными файлами, но с точки зрения представления элементов кода. PSI расшифровывается как Program Structure Interface – интерфейс структуры программы.


Место PsiFile в иерархии

image


А из чего состоит каждая программа? Рассмотрим обычный Java-класс.


Обычный Java-класс
package com.experiment; 

import javax.inject.Inject;

class SomeClass {

    @Inject 
    String injectedString;

    public void someMethod() {
        System.out.println(injectedString);
    }
} 

Он состоит из указания пакета, импортов, классов, полей, методов, аннотаций, ключевых слов, типов данных, модификаторов, идентификаторов, ссылок на методы, выражений и токенов. И для каждого элемента существует своя абстракция PsiElement-ов. То есть каждая ваша программа состоит из PsiElement-ов.


image


В свою очередь, PsiFile представляет собой древовидную структуру, где у каждого элемента может быть родитель и множество потомков.


image


Хочется упомянуть, что PSI не равняется абстрактному синтаксическому дереву. Абстрактное синтаксическое дерево – это дерево представления вашей программы после прохождения парсером вашей программы, и оно отвязано от какого-либо языка программирования. PSI же, наоборот, привязан к конкретному языку программирования. Когда вы работаете с Java-классом, то имеете дело с джавовыми PsiElement-ами. Когда работаете с Groovy-классом — с PsiElement-ами языка Groovy, и так далее. Это важно учитывать, потому что если вы попытаетесь работать с неправильными PSI-элементами внутри какого-то языка программирования, то плагин будет работать не так, как вы хотите, – будет стрелять и крашиться.


Почему еще важно знать про PSI – потому что PSI-структурой пронизана вся IDEA. Если при работе с кодом существующих программ вы не учитываете эту структуру, попробуете работать в обход, то ваш плагин будет работать совсем не так, как вы ожидаете. Про это я еще отдельно упомяну в практической части.


Резюмируем часть про внутренности IDEA


  • PSI нужен для представления программы внутри IDEA;
  • PSI-структурой пронизана вся IDEA, о ней знают все плагины и вы тоже должны про нее знать;
  • Для каждого языка программирования существует своя собственная коллекция PsiElement-ов.

На этом с теорией я закончу, впереди вас ждет практическая часть. Чуть ниже я прикрепил список полезных и интересных ссылок на тему плагиностроения.


Дополнительные материалы по плагиностроению


Список статей

Комментарии (7)


  1. maxzh83
    14.08.2019 13:30
    +1

    Пытался разобраться как сделать свое окно, но не tool window (про это есть информация), а на вкладке где открытые документы. Примерно как это делает плагин Database, когда открываешь таблицу. Но нигде внятного примера найти не получилось.


    1. Ztrel Автор
      14.08.2019 13:49

      Привет! К сожалению, готового ответа, как это сделать, у меня нет. Мы в hh.ru такого еще не делали. Но я бы предложил начал свой ресерч с исходников плагина Database Navigator (исходники).

      Я могу попробовать изучить вопрос, когда выдастся свободное время, вдруг что-то накопаю.


      1. maxzh83
        14.08.2019 13:59

        Я могу попробовать изучить вопрос, когда выдастся свободное время, вдруг что-то накопаю.

        Спасибо, но сейчас уже не актуально. По моему, я натыкался на эти исходники, там конечно много всего. Кажется, они эмулировали открытие фейкового документа, что мне не понравилось, но не уверен. Было это давно, потом стало не актуально.


        1. Ztrel Автор
          14.08.2019 14:11

          Окей. Могу только согласиться, что официальной документации по плагинам решительно не хватает для создания чего-то по-настоящему классного, остается изучать чужие плагины и исходники самой IDEA.


          1. maxzh83
            14.08.2019 14:16

            Да, документации мало. С учетом анонса маркетплейса для плагинов, этого прямо не хватает.


            1. MikhailVink
              14.08.2019 16:43

              Активно работаем над документацией сейчас, и буквально недавно начали искать Developer Advocate for IntelliJ Platform для помощи с координацией усилий и продвижением платформы. Если знаете кого можно позвать — будем рады рекоммендациям!


              1. Ztrel Автор
                15.08.2019 09:32

                Это классная новость =) Надо, кстати, отметить, что документация действительно улучшилась за последние полгода.