[Android翻译]Android Studio 4.1+的模板插件

155 阅读2分钟

本文由 简悦SimpRead 转码,原文地址 steewsc.medium.com

如果你正在开始一个新的项目,或者你想把旧的项目转移到一个新的架构上,你......

image.png

如果你正在开始一个新的项目,或者你想把旧的项目转移到一个新的架构上,你应该考虑创建自定义模板,这样你就可以避免编写所有的模板代码,把时间花在其他地方。

直到最近,为了创建你的自定义模板,你只需要进入 $ANDROID_STUDIO/plugins/android/lib/templates/ 文件夹并在那里寻找例子,但是从Android Studio 4.1开始,那个已经不起作用了,我在完成我的模板后就发现了这一点,想在尝试之前更新Android Studio :)

但这也有一个好处,因为现在你可以使用Kotlin而不是FTL来做你的模板,实际上现在的模板不再只是一个模板,而是一个JetBrains IntelliJ平台插件。

首先去github.com/JetBrains/i…,按照他们的README中的指示(简而言之:点击使用此模板的绿色按钮:)

image.png

按照这个向导,你会得到新的资源库,这就是你的插件代码所在。现在你需要通过克隆或下载来获得它。

image.png

一旦完成,从Android Studio中打开它,我们就可以开始做一些修改,使它在你的Android Studio版本上工作。

请记住,我使用某些类和包的名称只是作为一个例子,请随意使用你自己的。

重新组织包

在这个例子中,我把自动生成的类移到了我的基础包com.github.steewsc.mvisetup:

image.png

gradle.properties

gradle.properties文件开始,你需要设置。

pluginName_为 "mvi-setup"
pluginGroup为 "com.github.steewsc.mvisetup"

image.png

平台版本,以符合你正在使用的平台。对于Android Studio 4.14.2 Canary 14,我将其设置为2020.2,但关于如何确定这个的更多细节,请查阅。jetbrains.org/intellij/sd…

image.png

platformPluginsjava, com.intellij.java, org.jetbrains.android, android, org.jetbrains.kotlin

image.png

plugin.xml

接下来从src/main/resources/META-INF中打开plugin.xml文件并更新。 id为 "com.github.steewsc.mvisetup"(pluginGroup来自gradle.properties)。 name为 "mvi-setup" (pluginName_ from gradle.properties)
vendor为 "steewsc"(你的名字)。

image.png

然后添加三个额外的依赖项(com.intellij.modules.platform应该已经在那里了)
org.jetbrains.android
org.jetbrains.kotlin
com.intellij.modules.java

image.png

更新基础包为com.github.steewsc.mvisetup的所有部分。

image.png

settings.gradle.kts

更新rootProject.name为 "mvi-setup"(或你的插件名称)。

做一个gradle sync,这样我们就完成了设置的部分,我们就可以进入代码了。

为了让你的模板在新菜单中可见,我们必须有。

  • 继承自WizardTemplateProvider的类
  • 模板
  • Recipe
  • 我们的模板文件

这与我们在AS 4.1之前的设置类似,只是现在使用了Kotlin,而且我实际上使用了我的旧模板(基于FTL)作为新插件的来源。

我在写插件的时候遇到了一个小问题,关于我的RecipeExecutor的项目句柄/实例,所以我最后用了一个有点脏的方法,直到我找到其他的方法(我只是想尽快让它工作:D)

更新MyProjectManagerListener.kt以存储项目实例。

package com.github.steewsc.mvisetup.listeners

import com.intellij.openapi.project.Project
import com.intellij.openapi.project.ProjectManagerListener
import com.github.steewsc.mvisetup.services.MyProjectService

class MyProjectManagerListener : ProjectManagerListener {

    override fun projectOpened(project: Project) {
        projectInstance = project
        project.getService(MyProjectService::class.java)
    }

    override fun projectClosing(project: Project) {
        projectInstance = null
        super.projectClosing(project)
    }

    companion object {
        var projectInstance: Project? = null
    }
}

另外,我跳过了Java版本,但当你完成这个设置后,你可以很容易地写出它。

模板文件

ActivtyAndLayout.kt

import com.android.tools.idea.wizard.template.ProjectTemplateData
import com.android.tools.idea.wizard.template.extractLetters

fun someActivity(
        packageName: String,
        entityName: String,
        layoutName: String,
        projectData: ProjectTemplateData
) = """
package $packageName

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle

import ${projectData.applicationPackage}.R;

class ${entityName}sActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.${extractLetters(layoutName.toLowerCase())})
    }
}
"""
fun someActivityLayout(
        packageName: String,
        entityName: String) = """
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="${packageName}.${entityName}sActivity">

</androidx.constraintlayout.widget.ConstraintLayout>
"""

Recipe.kt

package other.mviSetup

import com.android.tools.idea.wizard.template.ModuleTemplateData
import com.android.tools.idea.wizard.template.RecipeExecutor
import com.android.tools.idea.wizard.template.activityToLayout
import com.android.tools.idea.wizard.template.extractLetters
import com.android.tools.idea.wizard.template.impl.activities.common.addAllKotlinDependencies
import com.github.steewsc.mvisetup.listeners.MyProjectManagerListener.Companion.projectInstance
import com.intellij.openapi.roots.ProjectRootManager
import com.intellij.psi.PsiDirectory
import com.intellij.psi.PsiFileFactory
import com.intellij.psi.PsiManager

import someActivity
import someActivityLayout
fun RecipeExecutor.mviSetup(
        moduleData: ModuleTemplateData,
        packageName: String,
        entityName: String,
        layoutName: String
) {
    val (projectData) = moduleData
    val project = projectInstance ?: return

    addAllKotlinDependencies(moduleData)

    val virtualFiles = ProjectRootManager.getInstance(project).contentSourceRoots
    val virtSrc = virtualFiles.first { it.path.contains("src") }
    val virtRes = virtualFiles.first { it.path.contains("res") }
    val directorySrc = PsiManager.getInstance(project).findDirectory(virtSrc)!!
    val directoryRes = PsiManager.getInstance(project).findDirectory(virtRes)!!

    someActivity(packageName, entityName, layoutName, projectData)
            .save(directorySrc, packageName, "${entityName}sActivity.kt")

    someActivityLayout(packageName, entityName)
            .save(directoryRes, "layout", "${layoutName}.xml")
}

fun String.save(srcDir: PsiDirectory, subDirPath: String, fileName: String) {
    try {
        val destDir = subDirPath.split(".").toDir(srcDir)
        val psiFile = PsiFileFactory
                .getInstance(srcDir.project)
                .createFileFromText(fileName, KotlinLanguage.INSTANCE, this)
        destDir.add(psiFile)
    }catch (exc: Exception) {
        exc.printStackTrace()
    }
}

fun List<String>.toDir(srcDir: PsiDirectory): PsiDirectory {
    var result = srcDir
    forEach {
        result = result.findSubdirectory(it) ?: result.createSubdirectory(it)
    }
    return result
}

Template.kt

package other.mviSetup

import com.android.tools.idea.wizard.template.*
import java.io.File
import mviSetup

val mviSetupTemplate
    get() = template {
        revision = 2
        name = "MY Setup with Activity"
        description = "Creates a new activity along layout file."
        minApi = 16
        minBuildApi = 16
        category = Category.Other // Check other categories
        formFactor = FormFactor.Mobile
        screens = listOf(WizardUiContext.FragmentGallery, WizardUiContext.MenuEntry,
                WizardUiContext.NewProject, WizardUiContext.NewModule)

        val packageNameParam = defaultPackageNameParameter
        val entityName = stringParameter {
            name = "Entity Name"
            default = "Wurst"
            help = "The name of the entity class to create and use in Activity"
            constraints = listOf(Constraint.NONEMPTY)
        }

        val layoutName = stringParameter {
            name = "Layout Name"
            default = "my_act"
            help = "The name of the layout to create for the activity"
            constraints = listOf(Constraint.LAYOUT, Constraint.UNIQUE, Constraint.NONEMPTY)
            suggest = { "${activityToLayout(entityName.value.toLowerCase())}s" }
        }

        widgets(
                TextFieldWidget(entityName),
                TextFieldWidget(layoutName),
                PackageNameWidget(packageNameParam)
        )

        recipe = { data: TemplateData ->
            mviSetup(
                    data as ModuleTemplateData,
                    packageNameParam.value,
                    entityName.value,
                    layoutName.value
            )
        }
    }

val defaultPackageNameParameter get() = stringParameter {
    name = "Package name"
    visible = { !isNewModule }
    default = "com.mycompany.myapp"
    constraints = listOf(Constraint.PACKAGE)
    suggest = { packageName }
}

WizardTemplateProviderImpl.kt

package other

import com.android.tools.idea.wizard.template.Template
import com.android.tools.idea.wizard.template.WizardTemplateProvider
import other.mviSetup.mviSetupTemplate

class WizardTemplateProviderImpl  : WizardTemplateProvider() {

    override fun getTemplates(): List<Template> = listOf(mviSetupTemplate)
}

现在回到plugin.xml中,将你的向导模板添加到其中。

<extensions defaultExtensionNs="com.android.tools.idea.wizard.template">
    <wizardTemplateProvider implementation="other.WizardTemplateProviderImpl" />
</extensions>

image.png

打开Gradle标签,运行buildPlugin

image.png

如果一切顺利,你应该看到你的插件已经准备好安装在YOUR_PROJECT_DIR\buildlibs\ my-setup-0.1.0.jar

image.png

现在你可以在Android Studio初始屏幕上拖放它,或者进入设置->插件->从磁盘安装插件,从libs中挑选jar。

image.png

重启IDE,在你的项目中右键点击某些包->新建->其他->MY Setup with Activity,如果没有错误,你应该看到Wizard屏幕,在你点击Next/Finish之后,应该有插件生成的新文件。

这只是一个基本的新活动设置,但你可以把它作为MVI的基本模板设置,并使重构一个旧项目变得轻而易举。

image.png 屏幕1:在新菜单中点击MVI设置与片段后!

image.png 屏幕2:由 "MVI设置与片段 "向导生成的文件(红色的)。

如果你遇到任何问题,你可以在idea.log文件中看到堆栈跟踪(Android Studio -> Help -> Show log in Explorer/Finder.)。

如果你在New菜单中看不到Activity/Fragment和其他常用的子菜单,只需卸载一个插件(Settings->Plugins->YourPlugin -> Uninstall)。 在IDE重启后它们应该会重新出现。检查idea.log是否有错误,纠正插件代码中的错误,增加插件的版本,构建它并重新安装。

可在以下网站获得演示。 github.com/steewsc/tem…


更新了。 我刚刚在github.com/steewsc/tem… 推送了一个更新,它没有使用项目实例黑客。 检查并更新androidStudioPathandroidStudioPathMacOSgradle.properties中指向你的Android Studio安装。

image.png

另外,你会发现三个运行配置。

image.png

你可以使用template-example [runIde] 来轻松地在AS中构建和运行你的插件,而不会弄乱你的工作AS,还可以看到它的实时日志。

更新2:
为了使该插件能在较新的/你的AS版本中工作,请更新。 pluginUntilBuild, pluginVerifierIdeVersions, platformCompilerVersiongradle.properties中。 这里有一个AS Bumblebee的例子。

image.png