原文:Write an Android Studio Plugin Part 4: Jira Integration
作者:Marcos Holgado
译者:却把清梅嗅
《编写AndroidStudio插件》系列是 IntelliJ IDEA 官方推荐的学习IDE插件开发的博客专栏,希望对有需要的读者有所帮助。
在本系列的第三部分中,我们学习了如何使用Component
对数据进行持久化,并利用这些数据来创建新的设置页面。在今天的文章中,我们将使用这些数据将Jira
与我们的插件快速集成在一起。
请记住,您可以在GitHub
上找到本系列的所有代码,还可以在对应的分支上查看每篇文章的相关代码,本文的代码在Part4
分支中。
https://github.com/marcosholgado/plugin-medium
我们要做什么?
今天这篇文章的目的是解释如何将第三方API
和库集成到插件中。我将应用一个简单的MVP
模式,您可以更改为MVC
或任何您喜欢的开发模式。
今天,我们将Jira
集成到我们的插件中,我们要做的是能够在Android Studio
中将Jira Scrum
板上的issue
移至下一栏。因为Jira
的issue ID
将基于我们当前的git
分支,所以我们的插件将从我们当前的分支中解析issue ID
,而不是强迫用户手动输入或从其他位置选择issue ID
。
在开始前,我们先进行一些假设。
- 1、在移动
issue
(比如记录时间等)之前,您无需填写任何必填字段,否则你的UI
将需要额外的字段供用户输入; - 2、我们将使用
Jira Cloud Platform API v3
; - 3、为了简单起见,我们还将使用 基本身份验证(
Basic auth
) ,因为我们的目标是学习如何集成第三方工具,而不是如何在Jira
中进行正确的身份验证。
除非您正在构建仅供内部使用的工具(例如脚本和机器人),否则我们(
Jira
)不建议使用基本身份验证。
这就是我的面板页在Jira
中展示出来的样子,issue
只能向前推进,并且只能从一列移至下一列,您不能跳过流程中的任何一列。
第一步
首先,我们将在设置页面中添加更多字段。根据我们的目标,我们需要添加一个新的regex
字段,其中将包含一个正则表达式以从当前分支中提取issue ID
。我们还将需要一个Jira URL
字段,该字段将用作API
调用的基本URL
。最后,Jira API
需要使用auth
令牌而不是密码,为了清楚起见,我将旧密码字段的名称更改为token
。
这些变动应该简单直观,如下所示:
@State(name = "JiraConfiguration",
storages = [Storage(value = "jiraConfiguration.xml")])
class JiraComponent(project: Project? = null) :
AbstractProjectComponent(project),
Serializable,
PersistentStateComponent<JiraComponent> {
var username: String = ""
var token: String = ""
var url: String = ""
var regex: String = ""
override fun getState(): JiraComponent? = this
override fun loadState(state: JiraComponent) =
XmlSerializerUtil.copyBean(state, this)
companion object {
fun getInstance(project: Project): JiraComponent =
project.getComponent(JiraComponent::class.java)
}
}
class JiraSettings(private val project: Project): Configurable, DocumentListener {
private val tokenField: JPasswordField = JPasswordField()
private val txtUsername: JTextField = JTextField()
private val txtUrl: JTextField = JTextField()
private val txtRegEx: JTextField = JTextField()
private var modified = false
override fun isModified(): Boolean = modified
override fun getDisplayName(): String = "MyPlugin Jira"
override fun apply() {
val config = JiraComponent.getInstance(project)
config.username = txtUsername.text
config.token = String(tokenField.password)
config.url = txtUrl.text
config.regex = txtRegEx.text
modified = false
}
override fun changedUpdate(e: DocumentEvent?) {
modified = true
}
override fun insertUpdate(e: DocumentEvent?) {
modified = true
}
override fun removeUpdate(e: DocumentEvent?) {
modified = true
}
override fun createComponent(): JComponent {
val mainPanel = JPanel()
mainPanel.setBounds(0, 0, 452, 254)
mainPanel.layout = null
val lblUsername = JLabel("Username")
lblUsername.setBounds(30, 25, 83, 16)
mainPanel.add(lblUsername)
val lblPassword = JLabel("Token")
lblPassword.setBounds(30, 74, 83, 16)
mainPanel.add(lblPassword)
val lblUrl = JLabel("Jira URL")
lblUrl.setBounds(30, 123, 83, 16)
mainPanel.add(lblUrl)
val lblRegEx = JLabel("RegEx")
lblRegEx.setBounds(30, 172, 83, 16)
mainPanel.add(lblRegEx)
txtUsername.setBounds(125, 20, 291, 26)
txtUsername.columns = 10
mainPanel.add(txtUsername)
tokenField.setBounds(125, 69, 291, 26)
mainPanel.add(tokenField)
txtUrl.setBounds(125, 118, 291, 26)
txtUrl.columns = 10
mainPanel.add(txtUrl)
txtRegEx.setBounds(125, 167, 291, 26)
txtRegEx.columns = 10
mainPanel.add(txtRegEx)
val config = JiraComponent.getInstance(project)
txtUsername.text = config.username
tokenField.text = config.token
txtUrl.text = config.url
txtRegEx.text = config.regex
tokenField.document?.addDocumentListener(this)
txtUsername.document?.addDocumentListener(this)
txtUrl.document?.addDocumentListener(this)
txtRegEx.document?.addDocumentListener(this)
return mainPanel
}
}
在进行下一步之前,请确保这些更改确实有效,并且新字段的数据已正确进行了保存。
第二步:您仍在编写代码!
现在,我们可以创建一个新的Action
,将所有代码插入其中,然后转到下一篇文章,但我们不会这样做,因为:
您仍在编写代码! -Marcos Holgado(就是我!)
仅仅因为这是一个插件,并不意味着您不必对其进行维护或遵循任何编码规范。我看到太多插件,其中所有代码都在一个Action
中。我不明白,如果您不想将所有代码都放在Action
中,为什么还要这么做?
今天,我将使用MVP
模式,但您可以使用任何您喜欢的模式,只要您遵循经典的编码规范,就不会有太大的不同。我们的看起来像这样:
我们的JiraMoveAction
将创建一个新的JiraMoveDialog
,它将具有一个JiraMoveDialogPresenter
,该JiraMoveDialogPresenter
将与Model
,网络等进行通信。此外,JiraMoveDialog
将创建一个JiraMovePanel
,其唯一原因是要分离更多的UI
层,我将解释说在步骤4中。
除此之外,我们将使用Retrofit
将对Jira API
和Dagger2
的网络请求用作DI
框架(您知道我有点喜欢Dagger
)。
首先,我们将在action package
中创建一个新的package
,以将Action
与其余代码进一步分开,然后创建所有提及的文件。
第三步:Models
我们的Model
将非常简单,因为我们只需要处理Transition
,并且由于不处理在Transition
的必填字段。我们唯一需要的信息是Transition id
和Transition name
。如果您需要配置其他东西(例如添加评论),请点击这里查看文档。
我将把这些Model
放在network
包下的Models.kt
文件中,实现如下:
data class Transition(val id: String, val name: String = "") {
override fun toString(): String = name
}
data class TransitionsResponse(val transitions: List<Transition>)
data class TransitionData(val transition: Transition)
第四步:JiraMovePanel
顾名思义,该文件将成为具有所需UI
的JPanel
。我们不会在此处创建任何 OK 或 Cancel 按钮,因为这将作为JiraMoveDialog
的一部分出现,但我将在下一小节中进行讨论。
现在,我们将创建一个非常简单的UI
,其包含一个combobox
(用于显示可用的Transition
)和一个text field
(用于显示issue ID
),这个text field
让用户根据需要手动进行配置。
我们的JiraMovePanel
类继承自JPanel
,根据上文,我们将在Eclipse
中创建UI
,复制粘贴代码并将其转换为Kotlin
。
与我们在设置页面上所做的操作相比,存在一些差异,这是因为我们继承了JPanel
,我们已经在Panel
中,因此我们可以直接调用add()
。
我们还必须重写getPreferredSize()
来设置Panel
的大小,不要忘记这样做!
最后,我添加了一些方法,这些方法将从JiraMoveDialog
中调用以更改字段的值,最终文件如下所示:
class JiraMovePanel : JPanel() {
private val comboTransitions = ComboBox<Transition>()
val txtIssue = JTextField()
init {
initComponents()
}
private fun initComponents() {
layout = null
val lblJiraTicket = JLabel("Issue")
lblJiraTicket.setBounds(25, 33, 77, 16)
add(lblJiraTicket)
txtIssue.setBounds(114, 28, 183, 26)
add(txtIssue)
val lblTransition = JLabel("Transition")
lblTransition.setBounds(25, 75, 77, 16)
add(lblTransition)
comboTransitions.setBounds(114, 71, 183, 27)
add(comboTransitions)
}
override fun getPreferredSize() = Dimension(300, 110)
fun addTransition(transition: Transition) = comboTransitions.addItem(transition)
fun setIssue(issue: String) {
txtIssue.text = issue
}
fun getTransition() : Transition = comboTransitions.selectedItem as Transition
}
第五步:展示UI
让我们确保刚才所创用户界面显示的正确性,我们首先需要将JiraMoveDialog
与JiraMovePanel
链接起来,因此我们来实现JiraMoveDialog
。
首先,我们需要继承DialogWrapper
,IntelliJ
提供了这个包装器,我们应该将其用于插件中的所有模式的对话框。IntelliJ
还提供了一些免费功能,例如OK
或Cancel
按钮,因此我们不必在JPanel
中创建它们。
我们还必须重写createCenterPanel()
以返回刚刚创建的Panel
,并在初始化对话框时调用init()
。现在,这是我们的JiraMoveDialog
类:
class JiraMoveDialog constructor(val project: Project):
DialogWrapper(true) {
init {
init()
}
override fun createCenterPanel(): JComponent? {
return JiraMovePanel()
}
}
现在,我们可以在我们的JiraMoveAction
中创建一个新对话框,这对您现在来说应该很简单,因此我不再赘述。
class JiraMoveAction : AnAction() {
override fun actionPerformed(event: AnActionEvent) {
val dialog = JiraMoveDialog(event.project!!)
dialog.show()
}
}
最后一步是将新的Action
添加到plugin.xml
文件中。
您现在可以调试插件,执行Action
,并且应该看到以下的内容:
第六步:DI 和获取 Git 分支
到目前为止,我们已经完成了UI
工作,让我们开始使用Presenter
并将Dagger2
集成到项目中。
注意:如果不需要,您不必使用
Dagger
,但我建议像在其他任何项目中一样,使用某种形式的依赖注入。
要添加Dagger
,我们只需像通常那样,在build.gradle
文件中添加依赖项即可。别忘了也添加kapt
。请注意,由于intellij-gradle-plugin
尚不支持implementation
或api
,因此我们尚未使用它们。
apply plugin: 'kotlin-kapt'
dependencies {
compile 'org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.3.10'
compile 'com.google.dagger:dagger:2.20'
kapt 'com.google.dagger:dagger-compiler:2.20'
}
我们还希望从IDE
中获取当前分支,为此,我们需要添加插件依赖。每当您需要第三方插件依赖(例如android
插件或任何其他插件)时,都必须将该插件添加到gradle
文件的插件列表中。在我们的例子中,我们将需要git4idea
插件。
intellij {
version '2018.1.6'
plugins = ['git4idea']
alternativeIdePath '/Applications/Android Studio.app'
}
我们还必须在plugin.xml
文件中添加依赖。
<depends>Git4Idea</depends>
添加了所有依赖后,我们可以专注于依赖注入。首先,我将创建一个新的Dagger Component
以及一个Module
,将来它将帮助我们进行测试,并使我们的架构更整洁。
现在,我们将只注入我们正在处理的project
,即JiraMoveDialog
(View
层)和保存我们的设置的JiraComponent
。 我还将Component
命名为JiraDIComponent
,因此我们不会把它和用于保存设置的JiraComponent
相混淆。
@Component(modules = [JiraModule::class])
interface JiraDIComponent {
fun inject(jiraMoveDialog: JiraMoveDialog)
}
@Module
class JiraModule(
private val view: JiraMoveDialog,
private val project: Project
) {
@Provides
fun provideView() : JiraMoveDialog = view
@Provides
fun provideProject() : Project = project
@Provides
fun provideComponent() : JiraComponent =
JiraComponent.getInstance(project)
}
如果您对依赖注入或Dagger不熟悉,建议您看一下Jake Wharton的演讲:
https://www.youtube.com/watch?v=plK0zyRLIP8
现在,我们可以使用Dagger
创建Presenter
并将所需一切注入到构造函数中,要获得当前正在使用的分支实际上非常简单,只需从当前项目中获得一个repository manager
即可。 通常,您将有一个repository
,因此您只需调用first()
并获取当前分支的名称。之后,通过使用存储在我们设置中的正则表达式,我们可以匹配并找到Jira
中issue
的ID
。
我将在设置中存储的正则表达式为[a-zA-Z] +-[0-9] +
,因为Jira ID
的格式为Project-Number
(即DROID-12
),并且我为分支命名作为DROID-12-this-is-a-bug
。
class JiraMoveDialogPresenter @Inject constructor(
private val view: JiraMoveDialog,
private val project: Project,
private val component: JiraComponent
) {
fun load() {
getBranch()
}
private fun getBranch() {
val repositoryManager = GitRepositoryManager.getInstance(project)
val repository = repositoryManager.repositories.first()
val ticket = repository.currentBranch!!.name
val match = Regex(component.regex).find(ticket)
match?.let {
view.setIssue(match.value)
}
}
}
回到JiraMoveDialog
,我们必须注入Presenter
,并实现setIssue()
方法以根据Git
分支更改字段的值。为此,我们将创建一个JPanel
的变量,而非在createCenterPanel()
上返回新的JPanel
,然后可以使用该Panel
来更改字段的值。
我将isModal
设置为true
。每当我们将modal
设置为true
时,我们都会阻止UI
,因此用户必须退出我们的对话框才能再次与IDE
交互,如果需要,可以随意更改该值。然后,我们调用presenter.load()
从IDE
中获取分支,和之前一样,我们还须调用init()
。
class JiraMoveDialog constructor(project: Project):
DialogWrapper(true) {
@Inject
lateinit var presenter: JiraMoveDialogPresenter
private val panel : JiraMovePanel = JiraMovePanel()
init {
DaggerJiraDIComponent.builder()
.jiraModule(JiraModule(this, project))
.build().inject(this)
isModal = true
presenter.load()
init()
}
override fun createCenterPanel(): JComponent? = panel
fun setIssue(issue: String) = panel.setIssue(issue)
}
如果现在运行插件,并在设置中使用我之前提到的正则表达式,您将看到,每当启动JiraMoveAction
时,它都会根据当前分支将issue
字段设置为Jira
正确的issue ID
。
第七步:用Retrofit和RxJava请求网络
剩下唯一要做的事情就是为Jira
的issue
获取下一个Transition
,并在用户按下OK
按钮时移动issue
。为此,我们将使用Retrofit
和RxJava
。
和往常一样,首先将新的依赖声明在build.gradle
文件中。
compile 'com.squareup.retrofit2:retrofit:2.5.0'
compile 'com.squareup.retrofit2:adapter-rxjava2:2.5.0'
compile 'com.squareup.retrofit2:converter-gson:2.5.0'
compile 'io.reactivex.rxjava2:rxjava:2.2.5'
compile 'com.github.akarnokd:rxjava2-swing:0.3.3' // 译者注:注意这个compile
最后compile
的依赖,可能会让你耳目一新,在RxJava
中,我们需要一组Scheduler
来进行订阅和观察。我们显然不能使用Android
的,而是需要在事件分发线程或EDT
中运行代码,新库将为我们提供EDT
的调度程序。
我们将从编写JiraService
开始,查看Jira API
文档后,实现起来并不复杂:
interface JiraService {
@GET("issue/{issueId}/transitions")
fun getTransitions(@Header("Authorization") authKey: String,
@Path("issueId") issueId: String): Single<TransitionsResponse>
@POST("issue/{issueId}/transitions")
fun doTransition(@Header("Authorization") authKey: String,
@Path("issueId") issueId: String,
@Body transitionData: TransitionData): Completable
}
现在我们可以通过Dagger
将JiraService
的依赖向外暴露:
@Provides
fun providesJiraService(component: JiraComponent) : JiraService {
val jiraURL = component.url
val retrofit = Retrofit.Builder()
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(
RxJava2CallAdapterFactory.create())
.baseUrl(jiraURL)
.build()
return retrofit.create(JiraService::class.java)
}
在Presenter
中,我们现在可以注入JiraService
并将其与RxJava
一起使用,以获取给定issue
的Transition
。
请注意,我们使用SwingSchedulers.edt()
进行线程切换。代码非常简单,使用Basic Auth
,我们将获得所有Transition
的响应,然后将其传递到视图(JiraMoveDialog
),以将其添加到组合框。
如果发生error
,我们在view.error()
处理,它将显示带有错误详细信息的通知弹窗。
private fun getTransitions() {
val auth = getAuthCode()
disposable = jiraService.getTransitions(auth, issue)
.subscribeOn(Schedulers.io())
.observeOn(SwingSchedulers.edt())
.subscribe(
{ response ->
view.setTransitions(response.transitions)
},
{ error ->
view.error(error)
}
)
}
private fun getAuthCode() : String {
val username = component.username
val token = component.token
val data: ByteArray =
"$username:$token".toByteArray(Charsets.UTF_8)
return "Basic ${Base64.encode(data)}"
}
对话框的新方法可以设置Transition
并显示error
:
fun setTransitions(transitionList: List<Transition>) {
for(transition in transitionList) {
panel.addTransition(transition)
}
}
fun error(throwable: Throwable) {
val noti = NotificationGroup("myplugin",
NotificationDisplayType.BALLOON, true)
noti.createNotification("Error", throwable.localizedMessage,
NotificationType.ERROR, null).notify(project)
}
立即运行插件,完成需要的配置后,效果如下:
第八步:执行 Transitions
最后一步,当用户按下OK
按钮时,将issue
移到组合框中显示的Transition
中,Presenter
中的代码再次使用RxJava
,如下所示。
fun doTransition(selectedItem: Transition, issue: String) {
val auth = getAuthCode()
val transition = TransitionData(selectedItem)
disposable = jiraService.doTransition(auth, issue, transition)
.subscribeOn(Schedulers.io())
.observeOn(SwingSchedulers.edt())
.subscribe(
{
view.success()
},
{ error ->
view.error(error)
}
)
}
现在,在对话框中,我们必须重写doOKAction()
以执行从Presenter
传递过来的Transition
,我们还创建了success()
以关闭对话框并通知用户。
override fun doOKAction() =
presenter.doTransition(
panel.getTransition(), panel.txtIssue.text
)
fun success() {
close(DialogWrapper.OK_EXIT_CODE)
val noti = NotificationGroup("myplugin",
NotificationDisplayType.BALLOON, true)
noti.createNotification("Success", "Issue moved",
NotificationType.INFORMATION, null).notify(project)
}
如果您现在运行插件,则无需离开Android Studio
就可以进行Jira
的更新。如果一切顺利,您现在应该会看到此弹出窗口,最重要的是,Jira
中的issue
现在应该移至下一阶段。
这就是所有的内容了!如有需要,您现在可以进行更多扩展,并创建Action
以使所有issue
都处于ToDo
状态、显示说明并在选择它们后使用正确的issue id
创建新分支(或查看我的Droidcon UK演讲以获取代码)。
请记住,本文的代码在该系列GitHub Repo的Part4
分支中可用。
在下一篇文章中,我们将整理一下代码,并创建一些util
方法以在代码中复用。我们还将研究如何以比硬编码更好的方式存储字符串。
在此之前,如果您有任何问题,请随时发表评论或在Twitter上关注我。
《编写AndroidStudio插件》译文系列
- 译: 编写AndroidStudio插件(一):创建一个基本插件
- 译: 编写AndroidStudio插件(二):持久化数据
- 译: 编写AndroidStudio插件(三):设置页
- 译: 编写AndroidStudio插件(四):集成Jira
- 译: 编写AndroidStudio插件(五):本地化和通知
关于译者
Hello,我是 却把清梅嗅 ,如果您觉得文章对您有价值,欢迎 ❤️,也欢迎关注我的 博客 或者 GitHub。
如果您觉得文章还差了那么点东西,也请通过 关注 督促我写出更好的文章——万一哪天我进步了呢?
- 我的Android学习体系
- 关于文章纠错
- 关于知识付费
- 关于《反思》系列