Хабрахабр

Фантастические плагины, vol. 1. Теория

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

image

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

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

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

image

Прописываем пути к модулю в файле settings.gradle, потому что у нас есть несколько видов модулей – core-модули и feature-модули, которые лежат в разных папках. 2.

Прописываем пути к модулям

// 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')

Меняем константы compileSdk, minSdk, targetSdk в сгенерированном build.gradle: заменяем их на константы, которые определены в рутовом build.gradle. 3.

Меняем константы в build.gradle нового модуля

// Feature module build.gradle

android
}

Примечание: эту часть работы мы недавно вынесли в наш Gradle-плагин, который помогает в несколько строк настроить все необходимые параметры build.gradle файла.

Поскольку весь новый код пишем на Kotlin, стандартно подключаем два плагина: kotlin-android и kotlin-kapt. 4. Если модуль каким-то образом связан с 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'

Подключаем общие библиотеки и core-модули. 5. 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
}

Настраиваем kapt для Toothpick-а. 6. Скорее всего вам известно: чтобы в релизной сборке использовать не рефлексию, а кодогенерацию, вам нужно донастроить annotation processor, чтобы он понимал, где брать фабрики для создаваемых объектов: Toothpick – это наш основной DI-фреймворк.

Настраиваем annotation processor для Toothpick

// Feature module build.gradle defaultConfig { ... javaCompileOptions { annotationProcessorOptions { arguments = [ toothpick_registry_package_name: "ru.hh.feature_worknear" ] } } ...

Примечание: в hh.ru мы используем первую версию Toothpick, во второй убрали возможность использовать кодогенерацию.

Донастраиваем kapt для Moxy внутри созданного модуля. 7. В частности, прописать пакет созданного модуля в аргументы kapt-а: Moxy – наш основной фреймворк для создания MVP в приложении, и его нужно немножко докручивать, чтобы он мог работать в многомодульном проекте.

Настраиваем kapt для Moxy

// Feature module build.gradle android { ... kapt { arguments { arg("moxyReflectorPackage", "ru.hh.feature_worknear") } } ...

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

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

image

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

Донастраиваем 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(",") ] } } …

У нас есть класс, который отмечен аннотацией @RegisterMoxyReflectorPackages – туда мы добавляем название пакета созданного модуля: После этого донастраиваем Moxy в application-модуле.

Настраиваем 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') ...

У нас получился чек-лист из девяти пунктов.

А после этого часами гадать, что же произошло и почему проект не собирается. Поскольку пунктов много, велика вероятность что-то забыть.

Мы решили, что так жить нельзя и нужно что-то менять.

Варианты автоматизации чек-листа

После составления чек-листа мы начали искать варианты автоматизации его пунктов.

Мы попробовали найти реализацию создания модуля Android Library, которая доступна нам “из коробки”. Первым вариантом стала попытка сделать “Ctrl+C, Ctrl+V”. Мы попробовали скопировать шаблон NewAndroidModule, поменяв id внутри файла template.xml.ftl. В папке с Android Studio (для MacOs: /Applications/Android\ Studio.app/Contents/plugins/android/lib/templates/gradle-projects/) можно отыскать специальную папку с шаблонами тех проектов, которые вы видите при выборе пункта File -> New -> New Module. При попытке взять и добавить, удалить или изменить какой-то элемент, Android Studio просто крашится. После чего запустили IDE, начали создавать новый модуль, и… Android Studio крашнулась, потому что список модулей, которые вы видите в меню создания нового модуля, жестко захардкожен, примитивным копи-пастом его изменить нельзя.

image

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

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

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

image

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

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

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

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

image

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

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

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

image

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

image

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

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

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

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

  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.

Это удобно, потому что почти каждый Android-разработчик знаком с Gradle и умеет с ним работать. gradle-intellij-plugin – Gradle-овый плагин, который позволяет использовать 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' } }

Во-вторых, мы должны переопределить метод actionPerformed, куда приходит специальный параметр класса AnActionEvent. Для создания Action-а мы, во-первых, создаем класс, который наследуется от класса AnAction. Под контекстом понимается проект, в котором вы работаете, файл, который сейчас открыт у пользователя в редакторе кода, выбранные в дереве проекта элементы и другие данные, которые могут помочь в обработке ваших задач. Этот параметр содержит в себе данные о контексте выполнения вашего Action-а.

Чтобы показать "Hello, world"-диалог, мы сначала получаем проект (как раз из параметра AnActionEvent), а затем используем утилитный класс Messages для показа диалогового окна.

Мы можем переопределить два метода: update и beforeActionPerformedUpdate. Какие дополнительные возможности внутри Action-а у нас есть?

Почему он может вам пригодиться: например, для обновления пункта меню, который был добавлен вашим плагином. Метод update вызывается каждый раз, когда меняется контекст выполнения вашего Action-а. Тогда в методе update вы можете сделать ваш action недоступным. Пусть вы написали Action, который может работать только с Kotlin-овскими файлами, а пользователь сейчас открыл Groovy-файл.

Это последняя возможность повлиять на ваш Action. Метод beforeActionPerformedUpdate похож на метод update, но вызывается прямо перед actionPerformed. Документация рекомендует не выполнять в этом методе ничего “тяжелого”, чтобы он выполнялся, как можно скорее.

Еще Actions можно привязывать к определенным элементам интерфейса IDEA и задавать им дефолтные комбинации клавиш для вызова – подробнее про это рекомендую почитать вот тут.

Разработка UI в плагинах

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

Первый создает нам пустую форму, второй – форму с двумя кнопками: Ok и Cancel. Plugin DevKit для разработки UI добавляет несколько action-ов, таких как GUI form и Dialog.

image

По сравнению с ним даже Layout designer в Android Studio выглядит удобным и хорошим. Окей, дизайнер форм есть, но он… так себе. Этот дизайнер форм генерирует человекочитаемый XML-файл. Весь UI разрабатывается на такой библиотеке, как Java Swing. Если у вас не получается что-то сделать в дизайнере форм (пример: вставить несколько контролов в одну и ту же ячейку сетки и скрыть все контролы, кроме одного), нужно идти в этот файл и менять его – IDEA подцепит эти изменения.

Он выступает в роли контроллера формы. Почти каждая форма состоит из двух файлов: первый имеет расширение .form, это как раз и есть XML-файл, второй – это так называемый Bound class, который можно писать на Java, Kotlin, да на чем хотите. Потому что, например, тулинг для Kotlin-а пока не такой совершенный. Неожиданно, но писать на Java его гораздо проще, чем на других языках. Зато в случае с Kotlin компоненты не добавляются – никакой интеграции не происходит, можно что-нибудь забыть и не понимать, почему ничего не работает. При добавлении новых компонентов в работе с Java-классом эти компоненты автоматически добавляются в класс, а при изменении имени компонента в дизайнере, оно подтягивается автоматом.

Резюмируем основы

  • Для создания плагина потребуется: IDEA Community Edition, подключенный к ней Plugin DevKit и Java.
  • gradle-intellij-plugin – ваш бро, он сильно упростит вам жизнь, рекомендую его использовать.
  • Не пишите собственный UI, если нет необходимости. В IDEA есть много утилитных классов, которые позволяют из коробки создать свой собственный UI. Если вам нужно что-то сложное – готовьтесь потрудиться.
  • В плагине может быть сколько угодно Action-ов. Один и тот же плагин может добавлять много функциональности вашей IDEA.

Рассказываю для того, чтобы у вас в голове ничего не рассыпалось, когда я буду объяснять практическую часть, и чтобы вы понимали, откуда что берется. Поговорим про кишочки IDEA, про то, как она устроена внутри.

На первом уровне иерархии стоит такой класс, как Application. Как устроена IDEA? На каждый инстанс IDEA создается один объект класса Application. Это отдельный инстанс IDEA. Этот класс предназначен для обработки потока ввода-вывода. Например, если вы запускаете одновременно AppCode, Intellij IDEA, Android Studio, у вас получатся три отдельных инстанса класса Application.

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

image

Это наиболее близкое понятие к тому, что вы видите, открывая новый проект в IDEA. Следующий уровень – класс Project. Обычно 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 не равняется абстрактному синтаксическому дереву. Когда вы работаете с Java-классом, то имеете дело с джавовыми PsiElement-ами. PSI же, наоборот, привязан к конкретному языку программирования. Это важно учитывать, потому что если вы попытаетесь работать с неправильными PSI-элементами внутри какого-то языка программирования, то плагин будет работать не так, как вы хотите, – будет стрелять и крашиться. Когда работаете с Groovy-классом — с PsiElement-ами языка Groovy, и так далее.

Если при работе с кодом существующих программ вы не учитываете эту структуру, попробуете работать в обход, то ваш плагин будет работать совсем не так, как вы ожидаете. Почему еще важно знать про 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.
Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть