Moxy Strategy plugin

    Иногда простые вещи очень утомляют, особенно когда их необходимо делать постоянно. Одна из таких вещей при работе с фреймворком Moxy — это добавление стратегий к функциям. Для ускорения этого процесса был написан плагин, который по "alt+enter" предоставляет выбор стратегии если ее нет или диалог с заменой на другую стратегию. Те, кто хочет узнать как это работает, добро пожаловать под кат.



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


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


    Inspection


    Итак, мы написали функцию и хотим добавить стратегию. В этот момент нам на помощь приходит code inspections. Они анализируют код текущего файла при помощи PSI (Program Structure Interface), и если что то не так, подсвечивают код красным или серым, и по alt+enter предлагают возможные исправления.
    Официальная документация по разработке code inspections.
    PSI — это структура, в которой у каждого выражения, ключевого слова и т.д. есть свой аналог, который содержит информацию о нем, его parent и child. Для того, чтобы понять какой класс соответствует необходимой конструкции языка, есть хороший плагин Psi viewer plugin.


    Рассмотрим для примера с помощью него структуру интерфейса с функцией.



    Видим, что интересующий нас класс это KtNamedFunction.


    Рассмотрим создание Inpection.


    Как создавать проект и не только хорошо расписано в цикле статей. Дополнительно понадобятся зависимости в файле plugins.xml


        <depends>org.jetbrains.kotlin</depends>
        <depends>com.intellij.modules.lang</depends>

    и в файле build.gradle


    dependencies {
        implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8"
        implementation "org.jetbrains.kotlin:kotlin-reflect"
        implementation "com.github.moxy-community:moxy:1.0.13"
    }
    
    intellij {
     ...
        plugins = ["Kotlin"]
     ...
    }

    Перейдем к созданию inpection.


    Для начала необходимо наследовать AbstractKotlinInspection


    class MvpViewStrategyInspection : AbstractKotlinInspection() {}

    В нем нам понадобится переопределить следующую функцию:


    override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean): PsiElementVisitor 

    В ней мы создаем Visitor, который будет анализировать текущий код файла и добавлять ошибки и предупреждения.


    Хорошей документации по Visitor я не нашел, но когда смотрел плагины kotlin наткнулся на очень удобные функции-фабрики для создания Visitor. Для анализа KtNamedFunction есть следующая фабрика:


    org.jetbrains.kotlin.psi VisitorWrappersKt.class 
    public fun namedFunctionVisitor(block: (KtNamedFunction) → Unit): KtVisitorVoid

    В лямбде нужно проанализировать PSI: выбрана ли функция без аннотации и является ли она частью интерфейса, который реализует интерфейс MvpView. Когда все условия выполнены, регистрируем ошибку с помощью ProblemsHolder.
    В итоге выглядит это вот так:


    private val fixes = MoxyStrategy.values().map { AddStrategyFix(it) }.toTypedArray()
    
    override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean): PsiElementVisitor {
            return namedFunctionVisitor { ktNamedFunction ->
                if (!ktNamedFunction.isClassInheritMvpView() ||
                    ktNamedFunction.isHasMoxyAnnotation()
                ) return@namedFunctionVisitor
                holder.registerProblem(
                    ktNamedFunction, // psiElement
                    StrategyIntentionType.MissingStrategy.title,  // description
                    *fixes // LocalQuickFix
                )
            }
        }

    Что такое AddStrategyFix? Это объект, который наследует LocalQuickFix. Он отвечает за исправления кода и выполнится, когда будет выбран из предложенных пунктов:


    class AddStrategyFix(
            private val strategy: MoxyStrategy
        ) : LocalQuickFix {
            override fun getFamilyName(): String = "add ${strategy.className}"
    
            override fun applyFix(project: Project, descriptor: ProblemDescriptor) {
                val ktFunction = (descriptor.psiElement as KtNamedFunction)
                val editor = ktFunction.getProjectEditor()
                ktFunction.addStrategyAnnotation(strategy, project, editor)
            }
        }

    getFamilyName — определяет название пункта, который будет в диалоге.
    В applyFix очевидно нужно выполнить исправление. В нашем случае добавить аннотацию.


    Осталось зарегистрировать Inspection в plguin.xml, (похоже Activity в Manifest)


     <extensions defaultExtensionNs="com.intellij">
            <localInspection language="kotlin"
                             displayName="missing strategy for function"
                             groupPath="Moxy"
                             groupBundle="messages.InspectionsBundle"
                             groupKey="group.names.probable.bugs"
                             enabledByDefault="true"
                             level="ERROR"
                             implementationClass="com.maksimnovikov.inspection.MvpViewStrategyInspection"/>
        </extensions>

    В итоге получаем:



    Так же автоматически добавляются импорты аннотации StateStrategyType и выбранной стратегии.


    Для стратегии AddToEndSingleTagStrategy дополнительно сделал добавление второго аргумента
    tag и перенос курсора на него.



    Intention


    Функцию написали, стратегию быстро добавили. Передумали и решили заменить ее на другую.
    Теперь наш помощник это Intention.
    Принцип работы аналогичен с Inspection, но без какого либо выделения кода и анализируется только текущий выбранный элемент под кареткой. Как использовать уже имеющиеся intention. Официальная документация по разработке.


    Рассмотрим создание Intention.


    Необходимо наследовать PsiElementBaseIntentionAction :


    class MvpViewStrategyIntention : PsiElementBaseIntentionAction(){
        override fun getText(): String 
        override fun getFamilyName(): String
        override fun isAvailable(project: Project, editor: Editor?, element: PsiElement): Boolean
        override fun invoke(project: Project, editor: Editor?, element: PsiElement)
    }

    getText — определяет отображаемое название
    getFamilyName — нужен для иерархии intention и отображения в настройках
    isAvailable — в этой функции исходя из текущего элемента под курсором, необходимо определить будет ли добавлен intention в текущий список или нет.
    invoke — вызывается, когда мы выбрали данный intention из списка


    Рассмотрим подробнее isAvailable и invoke:


    isAvailable


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


    override fun isAvailable(project: Project, editor: Editor?, element: PsiElement): Boolean {
            if ((element.containingFile ?: return false) !is KtFile) return false
            if (!element.isClassInheritMvpView()) return false
            val ktFunction = element.getParentOfType<KtNamedFunction>() ?: return false
            return ktFunction.isHasMoxyAnnotation()
        }

    invoke


    Поскольку добавление отдельного Intention для каждой стратегии не выгодно по производительности, необходимо показать еще один диалог по нажатию на один пункт.
    Под капотом наша любимая IDE использует swing. Соответственно, кто хорошо с ним знаком, может создавать разнообразный интерфейс. Я пошел другим путем и использовал простой билдер для диалога выбора — JBPopupFactory


    fun <T> Editor.showSelectPopup(
        items: List<T>,
        onSelected: (T) -> Unit
    ) {
        JBPopupFactory.getInstance()
            .createPopupChooserBuilder(items)
            .setRequestFocus(true)
            .setCancelOnClickOutside(true)
            .setItemChosenCallback { onSelected(it) }
            .createPopup()
            .showInBestPositionFor(this)
    }

    Однако, после создания диалога контекст сменяется на другой, который уже не может изменять код и при попытки сделать это выдает ошибку. Чтобы это исправить дополнительно оборачиваем действие по замене стратегии в функцию WriteCommandAction.runWriteCommandAction(project) {}


      override fun invoke(project: Project, editor: Editor, element: PsiElement) {
            editor.showSelectPopup(MoxyStrategy.values().toList()) { selectedStrategy ->
                WriteCommandAction.runWriteCommandAction(project) {
                    val ktFunction = element.getParentOfType<KtNamedFunction>() ?: return@runWriteCommandAction
                    ktFunction.replaceAnnotation(selectedStrategy, project, editor)
                }
            }
        }

    Так же регистрируем intention в plugin.xml


    <extensions defaultExtensionNs="com.intellij">
            <intentionAction>
                <className>com.maksimnovikov.intention.MvpViewStrategyIntention</className>
                <category>Moxy intentions</category>
            </intentionAction>
    </extensions>

    В итоге получаем следующий функционал:




    Итого


    С этим плагином решается проблема запоминания названий стандартных стратегий. Их список всегда виден. Если принцип работы стратегии поначалу может быть не понятен, возможно вставить её и перейти к исходному коду, где есть документация. Также в общем улучшается удобство работы со стратегиями Moxy.


    Создание плагинов раньше для меня казалось очень сложным и непонятным. В основном из-за отсутствия примеров и недостаточности документации на официальном сайте. Это и подтолкнуло меня написать эту статью. Надеюсь создание плагинов стало для вас чуть более понятным. Буду рад конструктивной критике и обратной связи.


    Полный код проекта можно посмотреть здесь


    Полезные ссылки


    Похожие публикации

    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

    Комментарии 2

      0
      Moxy еще живая в 2020?
        0

        Да, живее всех живых. В нашей компании 90% проектов на ней.
        Сейчас Google активно продвигают MVVM, поэтому может казаться, что MVP уходит, но оно как выполняло все необходимые функции, так и выполняет.

      Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

      Самое читаемое