Kotlin之美——DSL篇 和 如何讓你的回調更具Kotlin風味

 

如何讓你的回調更具Kotlin風味
https://mp.weixin.qq.com/s?__biz=MzAwOTQ4Mzk2Nw==&mid=2458585682&idx=1&sn=641073020fae0050f3a4523bd107c6c2&chksm=8c214982bb56c094c2e0c1d3d3decd111d9241006d4fdf80e5b71c4e502a5c61936eaab80910&scene=0&xtrack=1#rd

Kotlin之美——DSL篇 - 簡書
https://www.jianshu.com/p/f5f0d38e3e44

 

 

 

Kotlin之美——DSL篇

96 geniusmart 關注

2018.03.02 11:54* 字數 2618 閱讀 4214評論 5喜歡 41讚賞 1

Kotlin 系列:

Kotlin DSL 把 Kotlin 的語法糖演繹得淋漓盡致,這些語法糖可謂好吃、好看又好玩,但是,僅癡迷於語法糖只會對語言的理解遊離於表面,瞭解其實現原理,是我們閱讀優秀源碼、設計整潔代碼和理解編程語言的必經之路,本文我們通過 DSL 來感受 Kotlin 之美。

理解 DSL

DSL(domain specific language),即領域專用語言:專門解決某一特定問題的計算機語言,比如大家耳熟能詳的 SQL 和正則表達式。

通用編程語言 vs DSL

通用編程語言(如 Java、Kotlin、Android等),往往提供了全面的庫來幫助開發者開發完整的應用程序,而 DSL 只專注於某個領域,比如 SQL 僅支持數據庫的相關處理,而正則表達式只用來檢索和替換文本,我們無法用 SQL 或者正則表達式來開發一個完整的應用。

API vs DSL

無論是通用編程語言,還是領域專用語言,最終都是要通過 API 的形式向開發者呈現。良好的、優雅的、整潔的、一致的 API 風格是每個優秀開發者的追求,而 DSL 往往具備獨特的代碼結構和一致的代碼風格,從 SQL 和正則表達式的語法風格便可感受一二。

下文我們也將提到,Kotlin 構建的 DSL,代碼風格更具表現力和想象力,也更加優雅。

內部 DSL

但是,如果爲解決某一特定領域問題就創建一套獨立的語言,開發成本和學習成本都很高,因此便有了內部 DSL 的概念。所謂內部 DSL,便是使用通用編程語言來構建 DSL。比如,本文提到的 Kotlin DSL,我們爲 Kotlin DSL 做一個簡單的定義:

“使用 Kotlin 語言開發的,解決特定領域問題,具備獨特代碼結構的 API 。”

下面,我們就來領略下千變萬化的 Kotlin DSL 。

有趣的 Kotlin DSL

如果說 Kotlin 是一位魔術師,那麼 DSL 便是其賴以成名,令人嘖嘖稱讚的魔術作品,我們先來看下 Kotlin 在各個特定領域的有趣實現。

  1. 日期
val yesterday = 1.days.ago // 也可以這樣寫: val yesterday = 1 days ago
val twoMonthsLater = 2 months fromNow

以上日期處理的代碼,真正做到見名知意,深諳代碼整潔之道,更多細節可參考此庫:kxdate 。

如果不考慮規範,基於該庫的設計思路,我們甚至可以設計出如下的 api:

val yesterday = 1 天 前
val twoMonthsLater = 2 月 後

這個日期處理領域的 DSL 體現出來的代碼結構是鏈式的,並且近似於我們日常使用的英語

  1. 單元測試
val str = "kotlin"
str should startWith("kot")
str.length shouldBe 6

與上述日期庫的 api 風格類似,該單元測試的代碼也是賞心悅目,更多細節可參考此庫:kotlintest 。

基於該庫的設計思路,我們甚至可以實現如下的代碼風格,如同寫英語句子一般簡潔:

"kotlin" should start with "kot"
"kotlin" should have substring "otl"

這個 DSL 的代碼結構近似於我們日常使用的英語。

  1. HTML 構建器
fun createTable() = 
    table{
        tr{
            td{
                
            }
        }
    }
    
>>> println(createTable())
<table><tr><td></td></tr></table>

這個 DSL 的代碼結構使用了 lambda 嵌套,並且語義清晰,一目瞭然。更多詳情參考此庫:kotlinx.html

  1. SQL
(Users innerJoin Cities).slice(Users.name, Cities.name).
            select {(Users.id.eq("andrey") or Users.name.eq("Sergey")) and
                    Users.id.eq("sergey") and Users.cityId.eq(Cities.id)}.forEach {
            println("${it[Users.name]} lives in ${it[Cities.name]}")
        }

這類 SQL api 的風格,如果有用過 ORM 的框架,如 ActiveAndroid 或者 Realm 就不會陌生。以上代碼來自於此庫:Exposed 。

  1. Android 佈局

Anko Layouts 是一套幫助我們更簡潔的開發和複用 Android 佈局的 DSL ,它的代碼風格如下:

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
       
        super.onCreate(savedInstanceState)
        verticalLayout {
            val name = editText()
            button("Say Hello") {
                onClick { toast("Hello, ${name.text}!") }
            }
        }

    }
   
}

相比於笨重的 XML 佈局方式,Anko DSL 顯然是更先進和更高效的解決方案。

  1. Gradle 構建

Gradle 的構建腳本是 groovy,對 Android 程序員有一定的學習成本,目前,Gradle 官方也提供了基於 Kotlin 的構建腳本:Gradle Kotlin DSL , 並提供了類 groovy 的代碼風格:

dependencies {
    compile("com.android.support:appcompat-v7:27.0.1")
    compile("com.android.support.constraint:constraint-layout:1.0.2")
}

完整代碼請參考:build.gradle.kts

綜上,Kotlin DSL 所體現的代碼結構有如下特點:鏈式調用,大括號嵌套,並且可以近似於英語句子。

實現原理

看了那麼多 Kotlin DSL 的風格和使用場景,相較於刻板的、傳統的 Java 而言,更加神奇和富有想象力。要理解 Kotlin DSL 這場魔術盛宴,就必須瞭解其背後用到的魔術道具——擴展函數、lambda、中綴調用和 invoke 約定。

擴展函數(擴展屬性)

對於同樣作爲靜態語言的 Kotlin 來說,擴展函數(擴展屬性)是讓他擁有類似於動態語言能力的法寶,即我們可以爲任意對象動態的增加函數或屬性。

比如,爲 String 擴展一個函數: lastChar():

package strings

fun String.lastChar(): Char = this.get(this.length - 1)

調用擴展函數:

>>> println("Kotlin".lastChar())
n

與 JavaScript 這類動態語言不一樣,Kotlin 實現原理是: 提供靜態工具類,將接收對象(此例爲 String )做爲參數傳遞進來,以下爲該擴展函數編譯成 Java 的代碼

/* Java */
char c = StringUtilKt.lastChar("Java");

回顧前文講到的日期的 DSL:

val yesterday = 1.days.ago

爲配合擴展函數,我們先降低 api 的整潔程度,先實現一個擴展函數的版本:

val yesterday = 1.days().ago()

1 爲 Int 類型,顯然 Int 並沒有 days() 函數,因此days() 爲擴展函數,僞代碼如下:

fun Int.days() = {//邏輯實現}

結合 Java8 的 Time api,此處將會涉及到兩個擴展函數,完整實現如下:

fun Int.days() = Period.ofDays(this)
fun Period.ago() = LocalDate.now() - this

若要實現最終的效果,實際上就是將擴展函數修改爲擴展屬性的方式即可(擴展屬性需提供getter或setter,本質上等同於擴展函數):

val Int.days:Period
    get() = Period.ofDays(this)

val Period.ago:LocalDate
    get() = LocalDate.now() - this

代碼雖少,卻天馬行空,妙趣橫生。

lambda

lambda 爲 Java8 提供的新特性,於2014年3月18日發佈。在2018年的今天我們依然無法使用或者要花很大的代價才能在 Android 編程中使用,而 Kotlin 則幫助我們解決了這一瓶頸,這也是我們擁抱 Kotlin 的原因之一。

lambda 是構建整潔代碼的一大利器。

1. lambda 表達式

下圖是 lambda 表達式,他總是用一對大括號包裝起來,可以作爲值傳遞給下節要提到的高階函數。

圖片來自 Kotlin in Action

2. 高階函數

關於高階函數的定義,參考《Kotlin 實戰》:

高階函數就是以另一個函數作爲參數或返回值的函數

如果用 lamba 來作爲高價函數的參數(此時爲形參),就必須先了解如何聲明一個函數的形參類型,如下:

 

圖片來自於 Kotlin in Action

相對於上一小節,我們應該弄清楚 lambda 作爲實參和形參時的表現形式:

// printSum 爲高階函數,定義了 lambda 形參
fun printSum(sum:(Int,Int)->Int){
        val result = sum(1, 2)
        println(result)
}

// 以下 lambda 爲實參,傳遞給高階函數 printSum
val sum = {x:Int,y:Int->x+y}
printSum(sum)

有了高階函數,我們可以很輕易地做到一個 lambda 嵌套另一個 lambda 的代碼結構

3. 大括號放在最後

Kotlin 的 lambda 有個規約:如果 lambda 表達式是函數的最後一個實參,則可以放在括號外面,並且可以省略括號,如:

person.maxBy({ p:Person -> p.age })

// 可以寫成
person.maxBy(){
    p:Person -> p.age
}

// 更簡潔的風格:
person.maxBy{
    p:Person -> p.age
}

這個規約是 Kotlin DSL 實現嵌套結構的本質原因,比如上文提到的 anko Layout:

verticalLayout {
            val name = editText()
            button("Say Hello") {
                onClick { toast("Hello, ${name.text}!") }
            }
        }

這裏 verticalLayout 中 嵌套了 button,想必該庫定義瞭如下函數:

fun verticalLayout( ()->Unit ){
    
}

fun button( text:String,()->Unit ){
    
}

verticalLayout 和 button 均是高階函數,結合大括號放在最後的規約,就形成了 lambda 嵌套的語法結構。

4. 帶接收者的 lambda

lambda 作爲形參函數聲明時,可以攜帶接收者,如下圖:

圖片來自於 Kotlin in Action

帶接收者的 lambda 豐富了函數聲明的信息,當傳遞該 lambda值時,將攜帶該接收者,比如:

// 聲明接收者
fun kotlinDSL(block:StringBuilder.()->Unit){
  block(StringBuilder("Kotlin"))
}

// 調用高階函數
kotlinDSL {
  // 這個 lambda 的接收者類型爲StringBuilder
  append(" DSL")
  println(this)
}

>>> 輸出 Kotlin DSL

總而言之,lambda 在 Kotlin 和 Kotlin DSL 中扮演着很重要的角色,是實現整潔代碼的必備語法糖。

中綴調用

Kotlin 中有種特殊的函數可以使用中綴調用,代碼風格如下:

"key" to "value"

// 等價於
"key.to("value")

而 to() 的實現源碼如下:

infix fun Any.to(that:Any) = Pair(this,that)

這段源碼理解起來不難,infix 修飾符代表該函數支持中綴調用,然後爲任意對象提供擴展函數 to,接受任意對象作爲參數,最終返回鍵值對。

回顧下我們上文提到的不太規範的中文 api:

val yesteraty = 1 天 前

使用擴展函數和中綴調用便可實現:

object 前
infix fun Int.天(ago:前) = LocalDate.now() - Period.ofDays(this)

再比如上文提到的:

"kotlin" should start with "kot"

// 等價於
"kotlin".should(start).with("kot")

使用兩個中綴調用便可實現,以下是僞代碼:

object start
infix fun String.should(start:start):String = ""
infix fun String.with(str:String):String = ""

所以,中綴調用是實現類似英語句子結構 DSL 的核心。

invoke 約定

Kotlin 提供了 invoke 約定,可以讓對象向函數一樣直接調用,比如:

class Person(val name:String){
    operator fun invoke(){
        println("my name is $name")
    }
}

>>>val person = Person("geniusmart")
>>> person()
my name is geniusmart

回顧上文提到的 Gradle Kotlin DSL:

dependencies {
    compile("com.android.support:appcompat-v7:27.0.1")
    compile("com.android.support.constraint:constraint-layout:1.0.2")
}

// 等價於:
dependencies.compile("com.android.support:appcompat-v7:27.0.1")
dependencies.compile("com.android.support.constraint:constraint-layout:1.0.2")

這裏,dependencies 是一個實例,既可以調用成員函數 compile,同時也可以直接傳遞 lambda 參數,後者便是採用了 invoke 約定,實現原理簡化如下:

class Dependencies{

    fun compile(coordinate:String){
        println("add $coordinate")
    }

    operator fun invoke(block:Dependencies.()->Unit){
        block()
    }
}

>>>val dependencies = Dependencies()
>>>// 以兩種方式分別調用 compile()

invoke 約定讓對象調用函數的語法結構更加簡潔。

總結

細細品味 Kotlin,你會發現她將代碼整潔之道(Clean Code)和高效 Java 編程(Effective Java)中的部分精華融入到的語法和默認的規約中,因此她可以讓開發者無形中寫出整潔和高效的代碼。

而更進一步, Kotlin DSL 則是對 Kotlin 所有語法糖的一個大融合,她的代碼結構通常是鏈式調用、lambda 嵌套,並且接近於日常使用的英語句子,我們可以愉悅的使用 DSL 風格的 API,同時,也可以以此爲思路,爲社區貢獻各種 Kotlin DSL。

Kotlin DSL 體現了代碼的整潔之道,體現了天馬行空的想象力,在 DSL 的點綴下,Kotlin 顯示出整潔的美,自由的美。

Kotlin 有趣的外表之下,是一個更有趣的靈魂。

參考資料

  • 《Kotlin 實戰》

 

 

 

 

 

 

 

 

 

 

=====================================================

=====================================================

=====================================================

 

 

 

 

 

 

 

 

 

 

 

如何讓你的回調更具Kotlin風味

原創: mikyou Kotlin開發者聯盟 今天

簡述: 這應該是2019年的第一篇文章了,臨近過年回家一個月需求是真的很多,正如康少說的那樣,一年的需求幾乎都在最後一兩月寫完了。所以寫文章也擱置了很久,當然再忙每天都會刷掘金。很久就一直在使用Kotlin寫項目,說實話到目前爲止Kotlin用的是越來越順手了(心裏只能用美滋滋來形容了)。當然這次依然講的是Kotlin,說下我這次需求開發中自己一些思考和實踐。其中讓自己感受最深的就是: "Don't Repeat Yourself"。當你經常寫一些重複性的代碼,不妨停下來想下是否要去改變這樣一種狀態。

今天我們來講個非常非常簡單的東西,那就是回調俗稱Callback, 在Android開發以及一些客戶端開發中經常會使用回調。其實如果端的界面開發當做一個黑盒的話,無非就是輸入和輸出,輸入數據,輸出UI的渲染以及用戶的交互事件,那麼這個交互事件大多數場景會採用回調來實現。那麼今天一起來說說如何讓你的回調更具kotlin風味:

  • 1、Java中的回調實現

  • 2、使用Kotlin來改造Java中的回調

  • 3、進一步讓你的回調更具Kotlin風味

  • 4、Object對象表達式回調和DSL回調對比

  • 5、Kotlin中回調使用建議

  • 6、Don't Repeat Yourself(DSL回調配置太模板化了,不妨來擼個自動生成代碼的AS插件吧)

  • 7、DslListenerBuilder插件基本介紹和使用

  • 8、DslListenerBuilder插件源碼和Velocity模板引擎基本介紹

  • 9、總結

一、Java中的回調實現

Java中的回調一般處理步驟都是寫一個接口,然後在接口中定義一些回調函數;然後再暴露一個設置回調接口的函數,傳入函數實參就是回調接口的一個實例,一般情況都是以匿名對象形式存在。例如以Android中OnClickListener和TextWatcher源碼爲例:

  • 1、OnClickListener回調的Java實現

 1//OnClickListener的定義
 2public interface OnClickListener {
 3    void onClick(View v);
 4}
 5
 6public void setOnClickListener(OnClickListener listener) {
 7    this.clickListener = listener;
 8}
 9
10//OnClickListener的使用
11mBtnSubmit.setOnClickListener(new View.OnClickListener() {
12    @Override
13    public void onClick(View v) {
14        //add your logic code
15    }
16});
  • 2、TextWatcher回調的Java實現

 1//TextWatcher的定義
 2public interface TextWatcher extends NoCopySpan {
 3    public void beforeTextChanged(CharSequence s, int start,int count, int after);
 4
 5    public void onTextChanged(CharSequence s, int start, int before, int count);
 6
 7    public void afterTextChanged(Editable s);
 8}
 9
10public void addTextChangedListener(TextWatcher watcher) {
11    if (mListeners == null) {
12        mListeners = new ArrayList<TextWatcher>();
13    }
14
15    mListeners.add(watcher);
16}
17
18//TextWatcher的使用
19mEtComment.addTextChangedListener(new TextWatcher() {
20    @Override
21    public void beforeTextChanged(CharSequence s, int start, int count, int after) {
22             //add your logic code
23    }
24
25    @Override
26    public void onTextChanged(CharSequence s, int start, int before, int count) {
27            //add your logic code
28    }
29
30    @Override
31    public void afterTextChanged(Editable s) {
32            //add your logic code
33    }
34});

二、使用Kotlin來改造Java中的回調

針對上述Java中的回調寫法,估計大部分人轉到Kotlin後,估計會做如下處理:

1、如果接口只有一個回調函數可以直接使用lamba表達式實現回調的簡寫。

2、如果接口中含有多個回調函數,都會使用object對象表達式來實現的。

以改造上述代碼爲例:

  • 1、(只有一個回調函數簡寫形式)OnClickListener回調Kotlin改造

 1//只有一個回調函數普通簡寫形式: OnClickListener的使用
 2mBtnSubmit.setOnClickListener { view ->
 3    //add your logic code
 4}
 5
 6//針對OnClickListener監聽設置Coroutine協程框架中onClick擴展函數的使用
 7mBtnSubmit.onClick { view ->
 8    //add your logic code
 9}
10
11//Coroutine協程框架: onClick的擴展函數定義
12fun android.view.View.onClick(
13        context: CoroutineContext = UI,
14        handler: suspend CoroutineScope.(v: android.view.View?) -> Unit
15) {
16    setOnClickListener { v ->
17        launch(context) {
18            handler(v)
19        }
20    }
21}
  • 2、(多個回調函數object表達式)TextWatcher回調的Kotlin改造(object對象表達式)

 1mEtComment.addTextChangedListener(object: TextWatcher{
 2    override fun afterTextChanged(s: Editable?) {
 3       //add your logic code
 4    }
 5
 6    override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
 7       //add your logic code
 8    } 
 9
10    override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
11       //add your logic code
12    }
13
14 })

關於object對象表達式實現的Kotlin中回調,有不少的Kotlin的小夥伴在公衆號留言向我吐槽過,感覺這樣的寫法是直接從Java中的翻譯過來的一樣,完全看不出Kotlin的優勢在哪。問我有沒有什麼更加具有Kotlin風味的寫法,當然是有的,請接着往下看。

三、進一步讓你的回調更具Kotlin風味(DSL配置回調)

其實如果你看過很多國外大佬的有關Koltin項目的源碼,你就會發現他們寫回調很少去使用object表達式去實現回調,而是採用另一種方式去實現,並且整體寫法看起來更具有Kotlin風味。即使內部用到object表達式,暴露給外層中間都會做一層DSL配置轉換,讓外部調用起來更加Kotlin化。以Github中的MaterialDrawer項目(目前已經有1W多star)中官方指定MatrialDrawer項目Kotlin版本實現的MaterialDrawerKt項目中間一段源碼爲例:

  • 1、DrawerImageLoader 回調定義

 1//注意: 這個函數參數是一個帶返回值的lambda表達式
 2public fun drawerImageLoader(actions: DrawerImageLoaderKt.() -> Unit): DrawerImageLoader.IDrawerImageLoader {
 3    val loaderImpl = DrawerImageLoaderKt().apply(actions).build() //
 4    DrawerImageLoader.init(loaderImpl)
 5    return loaderImpl
 6}
 7
 8//DrawerImageLoaderKt: DSL listener Builder類
 9public class DrawerImageLoaderKt {
10    //定義需要回調的函數lamba成員對象
11    private var setFunc: ((ImageView, Uri, Drawable?, String?) -> Unit)? = null
12    private var placeholderFunc: ((Context, String?) -> Drawable)? = null
13
14    internal fun build() = object : AbstractDrawerImageLoader() {
15
16        private val setFunction: (ImageView, Uri, Drawable?, String?) -> Unit = setFunc
17                ?: throw IllegalStateException("DrawerImageLoader has to have a set function")
18
19        private val placeholderFunction = placeholderFunc
20                ?: { ctx, tag -> super.placeholder(ctx, tag) }
21
22        override fun set(imageView: ImageView, uri: Uri, placeholder: Drawable?, tag: String?) = setFunction(imageView, uri, placeholder, tag)
23
24        override fun placeholder(ctx: Context, tag: String?) = placeholderFunction(ctx, tag)
25
26    }
27
28    //暴露給外部調用的回調函數,在構建類中類似setter,getter方法
29    public fun set(setFunction: (imageView: ImageView, uri: Uri, placeholder: Drawable?, tag: String?) -> Unit) {
30        this.setFunc = setFunction
31    }
32
33    public fun placeholder(placeholderFunction: (ctx: Context, tag: String?) -> Drawable) {
34        this.placeholderFunc = placeholderFunction
35    }
  • 2、DrawerImageLoader回調使用

 1 drawerImageLoader {
 2   //內部的回調函數可以選擇性重寫
 3    set { imageView, uri, placeholder, _ ->
 4        Picasso.with(imageView.context)
 5               .load(uri)
 6               .placeholder(placeholder)
 7               .into(imageView)
 8        }
 9
10    cancel { imageView ->
11        Picasso.with(imageView.context)
12               .cancelRequest(imageView)
13    }
14}

可以看到使用DSL配置的回調更加具有Kotlin風味,讓整個回調看起來非常的舒服,那種效果豈止絲滑。

四、DSL配置回調基本步驟

在Kotlin的一個類中實現了DSL配置回調非常簡單主要就三步:

  • 1、定義一個回調的Builder類,並且在類中定義回調lamba表達式對象成員,最後再定義Builder類的成員函數,這些函數就是暴露給外部回調的函數。個人習慣把它作爲一個類的內部類。類似下面這樣

 1class AudioPlayer(context: Context){
 2     //other logic ...
 3
 4     inner class ListenerBuilder {
 5        internal var mAudioPlayAction: ((AudioData) -> Unit)? = null
 6        internal var mAudioPauseAction: ((AudioData) -> Unit)? = null
 7        internal var mAudioFinishAction: ((AudioData) -> Unit)? = null
 8
 9        fun onAudioPlay(action: (AudioData) -> Unit) {
10            mAudioPlayAction = action
11        }
12
13        fun onAudioPause(action: (AudioData) -> Unit) {
14            mAudioPauseAction = action
15        }
16
17        fun onAudioFinish(action: (AudioData) -> Unit) {
18            mAudioFinishAction = action
19        }
20    }
21}
  • 2、然後,在類中聲明一個ListenerBuilder的實例引用,並且暴露一個設置該實例對象的一個方法,也就是我們常說的註冊事件監聽或回調的方法,類似setOnClickListenter這種。但是需要注意的是函數的參數是帶ListenerBuilder返回值的lamba,類似下面這樣:

1class AudioPlayer(context: Context){
2      //other logic ...
3
4     private lateinit var mListener: ListenerBuilder
5     fun registerListener(listenerBuilder: ListenerBuilder.() -> Unit) {//帶ListenerBuilder返回值的lamba
6        mListener = ListenerBuilder().also(listenerBuilder)
7     }
8}     
  • 3、最後在觸發相應事件調用Builder實例中lamba即可

 1class AudioPlayer(context: Context){
 2      //other logic ...
 3     val mediaPlayer = MediaPlayer(mContext)
 4        mediaPlayer.play(mediaItem, object : PlayerCallbackAdapter() {
 5            override fun onPlay(item: MediaItem?) {
 6                if (::mListener.isInitialized) {
 7                    mListener.mAudioPlayAction?.invoke(mAudioData)
 8                }
 9            }
10
11            override fun onPause(item: MediaItem?) {
12                if (::mListener.isInitialized) {
13                    mListener.mAudioPauseAction?.invoke(mAudioData)
14                }
15            }
16
17            override fun onPlayCompleted(item: MediaItem?) {
18                if (::mListener.isInitialized) {
19                    mListener.mAudioFinishAction?.invoke(mAudioData)
20                }
21            }
22        })  
23}     
  • 4、外部調用

 1val audioPlayer = AudioPlayer(context)
 2    audioPlayer.registerListener {
 3       //可以任意選擇需要回調的函數,不必要完全重寫
 4        onAudioPlay {
 5            //todo your logic
 6        }
 7
 8        onAudioPause {
 9           //todo your logic
10        }
11
12        onAudioFinish {
13           //todo your logic
14        }
15    }

相比object表達式回調寫法,有沒有發現DSL回調配置更懂Kotlin. 可能大家看起來確實不錯,但是不知道它具體原理,畢竟這樣寫法太語法糖化,不太好理解,讓我們接下來一起揭開它的糖衣。

五、揭開DSL回調配置的語法糖衣

  • 1、原理闡述

DSL回調配置其實挺簡單的,實際上就一個Builder類中維護着多個回調lambda的實例,然後在外部回調的時候再利用帶Builder類返回值實例的lamba特性,在該lambda作用域內this可以內部表達爲Builder類實例,利用Builder類實例調用它內部定義成員函數並且賦值初始化Builder類回調lambda成員實例,而這些被初始化過的lambda實例就會在內部事件被觸發的時候執行invoke操作。如果在該lambda內部沒有調用某個成員方法,那麼在該Builder類中這個回調lambda成員實例就是爲null,即使內部事件觸發,爲空就不會回調到外部。

換句話就是外部回調的函數block塊會通過Builder類中成員函數初始化Builder類中回調lambda實例(在上述代碼表現就是mXXXAction實例),然後當內部事件觸發後,根據當前lambda實例是否被初始化,如果初始化完畢,就是立即執行這個lambda也就是執行傳入的block代碼塊

  • 2、代碼拆解
    爲了更加清楚論證上面的闡述,我們可以把代碼拆解一下:

 1mAudioPlayer.registerListener({
 2    //registerListener參數是個帶ListenerBuilder實例返回值的lambda
 3    //所以這裏this就是內部指代爲ListenerBuilder實例
 4    this.onAudioPlay ({  
 5        //logic block 
 6    })
 7    this.onAudioPause ({ 
 8        // logic block
 9    })
10    this.onAudioFinish({ 
11        // logic block
12    })
13  })

onAudioPlay爲例其他同理,調用ListenerBuilderonAudioPlay函數,並傳入block塊來賦值初始化ListenerBuilder類中的mAudioPlayActionlambda實例,當AudioPlayer中的onPlay函數被回調時,就執行mAudioPlayActionlambda。

貌似看起來object對象表達式回調相比DSL回調錶現那麼一無是處,是不是完全可以摒棄object對象表達式這種寫法呢?其實不然,object對象表達式這種寫法也是有它優點的,具體有什麼優點,請接着看它們兩種形式對比。

六、object對象表達式回調和DSL回調對比

  • 1、調用寫法上對比

 1//使用DSL配置回調
 2val audioPlayer = AudioPlayer(context)
 3    audioPlayer.registerListener {
 4       //可以任意選擇需要回調的函數,不必要完全重寫
 5        onAudioPlay {
 6            //todo your logic
 7        }
 8
 9        onAudioPause {
10           //todo your logic
11        }
12
13        onAudioFinish {
14           //todo your logic
15        }
16    }
17
18//使用object對象表達式回調
19val audioPlayer = AudioPlayer(context)
20    audioPlayer.registerListener(object: AudioPlayListener{
21        override fun onAudioPlay(audioData: AudioData) {
22                    //todo your logic
23        }
24        override fun onAudioPause(audioData: AudioData) {
25                    //todo your logic
26        }
27        override fun onAudioFinish(audioData: AudioData) {
28                    //todo your logic
29        }
30    })

調用寫法對比明顯感覺DSL配置更加符合Kotlin風格,所以DSL配置回調更勝一籌

  • 2、使用上對比

使用上DSL有個明顯優勢就是對於不需要監聽的回調函數可以直接省略,而對於object表達式是直接實現一個接口回調必須重寫,雖然它也能做到任意選擇自己需要方法回調,但是還是避免不了一層callback adapter層的處理。所以與其做個adapter層還不如一步到位。所以DSL配置回調更勝一籌

  • 3、性能上對比

其實通過上述調用寫法上看,一眼就能看出來,DSL配置回調這種方式會針對每個回調函數都會創建lambda實例對象,而object對象表達式不管內部回調的方法有多少個,都只會生成一個匿名對象實例。區別就在這裏,所以在性能方面object對象表達式這種方式會更優一點,但是通過問過一些Kotlin社區的大佬們他們還是更傾向於DSL配置這種寫法。所以其實這兩種方式都挺好的,看不同需求,自己權衡選擇即可, 反正我個人挺喜歡DSL那種。爲了驗證我們上述所說的,不妨來看下兩種方式下反編譯的代碼,看看是否是我們所說的那樣:

 1//DSL配置回調反編譯code
 2   public final void setListener(@NotNull Function1 listener) {
 3      Intrinsics.checkParameterIsNotNull(listener, "listener");
 4      ListenerBuilder var2 = new ListenerBuilder();
 5      listener.invoke(var2);
 6      ListenerBuilder var10000 = this.mListener;
 7      //獲取AudioPlay方法對應的實例對象
 8      Function0 var3 = var10000.getMAudioPlayAction$Coroutine_main();
 9      Unit var4;
10      if (var3 != null) {
11         var4 = (Unit)var3.invoke();
12      }
13      //獲取AudioPause方法對應的實例對象
14      var3 = var10000.getMAudioPauseAction$Coroutine_main();
15      if (var3 != null) {
16         var4 = (Unit)var3.invoke();
17      }
18      //獲取AudioFinish方法對應的實例對象
19      var3 = var10000.getMAudioFinishAction$Coroutine_main();
20      if (var3 != null) {
21         var4 = (Unit)var3.invoke();
22      }
23   }
24
25//object對象表達式反編譯code
26 public static final void main(@NotNull String[] args) {
27      Intrinsics.checkParameterIsNotNull(args, "args");
28      int count = true;
29      PlayerPlugin player = new PlayerPlugin();
30      //new Callback一個實例
31      player.setCallback((Callback)(new Callback() {
32         public void onAudioPlay() {
33         }
34
35         public void onAudioPause() {
36         }
37
38         public void onAudioFinish() {
39         }
40      }));
41   }

七、Don't Repeat Yourself(所以順便使用kotlin來擼個自動生成ListenerBuilder的插件吧)

使用過DSL配置回調的小夥伴們有沒有覺得寫這些代碼沒有任何技術含量的,且浪費時間, 那麼Don't Repeat Yourself從現在開始。如果整個DSL配置回調的過程可以做成類似toString、setter、getter方法那樣自動生成,豈不美滋滋,所以來擼個插件吧。所以接下來大致介紹下DslListenerBuilder插件的開發。

開發整體思路:

實際上就是通過Swing的UI窗口配置需要信息參數,然後通過Velocity模板引擎生成模板代碼,然後通過Intellij Plugin API 將生成的代碼插入到當前代碼文件中。所以所有需要自動生成代碼的需求都類似這樣流程。下次需要生成不一樣的代碼只需要修改Velocity模板即可。

使用到技術點:

  • 1、Kotlin基礎開發知識

  • 2、Kotlin擴展函數

  • 3、Kotlin的lambda表達式

  • 4、Swing UI組件開發知識

  • 5、Intellij Plugin開發基本知識

  • 6、IntelliJ Plugin 常用開發API(Editor、WriteCommandAction、PsiDocumentManager、Document等API的使用)

  • 7、Velocity模板基本語法(#if,#foreach,#set等)

  • 8、Velocity模板引擎API的基本使用

基本介紹和使用:

這是一款自動生成DSL ListenerBuilder回調模板代碼的IDEA插件,支持IDEA、AndroidStudio以及JetBrains全家桶。

第一步: 首先按照IDEA一般插件安裝流程安裝好DslListenerBuilder插件。

第二步: 然後打開具體某個類文件,將光標定位在具體代碼生成的位置,

第三步: 使用快捷鍵調出Generate中的面板,選擇其中的“Listener Builder”, 然後就會彈出一個面板,可以點擊add按鈕添加一個或多個回調函數的lamba, 也可以從面板中選擇任一一條不需要的Item進行刪除。

第四步: 最後點擊OK就可以在指定光標位置生成需要的代碼。

 

九、DslListenerBuilder插件源碼和Velocity模板引擎學習資源

這裏推薦一些有關Velocity模板引擎的學習資源,此外有關插件的更多具體實現內容請查看下面GitHub中的源碼,如果覺得不錯歡迎給個star~~~

DslListenerBuilder插件下載地址(https://github.com/BayMikyou/DslListenerBuilder/releases/tag/1.0.0)

DslListenerBuilder插件源碼地址(https://github.com/BayMikyou/DslListenerBuilder)

Velocity模板基本語法(http://www.blogjava.net/alexwan/archive/2008/06/07/206473.html)

使用 Velocity 模板引擎快速生成代碼(https://www.ibm.com/developerworks/cn/java/j-lo-velocity1/)

十、總結

到這裏有關Kotlin回調相關內容已經講得很清楚了,然後還給大家介紹瞭如何去開發一個自動生成代碼的插件。整個插件開發流程同樣適用於其他的代碼生成需求。爲什麼要寫這麼個插件呢,主要是由於最近需求太多,每次寫回調的時候都需要不斷重複去寫很多類似的代碼。有時候當我們在重複性做一些操作的時候,不妨去思考下用什麼工具能否把整個流程給自動化。歸根結底一句話: Don't Repeat Yourself.

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章