簡述: 這應該是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~~~
目前外掛已經上傳到JetBrains IntelliJ Plugins官方倉庫,還處於稽核,過幾天就可以直接在AndroidStudio或者IntelliJ IDEA中搜尋 DslListenerBuilder直接安裝了
DslListenerBuilder外掛下載地址
DslListenerBuilder外掛原始碼地址
Velocity模板基本語法
使用 Velocity 模板引擎快速生成程式碼
十、總結
到這裡有關Kotlin回撥相關內容已經講得很清楚了,然後還給大家介紹瞭如何去開發一個自動生成程式碼的外掛。整個外掛開發流程同樣適用於其他的程式碼生成需求。為什麼要寫這麼個外掛呢,主要是由於最近需求太多,每次寫回撥的時候都需要不斷重複去寫很多類似的程式碼。有時候當我們在重複性做一些操作的時候,不妨去思考下用什麼工具能否把整個流程給自動化。歸根結底一句話: Don't Repeat Yourself.
歡迎關注Kotlin開發者聯盟,這裡有最新Kotlin技術文章,每週會不定期翻譯一篇Kotlin國外技術文章。如果你也喜歡Kotlin,歡迎加入我們~~~
Kotlin系列文章,歡迎檢視:
Effective Kotlin翻譯系列
- [譯]Effective Kotlin系列之考慮使用原始型別的陣列優化效能(五)
- [譯]Effective Kotlin系列之使用Sequence來優化集合的操作(四)
- [譯]Effective Kotlin系列之探索高階函式中inline修飾符(三)
- [譯]Effective Kotlin系列之遇到多個構造器引數要考慮使用構建器(二)
- [譯]Effective Kotlin系列之考慮使用靜態工廠方法替代構造器(一)
原創系列:
- Jetbrains開發者日見聞(三)之Kotlin1.3新特性(inline class篇)
- JetBrains開發者日見聞(二)之Kotlin1.3的新特性(Contract契約與協程篇)
- JetBrains開發者日見聞(一)之Kotlin/Native 嚐鮮篇
- 教你如何攻克Kotlin中泛型型變的難點(實踐篇)
- 教你如何攻克Kotlin中泛型型變的難點(下篇)
- 教你如何攻克Kotlin中泛型型變的難點(上篇)
- Kotlin的獨門祕籍Reified實化型別引數(下篇)
- 有關Kotlin屬性代理你需要知道的一切
- 淺談Kotlin中的Sequences原始碼解析
- 淺談Kotlin中集合和函式式API完全解析-上篇
- 淺談Kotlin語法篇之lambda編譯成位元組碼過程完全解析
- 淺談Kotlin語法篇之Lambda表示式完全解析
- 淺談Kotlin語法篇之擴充套件函式
- 淺談Kotlin語法篇之頂層函式、中綴呼叫、解構宣告
- 淺談Kotlin語法篇之如何讓函式更好地呼叫
- 淺談Kotlin語法篇之變數和常量
- 淺談Kotlin語法篇之基礎語法
翻譯系列:
- [譯]記一次Kotlin官方文件翻譯的PR(內聯類)
- [譯]Kotlin中內聯類的自動裝箱和高效能探索(二)
- [譯]Kotlin中內聯類(inline class)完全解析(一)
- [譯]Kotlin的獨門祕籍Reified實化型別引數(上篇)
- [譯]Kotlin泛型中何時該用型別形參約束?
- [譯] 一個簡單方式教你記住Kotlin的形參和實參
- [譯]Kotlin中是應該定義函式還是定義屬性?
- [譯]如何在你的Kotlin程式碼中移除所有的!!(非空斷言)
- [譯]掌握Kotlin中的標準庫函式: run、with、let、also和apply
- [譯]有關Kotlin型別別名(typealias)你需要知道的一切
- [譯]Kotlin中是應該使用序列(Sequences)還是集合(Lists)?
- [譯]Kotlin中的龜(List)兔(Sequence)賽跑
實戰系列: