如何讓你的回調更具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爲例其他同理,調用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就可以在指定光標位置生成需要的代碼。

九、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系列文章,歡迎查看:

Effective Kotlin翻譯系列

原創系列:

翻譯系列:

實戰系列:

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