添加链接
link管理
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接

长期以来Android Studio除了官方模板之外并没有提供官方的、比较完善的模板编辑系统,而官方模板则是基于FreeMarker。如果用户有自定义模板需求,则基本上是基于官方的模板修改,然后按分类与Android Studio原生的模板放在同一路径下。
这种玩法优点是比较简单,大框架摆在那里,个性化的需求小修小改就可以了,缺点是没什么好用的编辑工具,只有文本编辑器,关键字提示是不存在的。另外一个问题是每次Android Studio版本更新过后,这些非官方的东西会在安装过程中被移除,要重新导入。
之前为了应付公司的新模块开发我也整了一套基于上述操作的模板,还写了shell/bat脚本用于在更新后一键导入,但更新了Android Studio 4.2之后,一键导入脚本全部报错,说找不到路径。我一看原来的tempaltes路径全没了,再一查,好么,原来的模板实现模式已经改了。

从Android Studio 4.1开始谷歌采用Geminio,使用Intellij Plugin的形式用kotlin编写模板,老的FreeMarker模板目前是无法在官方支持的体制下使用了。虽然说操作起来比FreeMarker麻烦一点,但起码是有IDE提示知道能用哪些功能了……

目前网上有一些继续使用FreeMarker的方法,但鉴于本文主要讲新的模板编辑方法,这里就不赘述了。

update

在Android Studio经过较大的版本更新后(特别是背后的IDEA版本更新了)已经编译过的插件Jar包可能会失效,如果出现了Android Studio更新后插件失效问题,建议将新版本Android Studio中的 wizard_template.jar 重新导出来,覆盖项目中老版本的 wizard_template.jar ,如果有必要的话,可以同步一下IDEA官方插件项目中更新的内容。

首先从Github上把 Intellij Platform Plugin Template 项目fork下来。这算是一个最简的插件项目工程模板了。
然后就是修改项目名称和相关配置属性:

  • 修改位于 settings.gradle.kts 里的 rootProject.name=name 为自己模板项目的名称
  • 修改 gradle.properties 里的项目配置
      # 插件制作者/所属组织
      pluginGroup = lx8421bcd
      # 插件名称
      pluginName = quickdevtemplates
      # 插件版本
      pluginVersion = 1.0.0
      
  • 修改项目包名, 将src/main里的代码包名从org.jetbrains.plugin.template改成你想要的包名(比如我为QuickDevFramework改为com.lx8421bcd.qdftemplate)。然后修改src/main/resources/META-INF/plugin.xml文件的配置
      <idea-plugin>
          <id>com.lx8421bcd.qdftemplates</id>
          <name>QDFTemplate</name>
          <vendor>lx8421bcd</vendor>
          ......
      </idea-plugin>
      
  • 从Android Studio安装目录中把wizard_template.jar复制出来,如果用的是mac,以下命令供参考:
     cp /Applications/Android\ Studio.app/Contents/plugins/android/lib/wizard-template.jar ~/Desktop
      
  • 在插件项目根目录中创建一个lib文件夹,把wizard_template.jar放进去。
  • 编辑项目的build.gradle.kts配置文件,加入依赖
     dependencies {
         detektPlugins("io.gitlab.arturbosch.detekt:detekt-formatting:1.17.1")
         // 添加依赖库
         compileOnly(files("lib/wizard-template.jar"))
      
  • plugin.xml中添加依赖项
     <idea-plugin>
         ......
         <!-- Product and plugin compatibility requirements -->
         <!-- https://plugins.jetbrains.com/docs/intellij/plugin-compatibility.html -->
         <depends>com.intellij.modules.platform</depends>
         <depends>org.jetbrains.android</depends>
         <depends>org.jetbrains.kotlin</depends>
     </idea-plugin>
      
  • sync project
  • 修改插件项目功能入口

  • 编辑MyProjectManagerListener.kt,配置插件在项目初始化时的操作 internal 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
  • 在与listener同级的package下创建一个otherpackage
     --- com.lx8421bcd.qdftemplate  
      |--- listener
      |--- services
      |--- other
      
  • 创建TemplateProvider继承实现,用于让IDE查找本插件项目提供的模板 package other import com.android.tools.idea.wizard.template.Template import com.android.tools.idea.wizard.template.WizardTemplateProvider import other.activity.SimpleViewBindingActivityTemplate import other.fragment.SimpleViewBindingFragmentTemplate class QDFPluginTemplateProviderImpl : WizardTemplateProvider() { override fun getTemplates(): List<Template> = listOf( // 在项目中创建的模板需要在这个列表内声明 SimpleViewBindingActivityTemplate, SimpleViewBindingFragmentTemplate,
  • 修改plugin.xml,添加对TemplateProviderImpl的声明,完整plugin.xml如下:
     <idea-plugin>
         <id>com.lx8421bcd.qdftemplates</id>
         <name>QDFTemplate</name>
         <vendor>lx8421bcd</vendor>
         <!-- Product and plugin compatibility requirements -->
         <!-- https://plugins.jetbrains.com/docs/intellij/plugin-compatibility.html -->
         <depends>org.jetbrains.android</depends>
         <depends>org.jetbrains.kotlin</depends>
         <depends>com.intellij.modules.platform</depends>
         <extensions defaultExtensionNs="com.intellij">
             <applicationService serviceImplementation="com.lx8421bcd.qdftemplates.services.MyApplicationService"/>
             <projectService serviceImplementation="com.lx8421bcd.qdftemplates.services.MyProjectService"/>
         </extensions>
         <applicationListeners>
             <listener class="com.lx8421bcd.qdftemplates.listeners.MyProjectManagerListener"
                     topic="com.intellij.openapi.project.ProjectManagerListener"/>
         </applicationListeners>
         <extensions defaultExtensionNs="com.android.tools.idea.wizard.template">
             <wizardTemplateProvider implementation="other.QDFPluginTemplateProviderImpl" />
         </extensions>
     </idea-plugin>
    import com.android.tools.idea.wizard.template.*
    import com.android.tools.idea.wizard.template.impl.activities.common.MIN_API
    import com.android.tools.idea.wizard.template.impl.activities.common.generateManifest
    import com.intellij.util.xml.DomManager
    //用于提供默认PackageName
    val defaultPackageNameParameter
        get() = stringParameter {
            name = "Package name"
            visible = { !isNewModule }
            default = "com.lx8421bcd.example"
            constraints = listOf(Constraint.PACKAGE)
            suggest = { packageName }
    // Activity模板Builder
    val SimpleViewBindingActivityTemplate
        get() = template {
            revision = 1
            name = "Simple ViewBinding Activity"
            description = "基于ViewBinding基类的Activity模板"
            minApi = MIN_API
            minBuildApi = MIN_API
            category = Category.Other
            formFactor = FormFactor.Mobile
            screens = listOf(WizardUiContext.ActivityGallery,
                WizardUiContext.MenuEntry,
                WizardUiContext.NewProject,
                WizardUiContext.NewModule
            lateinit var layoutName: StringParameter
            val activityClass = stringParameter {
                name = "Activity Name(不包含\"Activity\")"
                default = "Main"
                help = "只输入名字,不要包含Activity"
                constraints = listOf(Constraint.NONEMPTY)
            layoutName = stringParameter {
                name = "Layout Name"
                default = "activity_main"
                help = "请输入布局的名字"
                constraints = listOf(Constraint.LAYOUT, Constraint.UNIQUE, Constraint.NONEMPTY)
                suggest = { activityToLayout(activityClass.value.toLowerCase()) }
            val packageName = defaultPackageNameParameter
            widgets(
                TextFieldWidget(activityClass),
                TextFieldWidget(layoutName),
                PackageNameWidget(packageName)
            recipe = { data: TemplateData ->
                simpleViewBindingActivityRecipe(
                    data as ModuleTemplateData,
                    activityClass.value,
                    layoutName.value,
                    packageName.value)
    // 用于向项目写入模板文件的方法
    fun RecipeExecutor.simpleViewBindingActivityRecipe(
        moduleData: ModuleTemplateData,
        activityClass: String,
        layoutName: String,
        packageName: String
        val (projectData, srcOut, resOut) = moduleData
        val ktOrJavaExt = projectData.language.extension
        // 插入manifest声明
        generateManifest(
            moduleData = moduleData,
            activityClass = "${activityClass}Activity",
            activityTitle = activityClass,
            packageName = packageName,
            isLauncher = false,
            hasNoActionBar = false,
            generateActivityTitle = false,
        // 生成activity文件
        val activityFile = simpleViewBindingActivityKt(projectData.applicationPackage, activityClass, packageName)
        save(activityFile, srcOut.resolve("${activityClass}Activity.${ktOrJavaExt}"))
        // 生成xml布局文件
        val xmlFile = simpleViewBindingActivityXml(packageName, activityClass)
        save(xmlFile, resOut.resolve("layout/${layoutName}.xml"))
    /*-------------------- activity code generate function ----------------------*/
    fun simpleViewBindingActivityKt(
        applicationPackage:String?,
        activityClass:String,
        packageName:String
    )="""
    package $packageName
    import android.os.Bundle
    import com.linxiao.framework.architecture.SimpleViewBindingActivity
    import ${applicationPackage}.R
    import ${applicationPackage}.databinding.Activity${activityClass}Binding
    class ${activityClass}Activity : SimpleViewBindingActivity<Activity${activityClass}Binding>() {
         override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            initView()
        private fun initView() {
    /*-------------------- layout xml code generate function ----------------------*/
    fun simpleViewBindingActivityXml(
        packageName: String,
        activityClass: String
    ) = """
    <?xml version="1.0" encoding="utf-8"?>
    <RelativeLayout 
    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}.${activityClass}Activity">
    </RelativeLayout>
    

    完整的项目结构和代码,个人为QuickDevFramework做的模板插件项目QDFTemplates仅供参考

    导出插件、安装

    在Android Studio(Intellij ieda亦可)工具栏的Run上选择“Run Plugin”,build完成后会在项目的build/libs/目录下生成插件Jar包。

    安装插件:

  • 打开Android Studio设置
  • 在Plugins一栏点击齿轮图标,选择“Install Plugin from Disk”
  • 选择生成的Jar包安装
  • 重启Android Studio,完成安装
  • 本来自定义开发模板的本质也就是根据模板指引面板的输入,将一堆字符串改改保存到文件中存到指定位置,用不上太复杂的功能。 由于是使用wizard_template.jar包内提供的API直接编写kotlin代码,加之有IDE环境下,开发起来还是要方便不少,最起码不会在文本编辑器上抓瞎编写。其实这个模板不仅限于生成Activity、Fragment这些Android组件,也可以用于配置更复杂模板,例如MVVM架构下配套的ViewModel、带有列表的页面等等,只要参照生成文件的方法写好,保存到对应路径就行。

    需要注意的一点是,由于编辑Android组件模板内容其实就是编辑字符串,所以错误提醒是不存在的。咱们用模板就是图个一步到位,模板生成代码一片红看着都烦死。外加现在每次更新模板插件都需要重新打包,重新安装,重启IDE,非常麻烦,所以建议在编写复杂模板的同时最好开着一个Android项目用于测试模板代码,在项目环境下没问题了,再复制到模板中去。

    另外一点是,不建议搞巨无霸模板,一般来说,一套模板插件针对一个项目,或者使用相同基础库逻辑类似的项目(比如一个项目组所负责的多个项目)。还是那句话,谁都不喜欢模板生成个代码一片红,如果要针对差异化很大的多个项目开发通用模板,那就有更多内容需要靠模板引导页面灵活配置,如果最后用模板生成个页面整的像填报表一样的,那估计这模板也没谁想用吧……

  •