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

mikyou發表於2019-01-29

簡述: 這應該是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實現
//OnClickListener的定義
public interface OnClickListener {
    void onClick(View v);
}

public void setOnClickListener(OnClickListener listener) {
    this.clickListener = listener;
}

//OnClickListener的使用
mBtnSubmit.setOnClickListener(new View.OnClickListener() {
	@Override
	public void onClick(View v) {
	    //add your logic code
	}
});
複製程式碼
  • 2、TextWatcher回撥的Java實現
//TextWatcher的定義
public interface TextWatcher extends NoCopySpan {
    public void beforeTextChanged(CharSequence s, int start,int count, int after);
    
    public void onTextChanged(CharSequence s, int start, int before, int count);

    public void afterTextChanged(Editable s);
}

public void addTextChangedListener(TextWatcher watcher) {
    if (mListeners == null) {
        mListeners = new ArrayList<TextWatcher>();
    }

    mListeners.add(watcher);
}

//TextWatcher的使用
mEtComment.addTextChangedListener(new TextWatcher() {
	@Override
	public void beforeTextChanged(CharSequence s, int start, int count, int after) {
             //add your logic code
	}

	@Override
	public void onTextChanged(CharSequence s, int start, int before, int count) {
            //add your logic code
	}

	@Override
	public void afterTextChanged(Editable s) {
            //add your logic code
	}
});
複製程式碼

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

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

1、如果介面只有一個回撥函式可以直接使用lamba表示式實現回撥的簡寫。

2、如果介面中含有多個回撥函式,都會使用object物件表示式來實現的。

以改造上述程式碼為例:

  • 1、(只有一個回撥函式簡寫形式)OnClickListener回撥Kotlin改造
//只有一個回撥函式普通簡寫形式: OnClickListener的使用
mBtnSubmit.setOnClickListener { view ->
    //add your logic code
}

//針對OnClickListener監聽設定Coroutine協程框架中onClick擴充套件函式的使用
mBtnSubmit.onClick { view ->
    //add your logic code
}

//Coroutine協程框架: onClick的擴充套件函式定義
fun android.view.View.onClick(
        context: CoroutineContext = UI,
        handler: suspend CoroutineScope.(v: android.view.View?) -> Unit
) {
    setOnClickListener { v ->
        launch(context) {
            handler(v)
        }
    }
}

複製程式碼
  • 2、(多個回撥函式object表示式)TextWatcher回撥的Kotlin改造(object物件表示式)
mEtComment.addTextChangedListener(object: TextWatcher{
    override fun afterTextChanged(s: Editable?) {
       //add your logic code
    }

    override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
       //add your logic code
    } 

    override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
       //add your logic code
    }
    
 })
複製程式碼

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

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

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

  • 1、DrawerImageLoader 回撥定義
//注意: 這個函式引數是一個帶返回值的lambda表示式
public fun drawerImageLoader(actions: DrawerImageLoaderKt.() -> Unit): DrawerImageLoader.IDrawerImageLoader {
    val loaderImpl = DrawerImageLoaderKt().apply(actions).build() //
    DrawerImageLoader.init(loaderImpl)
    return loaderImpl
}

//DrawerImageLoaderKt: DSL listener Builder類
public class DrawerImageLoaderKt {
    //定義需要回撥的函式lamba成員物件
    private var setFunc: ((ImageView, Uri, Drawable?, String?) -> Unit)? = null
    private var placeholderFunc: ((Context, String?) -> Drawable)? = null

    internal fun build() = object : AbstractDrawerImageLoader() {

        private val setFunction: (ImageView, Uri, Drawable?, String?) -> Unit = setFunc
                ?: throw IllegalStateException("DrawerImageLoader has to have a set function")

        private val placeholderFunction = placeholderFunc
                ?: { ctx, tag -> super.placeholder(ctx, tag) }

        override fun set(imageView: ImageView, uri: Uri, placeholder: Drawable?, tag: String?) = setFunction(imageView, uri, placeholder, tag)

        override fun placeholder(ctx: Context, tag: String?) = placeholderFunction(ctx, tag)

    }

    //暴露給外部呼叫的回撥函式,在構建類中類似setter,getter方法
    public fun set(setFunction: (imageView: ImageView, uri: Uri, placeholder: Drawable?, tag: String?) -> Unit) {
        this.setFunc = setFunction
    }

    public fun placeholder(placeholderFunction: (ctx: Context, tag: String?) -> Drawable) {
        this.placeholderFunc = placeholderFunction
    }
複製程式碼
  • 2、DrawerImageLoader回撥使用
 drawerImageLoader {
   //內部的回撥函式可以選擇性重寫
    set { imageView, uri, placeholder, _ ->
        Picasso.with(imageView.context)
               .load(uri)
               .placeholder(placeholder)
               .into(imageView)
        }
        
    cancel { imageView ->
        Picasso.with(imageView.context)
               .cancelRequest(imageView)
    }
}
複製程式碼

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

四、DSL配置回撥基本步驟

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

  • 1、定義一個回撥的Builder類,並且在類中定義回撥lamba表示式物件成員,最後再定義Builder類的成員函式,這些函式就是暴露給外部回撥的函式。個人習慣把它作為一個類的內部類。類似下面這樣
class AudioPlayer(context: Context){
     //other logic ...
     
     inner class ListenerBuilder {
        internal var mAudioPlayAction: ((AudioData) -> Unit)? = null
        internal var mAudioPauseAction: ((AudioData) -> Unit)? = null
        internal var mAudioFinishAction: ((AudioData) -> Unit)? = null
    
        fun onAudioPlay(action: (AudioData) -> Unit) {
            mAudioPlayAction = action
        }
    
        fun onAudioPause(action: (AudioData) -> Unit) {
            mAudioPauseAction = action
        }
    
        fun onAudioFinish(action: (AudioData) -> Unit) {
            mAudioFinishAction = action
        }
    }
}

複製程式碼
  • 2、然後,在類中宣告一個ListenerBuilder的例項引用,並且暴露一個設定該例項物件的一個方法,也就是我們常說的註冊事件監聽或回撥的方法,類似setOnClickListenter這種。但是需要注意的是函式的引數是帶ListenerBuilder返回值的lamba,類似下面這樣:
class AudioPlayer(context: Context){
      //other logic ...
      
     private lateinit var mListener: ListenerBuilder
     fun registerListener(listenerBuilder: ListenerBuilder.() -> Unit) {//帶ListenerBuilder返回值的lamba
        mListener = ListenerBuilder().also(listenerBuilder)
     }
}     
複製程式碼
  • 3、最後在觸發相應事件呼叫Builder例項中lamba即可
class AudioPlayer(context: Context){
      //other logic ...
     val mediaPlayer = MediaPlayer(mContext)
        mediaPlayer.play(mediaItem, object : PlayerCallbackAdapter() {
            override fun onPlay(item: MediaItem?) {
                if (::mListener.isInitialized) {
                    mListener.mAudioPlayAction?.invoke(mAudioData)
                }
            }

            override fun onPause(item: MediaItem?) {
                if (::mListener.isInitialized) {
                    mListener.mAudioPauseAction?.invoke(mAudioData)
                }
            }

            override fun onPlayCompleted(item: MediaItem?) {
                if (::mListener.isInitialized) {
                    mListener.mAudioFinishAction?.invoke(mAudioData)
                }
            }
        })  
}     
複製程式碼
  • 4、外部呼叫
val audioPlayer = AudioPlayer(context)
    audioPlayer.registerListener {
       //可以任意選擇需要回撥的函式,不必要完全重寫
        onAudioPlay {
            //todo your logic
        }

        onAudioPause {
           //todo your logic
        }

        onAudioFinish {
           //todo your logic
        }
    }
複製程式碼

相比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、程式碼拆解 為了更加清楚論證上面的闡述,我們可以把程式碼拆解一下:
mAudioPlayer.registerListener({
    //registerListener引數是個帶ListenerBuilder例項返回值的lambda
    //所以這裡this就是內部指代為ListenerBuilder例項
    this.onAudioPlay ({  
        //logic block 
    })
    this.onAudioPause ({ 
        // logic block
    })
    this.onAudioFinish({ 
        // logic block
    })
  })
複製程式碼

onAudioPlay為例其他同理,呼叫ListenerBuilderonAudioPlay函式,並傳入block塊來賦值初始化ListenerBuilder類中的mAudioPlayActionlambda例項,當AudioPlayer中的onPlay函式被回撥時,就執行mAudioPlayActionlambda。

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

六、object物件表示式回撥和DSL回撥對比

  • 1、呼叫寫法上對比
//使用DSL配置回撥
val audioPlayer = AudioPlayer(context)
    audioPlayer.registerListener {
       //可以任意選擇需要回撥的函式,不必要完全重寫
        onAudioPlay {
            //todo your logic
        }

        onAudioPause {
           //todo your logic
        }

        onAudioFinish {
           //todo your logic
        }
    }
    
//使用object物件表示式回撥
val audioPlayer = AudioPlayer(context)
    audioPlayer.registerListener(object: AudioPlayListener{
        override fun onAudioPlay(audioData: AudioData) {
                    //todo your logic
        }
        override fun onAudioPause(audioData: AudioData) {
                    //todo your logic
        }
        override fun onAudioFinish(audioData: AudioData) {
                    //todo your logic
        }
    })
複製程式碼

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

  • 2、使用上對比

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

  • 3、效能上對比

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

//DSL配置回撥反編譯code
   public final void setListener(@NotNull Function1 listener) {
      Intrinsics.checkParameterIsNotNull(listener, "listener");
      ListenerBuilder var2 = new ListenerBuilder();
      listener.invoke(var2);
      ListenerBuilder var10000 = this.mListener;
      //獲取AudioPlay方法對應的例項物件
      Function0 var3 = var10000.getMAudioPlayAction$Coroutine_main();
      Unit var4;
      if (var3 != null) {
         var4 = (Unit)var3.invoke();
      }
      //獲取AudioPause方法對應的例項物件
      var3 = var10000.getMAudioPauseAction$Coroutine_main();
      if (var3 != null) {
         var4 = (Unit)var3.invoke();
      }
      //獲取AudioFinish方法對應的例項物件
      var3 = var10000.getMAudioFinishAction$Coroutine_main();
      if (var3 != null) {
         var4 = (Unit)var3.invoke();
      }
   }

//object物件表示式反編譯code
 public static final void main(@NotNull String[] args) {
      Intrinsics.checkParameterIsNotNull(args, "args");
      int count = true;
      PlayerPlugin player = new PlayerPlugin();
      //new Callback一個例項
      player.setCallback((Callback)(new Callback() {
         public void onAudioPlay() {
         }

         public void onAudioPause() {
         }

         public void onAudioFinish() {
         }
      }));
   }
複製程式碼

七、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就可以在指定游標位置生成需要的程式碼。

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

九、DslListenerBuilder外掛原始碼和Velocity模板引擎學習資源

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

目前外掛已經上傳到JetBrains IntelliJ Plugins官方倉庫,還處於稽核,過幾天就可以直接在AndroidStudio或者IntelliJ IDEA中搜尋 DslListenerBuilder直接安裝了

DslListenerBuilder外掛下載地址

DslListenerBuilder外掛原始碼地址

Velocity模板基本語法

使用 Velocity 模板引擎快速生成程式碼

十、總結

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

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

歡迎關注Kotlin開發者聯盟,這裡有最新Kotlin技術文章,每週會不定期翻譯一篇Kotlin國外技術文章。如果你也喜歡Kotlin,歡迎加入我們~~~

Kotlin系列文章,歡迎檢視:

Effective Kotlin翻譯系列

原創系列:

翻譯系列:

實戰系列:

相關文章