原文:https://steewsc.medium.com/template-plugin-for-android-studio-4-1-92dcbc689d39
作者:Stevica Trajanovic
如果你在開發一個新項目,或者你想將舊項目遷移到新架構,你應該考慮建立一個模板,好省去編寫樣板代碼的工作,把時間用到其他地方。
直到最近之前,要建立模板,只需要進入$ANDROID_STUDIO/plugins/android/lib/templates/文件夾尋找示例。但從Android Studio 4.1開始,這種方法不再奏效。就在我編寫完模板,並想要升級Android Studio的時候,我才意識到這一點。
當然也有好的一面。現在你可以用Kotlin替代FTL,同時,模板也被JetBrains IntelliJ平臺插件所替代。
首先打開 https://github.com/JetBrains/intellij-platform-plugin-template ,按照README的說明操作。(脫水版:點擊“Use this template”綠色按鈕)
按照嚮導指示執行完畢,你會得到一個代碼倉庫,用來保存插件代碼。現在你需要通過克隆或下載來得到代碼。
接着用Android Studio打開插件代碼。現在可以做一些調整,讓插件代碼適配你的Android Studio版本。
請記住:我在這裏使用某些類和包的名字只是作爲參考,你可以放心的修改。
1 重新組織包
對於這個例子,我將自動生成的類移動到了我的基礎包com.github.steewsc.mvisetup。
2 gradle.properties
對於gradle.properties文件,你要設置
- pluginName。設置爲mvi-setup。
- pluginGroup。設置爲com.github.steewsc.mvisetup。
3 plugin.xml
接下來在src/main/resources/META-INF目錄下打開plugin.xml文件並設置:
- id。設置爲com.github.steewsc.mvisetup(即gradle.properties中的pluginGroup)。
- name。設置爲mvi-setup(即gradle.properties中的pluginName)。
- vendor。設置爲steewsc。
然後增加3個依賴項(依賴項com.intellij.modules.platform應該已經自動添加)
- org.jetbrains.android
- org.jetbrains.kotlin
- com.intellij.modules.java
在所有部分中,將基礎包名稱設置爲com.github.steewsc.mvisetup。
4 settings.gradle.kts
設置rootProject.name爲mvi-setup(或你的插件的名字)。
執行gradle同步,這樣我們就完成設置部分,可以開始編碼。
爲了讓模板在菜單中可見,我們必須:
- 從WizardTemplateProvider派生類
- Template
- Recipe
- 模板文件
這裏有些類似於我們在Android Studio 4.1之前需要做的工作,除了現在使用的是Kotlin。實際上我使用了舊模板(基於FTL)作爲新插件的源代碼。
我在編寫插件時遇到一個問題。因爲我在RecipeExecutor中使用了Project實例,不得不使用一些不好的編碼技巧(我想盡快完成工作),直到我找到更合適的辦法。
更新MyProjectManagerListener.kt來存儲Project對象:
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 =
<
strong
>
null
<
/strong
>
super
.
projectClosing
(
project
)
var
projectInstance: Project? =
<
strong
>
null
<
/strong
>
這裏我忽略Java版本。在配置完成後,你可以很容易的用Java編寫插件。
5 模板文件
ActivityAndLayout.kt
import
com.android.tools.idea.wizard.template.ProjectTemplateData
import
com.android.tools.idea.wizard.template.extractLetters
projectData: ProjectTemplateData
import androidx.appcompat.app.AppCompatActivity
import
${projectData.applicationPackage}
.R;
class
${entityName}
sActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.
${extractLetters(layoutName.toLowerCase())}
)
entityName:
String
)
=
"""
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http:<em>//</em><em>schemas.android.com/apk/res/android"</em>
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
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
someActivityLayout
fun
RecipeExecutor.
mviSetup
(
moduleData: ModuleTemplateData,
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
)
{
val
destDir = subDirPath.
split
(
"."
)
.
toDir
(
srcDir
)
val
psiFile = PsiFileFactory
.
getInstance
(
srcDir.
project
)
.
createFileFromText
(
fileName, KotlinLanguage.
INSTANCE
,
this
)
}
catch
(
exc: Exception
)
{
fun
List
<
String
>
.
toDir
(
srcDir: PsiDirectory
)
: PsiDirectory
{
result = result.
findSubdirectory
(
it
)
?: result.
createSubdirectory
(
it
)
Template.kt
import
com.android.tools.idea.wizard.template.
*
name =
"MY Setup with Activity"
description =
"Creates a new activity along layout file."
category = Category.
Other
<
em
>// </em><em>Check other categories</em>
formFactor = FormFactor.
Mobile
screens =
listOf
(
WizardUiContext.
FragmentGallery
, WizardUiContext.
MenuEntry
,
WizardUiContext.
NewProject
, WizardUiContext.
NewModule
)
val
packageNameParam = defaultPackageNameParameter
val
entityName = stringParameter
{
help =
"The name of the entity class to create and use in Activity"
constraints =
listOf
(
Constraint.
NONEMPTY
)
val
layoutName = stringParameter
{
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"
}
TextFieldWidget
(
entityName
)
,
TextFieldWidget
(
layoutName
)
,
PackageNameWidget
(
packageNameParam
)
recipe =
{
data
: TemplateData -
>
data
as
ModuleTemplateData,
packageNameParam.
value
,
val
defaultPackageNameParameter
get
()
= stringParameter
{
visible =
{
!isNewModule
}
default =
"com.mycompany.myapp"
constraints =
listOf
(
Constraint.
PACKAGE
)
suggest =
{
packageName
}
WizardTemplateProviderImpl.kt
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"
/>
打開Gradle標籤,運行buildPlugin。
如果一切正常,你將會看到插件已經被保存到YOUR_PROJECT_DIR\build\libs\my-setup-0.1.0.jar。
你可以將插件拖拽到Android Studio中,或者通過菜單Settings->Plugins->Install Plugin from Disk來安裝插件jar。
重啓IDE,嘗試運行插件。右鍵點擊項目中的某個包,在彈出菜單中依次選擇New->Other->MY Setup with Activity,順利的話你會看到嚮導屏幕。當你點擊Next/Finish按鈕,插件生成文件。
這只是一個基本Activity創建模板,你可以將它作爲MVI或其他模式的基礎樣板,並讓重構舊項目輕而易舉。
Figure 14: 屏幕1:在新建菜單中點擊MVI Setup with Fragment後
Figure 15: 屏幕2:“MVI Setup with Fragment”嚮導生成的文件(紅色部分)
如果你遇到任何問題,你可以在idea.log文件中看到堆棧跟蹤(Android Studio->Help->Show log In Explorer/Finder..)。
如果你在新建菜單中看不到Activity/Fragment和其他常見的子菜單,只需卸載插件(Settings->Plugins->YourPlugin->Uninstall),這些子菜單會在IDE重啓後出現。檢查idea.log中的錯誤,修復插件源代碼,增加插件版本,構建並重新安裝插件。
示例代碼在https://github.com/steewsc/template。