[译] 编写AndroidStudio插件:集成Jira
Posted 却把清梅嗅
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了[译] 编写AndroidStudio插件:集成Jira相关的知识,希望对你有一定的参考价值。
原文: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 [译] 编写AndroidStudio插件:设置页
[译] 编写AndroidStudio插件:创建一个基本插件