如何讓你的回撥更具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實現
//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
為例其他同理,呼叫 ListenerBuilder
中 onAudioPlay
函式,並傳入 block
塊來賦值初始化 ListenerBuilder
類中的 mAudioPlayAction
lambda例項,當 AudioPlayer
中的 onPlay
函式被回撥時,就執行 mAudioPlayAction
lambda。
貌似看起來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就可以在指定游標位置生成需要的程式碼。

九、DslListenerBuilder外掛原始碼和Velocity模板引擎學習資源
這裡推薦一些有關Velocity模板引擎的學習資源,此外有關外掛的更多具體實現內容請檢視下面GitHub中的原始碼,如果覺得不錯歡迎給個star~~~
十、總結
到這裡有關Kotlin回撥相關內容已經講得很清楚了,然後還給大家介紹瞭如何去開發一個自動生成程式碼的外掛。整個外掛開發流程同樣適用於其他的程式碼生成需求。為什麼要寫這麼個外掛呢,主要是由於最近需求太多,每次寫回調的時候都需要不斷重複去寫很多類似的程式碼。有時候當我們在重複性做一些操作的時候,不妨去思考下用什麼工具能否把整個流程給自動化。歸根結底一句話: Don't Repeat Yourself .