在AndroidStudio4.X以后构建开发模板

Posted by lx8421bcd on June 2, 2021

前言

长期以来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>
    

导入依赖库

  1. 从Android Studio安装目录中把wizard_template.jar复制出来,如果用的是mac,以下命令供参考:
     cp /Applications/Android\ Studio.app/Contents/plugins/android/lib/wizard-template.jar ~/Desktop
    
  2. 在插件项目根目录中创建一个lib文件夹,把wizard_template.jar放进去。
  3. 编辑项目的build.gradle.kts配置文件,加入依赖
     dependencies {
         detektPlugins("io.gitlab.arturbosch.detekt:detekt-formatting:1.17.1")
         // 添加依赖库
         compileOnly(files("lib/wizard-template.jar"))
     }
    
  4. 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>
    
  5. sync project

修改插件项目功能入口

  1. 编辑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
         }
     }
        
    
  2. 在与listener同级的package下创建一个otherpackage
     --- com.lx8421bcd.qdftemplate  
      |--- listener
      |--- services
      |--- other
        
    
  3. 创建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,
         )
     }
        
    
  4. 修改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>
    

至此一个模板插件项目基本配置完成,之后编写模板只需将模板构建方法添加到TemplateProvider实现类的列表中即可。

编写模板

以下用Activity模板为例:

package other.activity

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包。

安装插件:

  1. 打开Android Studio设置
  2. 在Plugins一栏点击齿轮图标,选择“Install Plugin from Disk”
  3. 选择生成的Jar包安装
  4. 重启Android Studio,完成安装

小结

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

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

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