
作者:位元組跳動終端技術——周宸韬
概述
這篇文章旨在向讀者介紹IntelliJ IDE插件的開發流程以及常用的一些通用功能,任何基于IntelliJ開發的IDE都可以通過該方式制作插件,例如Android Studio(AS),本篇也将基于Android Studio進行展開介紹,讀者将從0到1學習到 Android Studio插件開發。
背景介紹
什麼是IDE插件、IDE插件能做什麼?
IDE插件是将一些功能內建到了IDE界面當中,當我們使用IDE進行開發工作時能很友善的通過UI界面使用這些功能,例如大家熟悉的project工程目錄,Gradle工具欄,IDE底部的Run、Terminal、Build界面等,都是通過IDE插件來實作的,可以說大部分需要通過指令行執行、或使用者手動的一些操作都可以通過插件實作,并以UI的形式呈現。
如下圖:左圖為Android Studio IDE界面右側Gradle工具欄,包含了很多Gradle任務,點選UI的效果等同于使用者在指令行中輸入Gradle指令。右圖為IDE頂部菜單欄版本控制部分,其中對于版本的送出、拉取等按鈕等價于指令行輸入對應指令。
為什麼要做Android Studio IDE插件?
筆者作為中台部門開發者,經常涉及到一些通用的功能的開發,并以工具或元件等形式交由外部使用。是以如何降低使用者學習成本、提高工作效率是我的目标,而這些優化方向都離不開巧妙的使用工具。例如本次要介紹的IDE插件的開發背景就是以此為目标:将原本需要使用指令行完成的工作、或者學習成本較高的操作通過UI進行包裝,并且依附在原生的AS界面中,通過UI的互動大幅降低使用者學習成本,同時提升使用體驗。
舉例對比一下,下面兩幅圖檔是某個工程自動搭建功能的截圖,左右兩圖分别為使用指令行和使用AS插件的體驗對比,可以看到左側在使用CLI指令行進行工程搭建時界面資訊不夠簡潔明了,且使用者互動體驗較差,使用者必須在使用前閱讀文檔,并且沒有容錯機制,輸錯就得從頭開始。而相同的功能使用右側AS插件的體驗則好很多,不僅各條資訊清楚明了,還能拓展更多細節功能,如動态檢驗使用者輸入,輸入無誤才可進行下一步等等,使用者完全可以在零知識背景的情況下使用該插件并輕松完成所有功能操作,而且接近原生的界面更美觀。
如何開發一個IDE插件?
準備工作
在開發第一個插件前,我們要下載下傳正确的開發工具,在JetBrain官網中下載下傳
IntelliJ IDEA
,***下載下傳連結***。
這裡使用的開發工具是IntelliJ IDEA而不是Android Studio,因為AS是基于IntelliJ為模版開發的,IDE插件必須通過IntelliJ開發、釋出,再安裝到Android Studio中才能使用。
我們需要确認我們使用的Android Studio是基于哪個IntelliJ版本。這很重要,和你目前使用的Android Studio版本相同能讓你在調試時很友善,而新版的IntelliJ包含的features在你使用的AS上可能并沒有,導緻插件無法安裝,或提示相容性報錯。(圖中的報錯也會出現在未開啟向高版本相容時發生,開發時按需開啟) 。
下載下傳時請跟随這個步驟:
- 打開你的Android Studio,檢視版本号(The Build Number),這就是我們需要的IntelliJ版本号。
- 在下載下傳頁面,點選Other versions,找到對應的IntelliJ版本,下載下傳安裝即可。
建立新的工程 + 配置
這部分也可參照官網:https://www.jetbrains.org/intellij/sdk/docs/basics/getting_started.html
- 建立新工程包含兩個向導頁面,【選擇工程模版架構 + 填寫插件工程資訊】,按圖中配置即可。
- 完成兩步向導程式後自動建立工程,我們需要先了解兩個核心檔案:【build.gradle + plugin.xml】,并做一些前置的配置工作。
build.gradle
因為build.gradle和Android工程中的建構檔案非常類似,這裡隻解釋Android中沒有的配置。
- version:intellij閉包建立時隻帶一個屬性 version,該屬性代表用來建構這個插件的IntelliJ 平台IDE的版本,如果我們在開發時調用【runIde】這個Gradle task,一個基于這個版本的IntelliJ IDE執行個體就會被建立。
- localPath:因為我們希望在AS的環境下測試我們的插件,是以我們需要将AS作為我們插件的一個依賴,增加一個屬性叫localPath指定本機Android Studio應用程式Contents的安裝目錄,一個基于這個版本的Android Studio執行個體就會被建立(注意localPath不能和version屬性同時使用,因為我們本地的AS路徑中已經有了版本資訊)。
- plugins:添加開發需要的依賴插件。可以在這裡添加很多我們想用的插件,比如我們想在插件中執行git指令,我們可以添加 ’git4idea‘ plugin。
intellij {
version '2020.1.4'
localPath '/Applications/Android Studio.app/Contents'
plugins = ['Kotlin','android','git4idea']
}
plugin.xml
在resource檔案夾下可以找到plugin.xml檔案,這個檔案中可以配置我們插件的各項屬性,核心功能是注冊我們插件包含的components和service(功能類實作後還需要在這裡進行注冊才能使用,類似在AndroidManifest.xml中聲明Activity和Service)。
- 聲明我們的插件需要并且和AS相相容:增加android和android studio modules作為依賴。
<idea-plugin>
...
<depends>com.intellij.modules.platform</depends>
<depends>org.jetbrains.android</depends>
<depends>com.intellij.modules.androidstudio</depends>
<extensions defaultExtensionNs="com.intellij">
<!-- Add your extensions here -->
</extensions>
<actions>
<!-- Add your actions here -->
</actions>
</idea-plugin>
運作插件
配置完成後我們可以嘗試運作插件工程,具體位置在Gradle工具欄
項目名稱/Tasks/intelliJ/runIde
路徑。運作
runIde
任務,因為我們配置了Android Studio為啟動路徑,是以一個Android Studio模拟IDE會打開,所有内容都和我們本地的Android Studio沒有差别。
IDE插件常用功能介紹
建立一個Action
什麼是Action?
Actions官方介紹: The system of actions allows plugins to add their own items to IDEA menus and toolbars. An action is a class, derived from the AnAction.
Actions是使用者調用插件功能最常見的方式,如下圖的工具目錄是開發者經常用到的,裡面所有的可選項都是一個Action,可以進一步展開的則是Action Group。
如何建立一個Action?
兩個步驟:
- 【code implementation - 實作Action的具體代碼邏輯】:決定了這個action在哪個context下有效,并且在UI中被選擇後的功能(繼承父類AnAction并重寫actionPerformed()方法,用于Action被執行後的回調)。
- 【registered - 在配置檔案中注冊】:決定了這個action在IDE界面的哪個位置出現(建立新的group或存放進現有的ActionGroup,以及在group中的位置)。
兩個條件達成,action就可以從IntelliJ Platform中獲得使用者執行動作後的回調,例子: ‘HelloWorld’。
Code implementation
class HelloWorldAction : AnAction() {
override fun actionPerformed(event: AnActionEvent) {
//這裡建立了一個消息提示彈窗,在IDE中展示“Hello World”
val notificationGroup = NotificationGroup(
displayId = "myActionId",
displayType = NotificationDisplayType.BALLOON
)
val notification = notificationGroup.createNotification(
title = "chentao Demo",
content = "Hello World",
type = NotificationType.INFORMATION
).notify(event.project) //從方法的Event對象中擷取到目前IDE正在展示的project,在該project中展示彈窗
}
}
Registering a Custom Action
<actions>
<!-- Add your actions here -->
<!-- 建立了一個ActionGroup -->
<group id = "ChentaoDemo.TopMenu"
text="ChentaoDemo Plugin"
description="Demo Plugin in top menu">
<!-- 注冊HelloWorld Action -->
<action class="com.chentao.demo.actions.HelloWorldAction"
id="DemoAction"
text="Hello World Action"
description="This is a test action">
<!-- 設定 HelloWorld Action 的鍵盤快捷鍵-->
<keyboard-shortcut first-keystroke="control alt p" keymap="$default"/>
<!-- 将HelloWorld Action添加到剪切拷貝組中 -->
<add-to-group group-id="CutCopyPasteGroup" anchor="last"/>
</action>
<!-- 将這個Group添加到主菜單 -->
<add-to-group group-id="MainMenu" anchor="last"/>
</group>
</actions>
運作插件 - 結果展示
實作了以上兩步後,運作
runIde
Task,頂部的主菜單欄末尾出現了我們添加的ActionGroup,展開可看見HelloWorldAction,點選Action,右下角彈出“Hello World”提示資訊。我們不僅可以建立Group來放置Action,還可以将Action添加進IDE已有的Group當中,如下左圖中,我們将HelloWorld Action添加進了IDE的CutCopyPasteGroup,和複制粘貼等Action放在了一起。
plugin.xml檔案中actions的group可以更為複雜,group可以互相包含,并形成工具欄或菜單(如下圖),有興趣的同學可以拉取Demo(文章末尾)體驗一下。
向導程式Wizard
Wizard意為向導程式,就是指引使用者完成某個功能的程式,通常為單個或多個指引界面組成。例如下面兩幅圖為Android Studio中經典的建立新工程視窗,就包含兩個頁面的向導程式。下面将介紹如何制作出和圖中主題完全相同的向導程式。
向導程式的基礎類屬于android.jar,由以下幾個核心類構成:
1. ModelWizard
向導程式的“主類”,一個ModelWizard包含了一個ModelWizardStep的隊列(Step隊列是一個有序的隊列,并且每個Step都包含了它的下一個Step的引用),當一個ModelWizard結束時,它會周遊所有steps,通路step對應的WizardModel,并調用WizardModel#handleFinished()方法。
2. ModelWizardStep
一個Step就是Wizard向導程式中的一個單獨頁面,它負責建立一個UI界面呈現給使用者,确定頁面上的資訊是否有效,并且将使用者資料儲存在對應的WizardModel對象中。
3. SkippableWizardStep
可以設定可見性的Step,可以通過前一個Step來控制跟在其後的Step可見性,例如一個Step提供了一些選項給使用者,并根據使用者的選擇來決定之後哪些Steps可以被展示。
4. WizardModel
Model就是資料的集合,這些資料由wizard中的每個step進行填充。多個step可以共享同一個model,核心的方法是handleFinished(),當使用者在向導程式中點選了“Finish”按鈕時,wizard結束,這個方法将會被調用進行最終的邏輯處理。
Wizard向導程式工作流程圖
建立一個Android Studio樣式的向導程式
同樣在android.jar庫中,和wizard同級的名叫ui的包中提供了一個很友善的類,幫助使用者建立AS樣式的ModelWizard,隻需将ModelWizard對象作為參數放入StudioWizardDialogBuilder的構造器中即可。使用AS樣式包裝我們的插件UI能讓使用者使用時更有原生的感覺,體驗更好。
class CreateNewProjectAction : AnAction() {
override fun actionPerformed(e: AnActionEvent) {
StudioWizardDialogBuilder(
ModelWizard.Builder().addStep(NewProjectStep()).build(),
"Create New MARS Project"
).build().show()
}
}
class ProjectWizardModel : WizardModel() {
//記錄一些希望儲存的字段
//...
override fun handleFinished() {
//處理最後的邏輯
}
}
class NewProjectStep : ModelWizardStep<ProjectWizardModel?>(ProjectWizardModel(), "Create MARS Project") {
init {
//建立Step頁面的UI
}
//連結下一個Step
override fun createDependentSteps(): MutableCollection<out ModelWizardStep<*>> {
return arrayListOf(SelectBaselineStep(model))
}
}
Tool Windows
Tool Windows是IDE的子視窗。這些視窗通常都在IDE主視窗的“邊框”上擁有屬于自己的一個tool window button,點選後将在IDE主視窗的左、右、下側激活panel來展示資訊。如下圖左一Gradle工具欄。建立Tool Window需要提供一個JPanel,并通過ToolWindowFactory來實作。
ToolWindowFactory
Performs lazy initialization of a tool window registered in {@code plugin.xml}.
使用者必須建立ToolWindowFactory的實作類,并實作createToolWindowContent()方法,在該方法中初始化tool Window的UI,并添加到Android Studio中。ToolWindowFactory提供了懶加載機制,這樣實作的好處是未使用的工具視窗不會增加啟動時間或導緻記憶體使用方面的任何開銷:如果使用者沒有與該工具視窗互動,相關代碼就不會被加載和執行。
public class MyToolWindowFactory implements ToolWindowFactory {
@Override
public void createToolWindowContent(@NotNull Project project, @NotNull ToolWindow toolWindow) {
// 初始化自定義元件對象
MyToolWindow myToolWindow = new MyToolWindow(project, toolWindow);
// 元件添加到AS中
ContentFactory contentFactory = ContentFactory.SERVICE.getInstance();
Content content = contentFactory.createContent(myToolWindow.getContent(), "", false);
toolWindow.getContentManager().addContent(content);
}
}
在插件中使用Tool Windows有兩種形式:
- declarative setup:可以了解為靜态,在
檔案中注冊,始終可見使用者随時都可以使用。plugin.xml
- Programmatic Setup:通過API接口動态注入,可以在一些操作前後出現和隐藏。
Declarative Setup
<extensions defaultExtensionNs="com.intellij">
<!-- Add your extensions here -->
<toolWindow id="MyToolWindow" secondary="true" anchor="right" factoryClass="com.volcengine.plugin.toolwindow.MyToolWindowFactory"/>
</extensions>
Programmatic Setup
updateBaselineBtn.addActionListener(e -> {
BaselineWindow baselineWindow = new BaselineWindow(versionsJson, project, toolWindow);
ContentFactory contentFactory = ContentFactory.SERVICE.getInstance();
Content content = contentFactory.createContent(baselineWindow.getContent(), "", false);
toolWindow.getContentManager().addContent(content);
toolWindow.getContentManager().setSelectedContent(content);
});
UI建立工具
Wizard向導程式和Tool Window工具欄都需要UI作為面闆内容的填充,根本上來說,需要的隻是一個内容豐富的JPanel作為Content。AS 插件中的UI大量的使用了Java Swing元件,是以對Swing比較熟悉的同學上手會很快,這裡介紹幾種在AS插件中生成UI的技巧。
GUI Form
New --> Swing UI Designer --> GUI Form 填寫資訊後就會生成對可視化的.form檔案以及綁定的java類,在對應的java檔案中增加一個getRootPanel方法擷取root panel就可以将建構好的Panel給到向導程式或工具欄中使用。
Eclipse - WindowBuilder
上面提到的GUI Form有一個缺點,隻能使用Java,并且.fome檔案和.java檔案強綁定,我們也無法單獨使用這個生成的java檔案,并且當我們想編寫純Kotlin代碼時,GUI Form就顯得不好用了。
Eclipse是多數同學剛接觸Java時使用的經典IDE,其中有一個WindowBuilder插件同樣可以可視化建立GUI界面,但是相比于GUI Form,WindowBuilder生成的是單獨的.java檔案,使用者在GUI可視化界面操作的每個步驟都會生成對應的源碼,我們可以直接copy這些代碼到AS插件當中,并使用“convert java code to Kotlin”功能,将這些代碼一鍵轉為Kotlin代碼,非常友善(更重要的是,WindowBuilder的使用體驗個人覺得更好)。
Kotlin UI DSL
IntelliJ 插件官方提供的一些基于Kotlin的領域特定語言,可以在Kotlin代碼中寫UI,優點是代碼優美,缺點是累,具體可參考官網的指引https://plugins.jetbrains.com/docs/intellij/kotlin-ui-dsl.html
資料持久化
有時我們希望能儲存使用者在插件中的操作或一些配置,避免重複的工作以及必要資料的讀取,或避免使用者重複多次輸入。IntelliJ Platform提供了一些友善的API來做資料持久化。
Plugin Service
這是IntelliJ插件開發中的基礎能力,分為三種不同的類型,當我們想要在IDE插件的不同生命周期進行一些狀态和邏輯上的處理,就可以使用這三種服務,例如:持久化狀态、訂閱事件、Application啟動/關閉時、Project被打開/關閉時。
Service 接口類型 | 作用描述 |
---|---|
Application Level | IDEA啟動時會初始化,IDEA生命周期中僅存在一個執行個體 |
Project Level | IDEA 會為每一個 Project 執行個體建立一個 Project 級别的執行個體 |
Module Level | IDEA 會為每一個 Project 的加載過的Module執行個體Module級别的執行個體,在多子產品項目中容易導緻記憶體洩露 |
這塊代碼是模拟在IDE啟動時,自動檢驗目前是否存在新版本的功能,若有新版本則進行更新操作,就是使用了持久化存儲來實作的。
@State(name = "DemoConfiguration", storages = [
Storage(value = "demoConfiguration.xml")
])
class DemoComoponent:ApplicationComponent, PersistentStateComponent<DemoComoponent>, Serializable {
var version = 1
var localVersion = 0;
private fun isANerVersion() = localVersion < version
private fun updateVersion(){
localVersion = version
}
override fun initComponent() {
if(isANerVersion()){
updateVersion()
}
}
override fun getState(): DemoComoponent? = this
override fun loadState(state: DemoComoponent) {
XmlSerializerUtil.copyBean(state, this)
}
}
持久化存儲的兩種方式
1. PropertiesComponent
這是一個簡單的Key-Value資料結構,可以當作Map使用,用于儲存application 和 project 級别的資料。
//擷取 application 級别的 PropertiesComponent
PropertiesComponent propertiesComponent = PropertiesComponent.getInstance();
//擷取 project 級别的 PropertiesComponent,指定相應的 project
PropertiesComponent propertiesComponent = PropertiesComponent.getInstance(Project);
// set & get
propertiesComponent.setValue(name, value)
propertiesComponent.getValue(name)
2. PersistentStateComponent
複雜類型的資料結構使用PersistentStateComponent,可以指定持久化的存儲位置。
public interface PersistentStateComponent<T> {
@Nullable
T getState();
void loadState(T state);
}
- 建立一個PersistentStateComponent的實作類,T表示需要持久化的資料結構類型,可以是任意類,甚至是實作類本身,然後重寫getState和loadState方法。
- 若要指定存儲的位置,需要在顯現類上增加*@State*注解。
- 若不希望其中的某個字段被持久化,可以在該字段上增加*@Transient* 注解。
@State(
name = "ChentaoPlugin" ,
storages = [Storage("chentao-plugin.xml")]
)
class AarCheckBoxSettings :PersistentStateComponent<HashMap<String, AarCheckBoxState>> {
var checkBoxStateList = HashMap<String, AarCheckBoxState>()
override fun getState(): HashMap<String, AarCheckBoxState>? {
return checkBoxStateList
}
override fun loadState(stateList: HashMap<String, AarCheckBoxState>) {
checkBoxStateList = stateList
}
//将持久化元件聲明為Serveice的擷取方式是通過ServiceManager
companion object{
@JvmStatic
fun getInstance(): PersistentStateComponent<HashMap<String, AarCheckBoxState>>{
return ServiceManager.getService(AarCheckBoxSettings::class.java)
}
}
}
data class AarCheckBoxState(val componentId:String, val isSelected:Boolean)
注冊持久化元件
PersistentStateComponent的實作類需要在plugin.xml中注冊為 Service後使用。
<extensions defaultExtensionNs="com.intellij">
<applicationService serviceImplementation="com.volcengine.plugin.actions.AarCheckBoxSettings"/>
</extensions>
插件打包與安裝
- 打包:在Gradle工具欄中運作assemble任務,即可在/build/distribution/{插件名稱}-{插件版本}.zip路徑下找到打包好的插件zip包。
- 本地安裝:還沒将插件釋出到插件市場前我們可以選擇安裝本地插件,打開AS菜單欄/Android Studio/Preference/Plugins/Install Plugin from Disk... 安裝後即可使用。
- 釋出插件市場:
- 通路https://hub.jetbrains.com/,建立賬号。
- 使用賬号登陸jetbrains marketplace https://plugins.jetbrains.com/,釋出插件(需官方稽核2個工作日)。
- 插件的第一個版本都需要在網站手動上傳,之後的版本可以使用hub賬号中的token自動更新。
總結
回顧開發過程:IDE插件的核心步驟:安裝正确版本IntelliJ --> 配置工程 --> 建立Action --> 将複雜流程注入Wizard向導程式或ToolWindow工具欄(同時建立UI) --> 使用資料持久化儲存必要資料 --> 打包&安裝&釋出。
筆者覺得IDE插件開發的難點主要是摸索的過程,IDE插件較冷門,網上介紹的文章很少,官網介紹了一些功能群組件後也沒有詳細的API指引,令人有點無從下手。最終,通過反編譯檢視一些官方插件(Firebase、Flutter等)的源碼,以及收集Google、Youtube和各大部落格的資訊,終于将AS插件的一期雛形打造完畢,也将學到的一些常用的通用能力在本文中進行整理,希望能幫到之後想要接觸AS插件開發的同學。
Demo
https://github.com/ChentaoZhou/ChentaoDemo
關于位元組終端技術團隊
位元組跳動終端技術團隊(Client Infrastructure)是大前端基礎技術的全球化研發團隊(分别在北京、上海、杭州、深圳、廣州、新加坡和美國山景城設有研發團隊),負責整個位元組跳動的大前端基礎設施建設,提升公司全産品線的性能、穩定性和工程效率;支援的産品包括但不限于抖音、今日頭條、西瓜視訊、飛書、懂車帝等,在移動端、Web、Desktop等各終端都有深入研究。
就是現在!用戶端/前端/服務端/端智能算法/測試開發 面向全球範圍招聘!一起來用技術改變世界,感興趣請聯系 [[email protected]](mailto:[email protected]),郵件主題 履歷-姓名-求職意向-期望城市-電話。
火山引擎應用開發套件MARS是位元組跳動終端技術團隊過去九年在抖音、今日頭條、西瓜視訊、飛書、懂車帝等 App 的研發實踐成果,面向移動研發、前端開發、QA、 運維、産品經理、項目經理以及營運角色,提供一站式整體研發解決方案,助力企業研發模式更新,降低企業研發綜合成本。