Android:基于intellij-platform-plugin-template实现自定义页面模板插件

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 21 天,点击查看活动详情

大家平时开发使用的框架是自己搭建的吗?可能对于大公司而言,都有专业负责管理框架的大神,根本轮不到自己出场。但对于一些中小型公司来说,往往都是自己孤身奋战,独立开发,这时都是使用自己搭建的框架,时间充裕的时候还会制造一些有意思的插件,以提升平时的开发效率。今天咱们就来说说关于Android端页面级模板插件的自定义。

时间回退三四年,那时还主流MVP模式,当时项目使用的便是组长封装的 MVPArms 框架,自开始使用便根据文档安装了与之配套的页面级(MVPArmsTemplate)和模板级(MVPArms-Module-Template)插件,文档写的非常仔细,一看就会,平时开发起来相当节约时间,一个页面、一个模块只需一键即可生成。奈何AndroidStudio几乎每次更新,内部的模板就会更改,所以自定义的模板也就面临更改,由于之前框架一直再维护,所以模块也会跟着维护。

如今,MVP已不再是主流,MVPArms内使用的技术也已过时,所以这个框架注定被淘汰。相信大家都已经跟随谷歌爸爸的脚步基于Jetpack自己搭建了MVVM或者最新的MVI模式框架吧。不得不说,现在的框架和以前的框架相比,确实优化了很多不足,使用起来也变得简单,主要还是看自己的代码风格和喜好。

有了自己的框架,不自定义一个模板总感觉少了点什么,所以我们就来讲讲AndroidStudio如何自定义模板吧:

大概是在AS 4.1版本开始,以往的自定义模板方法已经不可用了,就连对应的templates文件夹都不存在。最后才发现,只能使用官方提供的intellij-platform-plugin-template 库进行插件自定义,生成Jar包供AS安装。但其实内部的样板代码是差不多的,主要就是配置环境和生成Jar包容易出现问题。

image.png

创建完成后,使用AS打开,注意这里还需要依赖一个Jar包,可以理解为AS的基础模板Jar包,位置在AS安装目录下/Applications/Android/Studio.app/Contents/plugins/android/lib/目录下。

build.gradle.kts中添加依赖

dependencies {
    compileOnly(files("lib/wizard-template.jar"))
}

gradle.properties中修改对应信息

pluginGroup = com.enample.mvvm.mvvmtemplatenew  //插件路径
pluginName_ = MVVMTemplateNew //插件名
pluginVersion = 1.0.4 //插件版本
platformVersion = 2021.1.3 //ide版本
platformPlugins = com.intellij.java, org.jetbrains.kotlin //编译语言

listeners目录下新建类

internal class MyProjectManagerListener : ProjectManagerListener {
    override fun projectOpened(project: Project) {
        project.service<MyProjectService>()
    }
}

settings.properties中修改模板名

rootProject.name = "MVVMTemplateNew"

基础目录就是这样:

image.png

内部修改的代码其实都是照猫画虎,咱们就看看mvvmRecipe和mvvmTemplate两个工具类的部分方法:

val MVVMTemplate
    get() = template {
        name = "MVVM Template"
        description = "一键创建 MVVM 单个页面所需要的全部组件"
        minApi = 19

        category = Category.Other
        formFactor = FormFactor.Mobile
        screens = listOf(WizardUiContext.ActivityGallery, WizardUiContext.MenuEntry, WizardUiContext.NewProject, WizardUiContext.NewModule)

        val pageName = stringParameter {
            name = "Page Name"
            default = "Main"
            help = "请填写页面名,如填写 Main,会自动生成 MainActivity, MainViewModel 等文件"
            constraints = listOf(Constraint.NONEMPTY, Constraint.UNIQUE)
        }

        val packageName = stringParameter {
            name = "Root Package Name"
            default = "com.mycompany.myapp"
            constraints = listOf(Constraint.PACKAGE)
            help = "请填写你的项目包名,请认真核实此包名是否是正确的项目包名,不能包含子包,正确的格式如:com.exanmple.app"
        }

        //是否需要生成Activity
        val needActivity = booleanParameter {
            name = "Generate Activity"
            default = true
            help = "是否需要生成 Activity ? 不勾选则不生成"
        }

        //布局名
        val activityLayoutName = stringParameter {
            name = "Activity Layout Name"
            default = "activity_main"
            visible = { needActivity.value }
            help = "Activity 创建之前需要填写 Activity 的布局名,若布局已创建就直接填写此布局名,若还没创建此布局,请勾选下面的单选框"
            constraints = listOf(Constraint.LAYOUT, Constraint.NONEMPTY)
            suggest = { "${activityToLayout(pageName.value.upperCase())}" }
        }

        //是否需要Activity的布局
        val generateActivityLayout = booleanParameter {
            name = "Generate Activity Layout"
            default = true
            visible = { needActivity.value }
            help = "是否需要给 Activity 生成布局? 若勾选,则使用上面的布局名给此 Activity 创建默认的布局"
        }

        val activityPackageName = stringParameter {
            name = "Activity Package Name"
            default = "Activity Package Name"
            visible = { needActivity.value }
            help = "Activity 将被输出到此包下,请认真核实此包名是否是你需要输出的目标包名"
            constraints = listOf(Constraint.PACKAGE)
            suggest = { "${packageName.value}.${pageName.value.toLowerCase()}" }
        }

        //Fragment
        //是否需要生成Fragment
        val needFragment = booleanParameter {
            name = "Generate Fragment"
            default = false
            help = "是否需要生成 Fragment ? 不勾选则不生成"
        }

        //布局名
        val fragmentLayoutName = stringParameter {
            name = "Fragment Layout Name"
            default = "fragment_main"
            visible = { needFragment.value }
            help = "Fragment 创建之前需要填写 Fragment 的布局名,若布局已创建就直接填写此布局名,若还没创建此布局,请勾选下面的单选框"
            constraints = listOf(Constraint.LAYOUT, Constraint.UNIQUE, Constraint.NONEMPTY)
            suggest = { "${fragmentToLayout(pageName.value.upperCase())}" }
        }

        //是否需要Fragment的布局
        val generateFragmentLayout = booleanParameter {
            name = "Generate Fragment Layout"
            default = true
            visible = { needFragment.value }
            help = "是否需要给 Fragment 生成布局? 若勾选,则使用上面的布局名给此 Fragment 创建默认的布局"
        }

        val fragmentPackageName = stringParameter {
            name = "Fragment Package Name"
            default = "function Package Name"
            constraints = listOf(Constraint.PACKAGE)
            visible = { needFragment.value }
            help = "Fragment 将被输出到此包下,请认真核实此包名是否是你需要输出的目标包名"
            suggest = {"${packageName.value}.${pageName.value.toLowerCase()}"}
        }

        val needRepository = booleanParameter {
            name = "Generate Repository"
            default = true
            help = "是否需要生成 Repository ? 不勾选则不生成"
        }

        val repositoryPackageName = stringParameter {
            name = "Repository Package Name"
            default = "Repository Package Name"
            constraints = listOf(Constraint.PACKAGE)
            visible = { needRepository.value }
            help = "Repository 将被输出到此包下,请认真核实此包名是否是你需要输出的目标包名"
            suggest = {"${packageName.value}.${pageName.value.toLowerCase()}"}
        }

        val needViewModel = booleanParameter {
            name = "Generate ViewModel"
            default = true
            help = "是否需要生成 ViewModel ? 不勾选则不生成"
        }

        val viewModelPackageName = stringParameter {
            name = "ViewModel Package Name"
            default = "ViewModel Package Name"
            constraints = listOf(Constraint.PACKAGE)
            visible = { needViewModel.value }
            help = "ViewModel 将被输出到此包下,请认真核实此包名是否是你需要输出的目标包名"
            suggest =  {"${packageName.value}.${pageName.value.toLowerCase()}"}
        }

        widgets(
                TextFieldWidget(pageName),
                PackageNameWidget(packageName),
                CheckBoxWidget(needActivity),
                TextFieldWidget(activityLayoutName),
                CheckBoxWidget(generateActivityLayout),
                TextFieldWidget(activityPackageName),
                CheckBoxWidget(needFragment),
                TextFieldWidget(fragmentLayoutName),
                CheckBoxWidget(generateFragmentLayout),
                TextFieldWidget(fragmentPackageName),
                CheckBoxWidget(needRepository),
                TextFieldWidget(repositoryPackageName),
                CheckBoxWidget(needViewModel),
                TextFieldWidget(viewModelPackageName),
                LanguageWidget()
        )

        thumb { File("template_blank_activity.png") }

        recipe = { data: TemplateData ->
            mvvmRecipe(
                    data as ModuleTemplateData,
                    pageName.value,
                    packageName.value,
                    needActivity.value,
                    activityLayoutName.value,
                    generateActivityLayout.value,
                    activityPackageName.value,
                    needFragment.value,
                    fragmentLayoutName.value,
                    generateFragmentLayout.value,
                    fragmentPackageName.value,
                    needRepository.value,
                    needViewModel.value,
                    repositoryPackageName.value,
                    viewModelPackageName.value
            )
        }
    }

也就是创建时需要填写的一些页面信息,需要的就勾上,不需要的则不勾选

image.png

在mvvmRecipe中根据勾选的文件执行相应操作

if (needActivity) {
    mergeXml(
        manifestTemplateXml(packageRealName, activityPackageName, "${pageName}Activity"),
        manifestOut.resolve("AndroidManifest.xml")
    )
}

if (needActivity) {
        save(
            mvvmActivityKt(
                packageRealName,
                pageName,
                activityPackageName,
                activityLayoutName,
                needViewModel
            ), srcOut.resolve("${pageName.toLowerCase()}/${pageName}Activity.${ktOrJavaExt}")
        )
}

这里还差一步,把刚刚mvvmTemplate中的MVVMTemplate配置到SamplePluginTemplateProviderImpl中

class SamplePluginTemplateProviderImpl : WizardTemplateProvider(){
    override fun getTemplates(): List<Template> = listOf(
            // activity的模板
            MVVMTemplate
    )
}

到此就可以点击AS中的Run Plugin进行编译,这里有个奇怪的现象是AS会下载配置中对应的IntelLiJ IDEA,下载完后会自动启动。此时在IntelLiJ IDEA编译成功无误后,两个编译器都可点击Run Plugin生成Jar包,运行成功后会在其根目录下/bulid/libs/中查看到对应Jar包。

image.png

这个时候拷贝出这个Jar包,接下来就是Android开发熟悉的环节了,按下图安装完成重启AS

image.png

image.png

此时的你就可以原地起飞,创建一个页面只需一键式操作,大大节约开发时间。模板内容可以根据自己喜好添加,AS更新需要慎重,有可能更新后插件无法使用,但根据报错信息和AS自带模板对比修改问题应该不大,所以如非必要尽量不要更新AS。

总结

整体实现下来也不是很难,但很容易出现编译失败等问题,还有就是插件安装后无法一键创建,或者创建出来的文件和预期不符的情况。很正常,这都是一个必经之路,仔细查看报错原因和代码,再根据官方文档(英文哦)提示,基本都能克服。无非就是一些版本原因导致,但插件成功了,在今后的开发中,尤其是初期开发,效率是大大提升,何乐而不为呢?

好了,这篇文章就讲解到这里,希望对大家有所帮助。

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 21 天,点击查看活动详情