Fantastic plugins, vol. 1. Teoria

A vida com um projeto de vários módulos não é tão simples. Para evitar a rotina de criação de um novo módulo, criamos nosso próprio plug-in para o Android Studio. No processo de implementação, encontramos uma falta de documentação prática, tentamos várias abordagens e descobrimos muitas armadilhas. Foram publicados dois artigos: “Teoria” e “Prática” . Me encontre!


imagem


Sobre o que vou falar?


  • Por que um plugin? Por que um plugin?
    • Elaborando uma lista de verificação
    • Opções de automação da lista de verificação
  • Noções básicas de desenvolvimento de plugins
    • Acções
    • Desenvolvimento de UI em plugins
    • Conclusões
  • Internos da IDEA: componentes, PSI
    • Unidade Interna da IDEA
    • PSI
    • Conclusões

Por que um plugin? Por que um plugin?


Se você estiver desenvolvendo um projeto Android com vários módulos, saberá que tipo de rotina é criar um novo módulo a cada vez. Você precisa criar um módulo, configurar o Gradle nele, adicionar dependências, esperar pela sincronização, não se esqueça de consertar algo no módulo do aplicativo - tudo isso leva muito tempo. Queríamos automatizar a rotina e começamos compilando uma lista de verificação do que fazemos cada vez que criamos um novo módulo.


1. Primeiramente, criamos o próprio módulo através do menu Arquivo -> Novo -> Novo módulo -> Biblioteca Android.


imagem


2. Escrevemos os caminhos para o módulo no arquivo settings.gradle, porque temos vários tipos de módulos - módulos principais e módulos de recursos, que estão em pastas diferentes.


Escrevemos caminhos para os módulos
// 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. Alteramos as constantes compileSdk , minSdk , targetSdk no build.gradle gerado: as substituímos pelas constantes definidas na raiz build.gradle .


Alterando constantes no build.gradle do novo módulo
 // Feature module build.gradle … android { compileSdkVersion rootProject.ext.targetSdkVersion defaultConfig { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion ... } } 

Nota: removemos recentemente esta parte do nosso trabalho para o plugin Gradle, que ajuda a configurar todos os parâmetros necessários do arquivo build.gradle em várias linhas.


4. Como estamos escrevendo todo o novo código no Kotlin, como padrão, conectamos dois plugins: kotlin-android e kotlin-kapt . Se o módulo estiver de alguma forma conectado à interface do usuário, conectamos adicionalmente o módulo kotlin-android-extensions .


Conectar plugins Kotlin
 // Feature module build.gradle apply plugin: 'com.android.library' apply plugin: 'kotlin-android' apply plugin: 'kotlin-android-extensions' apply plugin: 'kotlin-kapt' 

5. Conectamos as bibliotecas gerais e módulos principais. Os módulos principais são, por exemplo, criador de logs, análises, alguns utilitários gerais e bibliotecas - RxJava, Moxy e muitos outros.


Conectamos bibliotecas e módulos compartilhados
 // 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. Configure o kapt para palito. Palito de dente é a nossa estrutura principal de DI. Provavelmente você sabe: para usar a geração de código em vez de refletir na versão, você precisa configurar o processador de anotações para que ele entenda onde obter as fábricas dos objetos que estão sendo criados:


Configurando o Processador de Anotação para o Palito
 // Feature module build.gradle defaultConfig { ... javaCompileOptions { annotationProcessorOptions { arguments = [ toothpick_registry_package_name: "ru.hh.feature_worknear" ] } } ... 

Nota: no hh.ru, usamos a primeira versão do Toothpick, na segunda, removemos a capacidade de usar a geração de código .


7. Nós configuramos o kapt para Moxy dentro do módulo criado. O Moxy é nossa principal estrutura para criar MVP em um aplicativo, e você precisa torcer um pouco para que ele possa funcionar em um projeto de vários módulos. Em particular, registre o pacote do módulo criado nos argumentos do kapt:


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

Nota: já mudamos para a nova versão do Moxy , e essa parte da geração de código perdeu sua relevância.


8. Geramos um monte de novos arquivos. Não me refiro aos arquivos criados automaticamente (AndroidManifest.xml, build.gradle, .gitignore), mas à estrutura geral do novo módulo: interatores, repositórios, módulos DI, apresentadores, fragmentos. Existem muitos desses arquivos, eles têm a mesma estrutura no início e criá-los é uma rotina.


imagem


9. Conectamos nosso módulo criado ao módulo de aplicativo. Nesta etapa, você deve se lembrar de configurar o Toothpick no arquivo build.gradle do módulo de aplicativo. Para fazer isso, adicionamos o pacote do módulo criado ao processador de anotação de argumento especial - toothpick_registry_children_package_names .


Palito de Sintonia
 // 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(",") ] } } … 

Depois disso, configuramos o Moxy no módulo de aplicativo. Temos uma classe que é marcada com a anotação @RegisterMoxyReflectorPackages - aí adicionamos o nome do pacote do módulo criado:


Configurar o MoxyReflectorStub
 // App module file @RegisterMoxyReflectorPackages( "ru.hh.feature_force_update", "ru.hh.feature_profile", "ru.hh.feature_worknear" ... ) class MoxyReflectorStub 

E, no final, não esqueça de conectar o módulo criado ao bloco de dependências do módulo de aplicativo:


Adicionando dependências
 // 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') ... 

Temos uma lista de verificação de nove pontos.


Como existem muitos pontos, é provável que esqueça algo. Depois, passe horas imaginando o que aconteceu e por que o projeto não está indo.


Decidimos que você não pode viver assim e precisa mudar alguma coisa.


Opções de automação da lista de verificação


Após compilar a lista de verificação, começamos a procurar opções para automatizar seus itens.


A primeira opção foi uma tentativa de fazer "Ctrl + C, Ctrl + V" . Tentamos encontrar uma implementação para criar o módulo Biblioteca Android, que está disponível para nós "pronto para uso". Na pasta com o Android Studio (para MacOs: / Aplicativos / Android \ Studio.app/Contents/plugins/android/lib/templates/gradle-projects/ ), você pode encontrar uma pasta especial com modelos dos projetos que vê quando seleciona Arquivo - > Novo -> Novo módulo. Tentamos copiar o modelo NewAndroidModule alterando o ID dentro do arquivo template.xml.ftl . Em seguida, eles lançaram o IDE, começaram a criar um novo módulo e ... O Android Studio falhou porque a lista de módulos que você vê no menu para criar um novo módulo é codificada, não é possível alterá-la com uma cópia-colagem primitiva. Quando você tenta capturar e adicionar, excluir ou alterar um elemento, o Android Studio trava.


imagem


A segunda opção para automatizar a lista de verificação foi o mecanismo de modelo FreeMarker . Após uma tentativa malsucedida de copiar e colar, decidimos examinar mais de perto os modelos de módulo e encontramos modelos do FreeMarker sob o capô.


Não vou contar o que é o FreeMarker em detalhes - há um bom artigo do RedMadRobot e um vídeo do MosDroid de Lesha Bykov . Mas, resumindo, este é um mecanismo para gerar arquivos usando modelos e objetos java Map-ki especiais. Você alimenta modelos, objetos e o FreeMarker gera código na saída.


Mas olhe novamente para a lista de verificação:


imagem


Se você observar atentamente, poderá ver que ele está dividido em dois grandes grupos de tarefas:


  • As tarefas de gerar um novo código (1, 3, 4, 5, 6, 7, 8) e
  • Tarefas para modificar um código existente (2, 7, 8, 9)

E se o FreeMarker lida com tarefas do primeiro grupo com um estrondo, ele não lida com o segundo. Como um pequeno exemplo: na implementação atual da integração do FreeMarker no Android Studio, quando você tenta inserir uma linha no arquivo settings.gradle que não começa com a palavra 'include', o estúdio trava . Aqui pegamos o triste e decidimos abandonar o uso do FreeMarker.


Após uma falha no FreeMarker, surgiu a idéia de escrever meu próprio utilitário de console para executar uma lista de verificação. Dentro do Intellij IDEA, é possível usar um terminal. Por que não? Vamos escrever um script sobre o negócio total do bash:


imagem


Porém, como queremos configurar de forma flexível o módulo criado, teremos que inserir muitos sinalizadores diferentes, o que não será muito conveniente para imprimir no console.


Depois disso, demos um passo atrás e lembramos que estávamos trabalhando dentro da Intellij IDEA. E como é organizado? Há um certo núcleo de classes, um mecanismo ao qual muitos plug-ins estão conectados, que adicionam a funcionalidade necessária.


Quantos de vocês vêem na captura de tela mais de dois plugins conectados?


imagem


Para onde olhar?

imagem


Aqui eles estão conectados pelo menos três. Se você trabalha com o Kotlin, tem o plugin Kotlin ativado. Se você estiver trabalhando em um projeto com Gradle, o plug-in Gradle também será incluído. Se você estiver trabalhando em um projeto com um sistema de controle de versão - Git, SVN ou qualquer outra coisa - você terá o plug-in apropriado incluído para integrar este VCS.


Analisamos o repositório oficial de plugins JetBrains e constatamos que já existem mais de 4000 plugins registrados oficialmente! Quase todo o mundo escreve plugins, e esses plugins podem fazer qualquer coisa: começando pela integração de uma linguagem de programação no IDEA e terminando com ferramentas específicas que podem ser executadas no IDEA.


Resumindo, decidimos escrever nosso próprio plugin.


Noções básicas de desenvolvimento de plugins


Passamos ao básico do desenvolvimento de plugins. Para começar, você só precisa de três coisas:


  1. IntelliJ IDEA , mínimo Community Edition (você pode trabalhar na versão Ultimate, mas não oferecerá vantagens especiais ao desenvolver plugins);
  2. O plugin DevKit conectado a ele é um plugin especial que adiciona a capacidade de escrever outros plugins;
  3. E qualquer linguagem da JVM na qual você deseja escrever um plugin. Pode ser Kotlin, Java, Groovy - qualquer coisa.

Começamos criando um projeto de plugin. Selecionamos Novo projeto , aponte Gradle , assinalamos o IntelliJ Platform Plugin e criamos o projeto.


imagem


Nota: Se você não encontrar a caixa de seleção IntelliJ Platform Plugin, significa que você não possui o Plugin DevKit instalado.


Após preencher os campos obrigatórios, veremos uma estrutura de plug-in vazia.


imagem


Vamos dar uma olhada mais de perto. Consiste em:


  • Pastas nas quais você escreverá o código para o projeto futuro; ( main / java , main / kotlin , etc);
  • arquivo build.gradle , no qual você declarará as dependências do seu plugin em algumas bibliotecas e também configurará algo como gradle-intellij-plugin .

gradle-intellij-plugin - plugin Gradle que permite usar o Gradle como um sistema de criação de plugins. Isso é conveniente porque quase todos os desenvolvedores do Android estão familiarizados com o Gradle e sabem como trabalhar com ele. Além disso, o gradle-intellij-plugin adiciona tarefas gradle úteis ao seu projeto, em particular:


  • runIde - esta tarefa inicia uma instância IDEA separada com um plug-in que você está desenvolvendo para poder depurá-la;
  • buildPlugin - coleta o arquivo zip do seu plug-in para que você possa distribuí-lo localmente ou através do repositório oficial da IDEA;
  • confirmPlugin - esta tarefa verifica se há erros graves no plug-in que podem não permitir a integração no Android Studio ou em outra IDEA.

O que mais está dando o gradle-intellij-plugin ? Com sua ajuda, fica mais fácil adicionar dependências em outros plugins, mas falaremos sobre isso um pouco mais tarde, mas por enquanto posso dizer que gradle-intellij-plugin é seu irmão, use-o.


Voltar para a estrutura do plugin. O arquivo mais importante de qualquer plug-in é o 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> 

Este é um arquivo que contém:


  • Os metadados do seu plug-in: identificador, nome, descrição, informações do fornecedor, log de alterações
  • Descrição de dependências em outros plugins;
  • Aqui você também pode especificar a versão do IDEA com a qual seu plug-in funcionará corretamente
  • As ações também são descritas aqui.

Acções


O que são ações ? Suponha que você abra um menu para criar um novo arquivo. De fato, cada elemento deste menu foi adicionado por algum tipo de plugin:


imagem


Ações são os pontos de entrada do seu plug-in para os usuários. Cada vez que o usuário clica em um item de menu, você obtém controle dentro do plug-in, pode responder a esse clique e fazer o que for necessário.
Como são criadas as ações? Vamos escrever uma ação simples que exibirá uma caixa de diálogo com a mensagem "Olá, mundo".


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' } } 

Para criar uma ação, primeiro criamos uma classe que herda da classe AnAction . Em segundo lugar, devemos redefinir o método actionPerformed, onde o parâmetro especial da classe AnActionEvent vem . Este parâmetro contém informações sobre o contexto de execução da sua ação. O contexto refere-se ao projeto em que você está trabalhando, o arquivo que está aberto para o usuário no editor de código, os elementos selecionados na árvore do projeto e outros dados que podem ajudar no processamento de suas tarefas.


Para mostrar a caixa de diálogo "Olá, mundo", primeiro obtemos o projeto (apenas a partir do parâmetro AnActionEvent ) e depois usamos a classe de utilitário Messages para exibir a caixa de diálogo.


Quais recursos adicionais temos dentro da ação? Podemos substituir dois métodos: update e beforeActionPerformedUpdate .


O método de atualização é chamado sempre que o contexto de execução da sua ação é alterado. Por que pode ser útil para você: por exemplo, para atualizar o item de menu que foi adicionado pelo seu plugin. Suponha que você escreveu uma Ação que só pode funcionar com arquivos Kotlin e que o usuário abriu o arquivo Groovy. Em seguida, no método de atualização, você pode tornar sua ação inacessível.


O método beforeActionPerformedUpdate é semelhante ao método update, mas é chamado logo antes do actionPerformed . Esta é a última oportunidade de influenciar sua ação. A documentação recomenda que você não execute nada "pesado" neste método, para que seja executado o mais rápido possível.


Você também pode vincular ações a certos elementos da interface IDEA e atribuir combinações de teclas padrão para chamadas - eu recomendo ler mais sobre isso aqui .


Desenvolvimento de UI em plugins


Se você precisar de seu próprio design de diálogo, precisará trabalhar duro. Desenvolvemos nossa interface do usuário porque queríamos ter uma interface gráfica conveniente na qual seria possível marcar alguns ticks, ter um seletor para valores de enumeração e assim por diante.


O plug-in DevKit para desenvolvimento da interface do usuário adiciona algumas ações, como o formulário da GUI e o Diálogo . O primeiro cria um formulário vazio para nós, o segundo - um formulário com dois botões: Ok e Cancelar .


imagem


Ok, existe um designer de formulários , mas ele é ... mais ou menos. Em comparação, até o designer de Layout do Android Studio parece confortável e bom. A interface do usuário inteira é desenvolvida em uma biblioteca como o Java Swing. Este designer de formulário gera um arquivo XML legível por humanos. Se você não pode fazer algo no designer de formulários (exemplo: inserir vários controles na mesma célula da grade e ocultar todos os controles, exceto um), é necessário acessar este arquivo e alterá-lo - o IDEA selecionará essas alterações.


Quase todos os formulários consistem em dois arquivos: o primeiro possui a extensão .form , este é apenas o arquivo XML, o segundo é a chamada classe Bound , que pode ser escrita em Java, Kotlin, mas o que você quiser. Ele atua como um controlador de formulário. Inesperadamente, mas escrever em Java é muito mais fácil do que em outras linguagens. Porque, por exemplo, o ajuste para Kotlin ainda não é tão perfeito. Ao adicionar novos componentes ao trabalhar com a classe Java, esses componentes são automaticamente adicionados à classe e, quando você altera o nome do componente no designer, ele é automaticamente puxado. Mas no caso do Kotlin, os componentes não são adicionados - nenhuma integração ocorre, você pode esquecer algo e não entender por que nada funciona.


Resumimos o básico


  • Para criar um plugin, você precisará: IDEA Community Edition, Plugin DevKit e Java conectado a ele.
  • gradle-intellij-plugin é seu irmão, isso simplificará bastante sua vida, eu recomendo usá-lo.
  • Não escreva sua própria interface do usuário, a menos que seja necessário. Existem muitas classes de utilitários no IDEA que permitem criar sua própria interface do usuário imediatamente. Se você precisar de algo complicado - prepare-se para trabalhar duro.
  • O plug-in pode ter qualquer número de ações. O mesmo plugin pode adicionar muitas funcionalidades à sua IDEA.

Internos da IDEA: componentes, PSI


Vamos falar sobre o intestino da IDEA, sobre como ele é organizado no interior. Estou lhe dizendo para que nada desmorone na sua cabeça quando explico a parte prática, e para que você entenda de onde vem.


Como é organizado o IDEA? No primeiro nível da hierarquia, há uma classe como Aplicativo . Esta é uma instância separada da IDEA. Para cada instância do IDEA, um objeto de classe Aplicativo é criado. Por exemplo, se você executar o AppCode, Intellij IDEA, Android Studio ao mesmo tempo, obterá três instâncias separadas da classe Application. Esta classe foi projetada para lidar com o fluxo de entrada / saída.


Coloque o aplicativo na hierarquia

imagem


O próximo nível é a classe Projeto . Esse é o conceito mais próximo do que você vê ao abrir um novo projeto no IDEA. Geralmente, é necessário um projeto para obter outros componentes dentro do IDEA: classes de utilidade, gerentes e muito mais.


Lugar do projeto na hierarquia

imagem


O próximo nível de detalhe é a classe Module . Em geral, um módulo é uma hierarquia de classes agrupadas em uma pasta. Mas aqui por módulos queremos dizer módulos Maven, módulos Gradle. Essa classe é necessária, primeiro, para determinar as dependências entre os módulos e, em segundo lugar, para procurar classes dentro desses módulos.


Coloque o módulo em uma hierarquia

imagem


O próximo nível de detalhe é a classe VirtualFile . Esta é uma abstração sobre o arquivo real que está no seu disco. Várias instâncias do VirtualFile podem corresponder a cada arquivo real, mas são todas iguais. Ao mesmo tempo, se o arquivo real for excluído, o VirtualFile não será excluído por si só, mas simplesmente se tornará inválido.


Local da hierarquia do VirtualFile

imagem


Uma entidade como Document está associada a cada VirtualFile . É uma abstração sobre o texto do seu arquivo. O documento é necessário para que você possa rastrear eventos relacionados a alterações no texto do arquivo : o usuário inseriu uma linha, excluiu uma linha, etc., etc.


Colocar documento na hierarquia

imagem


Um pouco ao lado dessa hierarquia é a classe Editor - é um editor de código. Cada projeto pode ter um editor . É necessário para que você possa rastrear eventos relacionados ao editor de código: o usuário destacou a linha onde está o cursor, e assim por diante.


Lugar do editor na hierarquia

imagem


A última coisa que eu queria falar é sobre PsiFile . Isso também é uma abstração sobre arquivos reais, mas do ponto de vista da representação de elementos de código . PSI significa Program Structure Interface.


Lugar do PsiFile na hierarquia

imagem


E em que consiste cada programa? Considere uma classe Java regular.


Classe Java simples
 package com.experiment; import javax.inject.Inject; class SomeClass { @Inject String injectedString; public void someMethod() { System.out.println(injectedString); } } 

Consiste em especificar um pacote, importações, classes, campos, métodos, anotações, palavras-chave, tipos de dados, modificadores, identificadores, referências de métodos, expressões e tokens. E para cada elemento, há uma abstração do PsiElement . Ou seja, cada um dos seus programas consiste em PsiElements .


imagem


O PsiFile , por sua vez, é uma estrutura em árvore na qual cada elemento pode ter um pai e muitos descendentes.


imagem


Eu gostaria de mencionar que o PSI não é igual a uma árvore de sintaxe abstrata . Uma árvore de sintaxe abstrata é uma árvore de representação do seu programa depois que o analisador passou no programa e é desanexada de qualquer linguagem de programação. O PSI, ao contrário, está vinculado a uma linguagem de programação específica. Quando você trabalha com uma classe Java, está lidando com PsiElements Java. Ao trabalhar com uma classe Groovy - com Groovy PsiElements e assim por diante. , PSI- - , , , – .


PSI – PSI- IDEA. , , , . .


IDEA


  • PSI IDEA;
  • PSI- IDEA, ;
  • PsiElement-.

, . .



Source: https://habr.com/ru/post/pt463583/


All Articles