簡述: 這應該是2019年的第一篇文章了,臨近過年回家一個月需求是真的不少,正如康少說的那樣,一年的需求幾乎都在最後一兩月寫完了。因此寫文章也擱置了好久,固然再忙天天都會刷掘金。好久就一直在使用Kotlin寫項目,說實話到目前爲止Kotlin用的是愈來愈順手了(內心只能用美滋滋來形容了)。固然此次依然講的是Kotlin,說下我此次需求開發中本身一些思考和實踐。其中讓本身感覺最深的就是: "Don't Repeat Yourself"。當你常常寫一些重複性的代碼,不妨停下來想下是否要去改變這樣一種狀態。html
今天咱們來說個很是很是簡單的東西,那就是回調俗稱Callback, 在Android開發以及一些客戶端開發中常常會使用回調。其實若是端的界面開發當作一個黑盒的話,無非就是輸入和輸出,輸入數據,輸出UI的渲染以及用戶的交互事件,那麼這個交互事件大多數場景會採用回調來實現。那麼今天一塊兒來講說如何讓你的回調更具kotlin風味:java
Java中的回調通常處理步驟都是寫一個接口,而後在接口中定義一些回調函數;而後再暴露一個設置回調接口的函數,傳入函數實參就是回調接口的一個實例,通常狀況都是以匿名對象形式存在。例如以Android中OnClickListener和TextWatcher源碼爲例:android
//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
}
});
複製代碼
//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
}
});
複製代碼
針對上述Java中的回調寫法,估計大部分人轉到Kotlin後,估計會作以下處理:git
一、若是接口只有一個回調函數能夠直接使用lamba表達式實現回調的簡寫。github
二、若是接口中含有多個回調函數,都會使用object對象表達式來實現的。數組
以改造上述代碼爲例:app
//只有一個回調函數普通簡寫形式: 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)
}
}
}
複製代碼
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風味的寫法,固然是有的,請接着往下看。框架
其實若是你看過不少國外大佬的有關Koltin項目的源碼,你就會發現他們寫回調不多去使用object表達式去實現回調,而是採用另外一種方式去實現,而且總體寫法看起來更具備Kotlin風味。即便內部用到object表達式,暴露給外層中間都會作一層DSL配置轉換,讓外部調用起來更加Kotlin化。以Github中的MaterialDrawer項目(目前已經有1W多star)中官方指定MatrialDrawer項目Kotlin版本實現的MaterialDrawerKt項目中間一段源碼爲例:ide
//注意: 這個函數參數是一個帶返回值的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
}
複製代碼
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風味,讓整個回調看起來很是的舒服,那種效果豈止絲滑。函數
在Kotlin的一個類中實現了DSL配置回調很是簡單主要就三步:
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
}
}
}
複製代碼
class AudioPlayer(context: Context){
//other logic ...
private lateinit var mListener: ListenerBuilder
fun registerListener(listenerBuilder: ListenerBuilder.() -> Unit) {//帶ListenerBuilder返回值的lamba
mListener = ListenerBuilder().also(listenerBuilder)
}
}
複製代碼
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)
}
}
})
}
複製代碼
val audioPlayer = AudioPlayer(context)
audioPlayer.registerListener {
//能夠任意選擇須要回調的函數,沒必要要徹底重寫
onAudioPlay {
//todo your logic
}
onAudioPause {
//todo your logic
}
onAudioFinish {
//todo your logic
}
}
複製代碼
相比object表達式回調寫法,有沒有發現DSL回調配置更懂Kotlin. 可能你們看起來確實不錯,可是不知道它具體原理,畢竟這樣寫法太語法糖化,不太好理解,讓咱們接下來一塊兒揭開它的糖衣。
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代碼塊
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對象表達式這種寫法也是有它優勢的,具體有什麼優勢,請接着看它們兩種形式對比。
//使用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配置回調更勝一籌
使用上DSL有個明顯優點就是對於不須要監聽的回調函數能夠直接省略,而對於object表達式是直接實現一個接口回調必須重寫,雖然它也能作到任意選擇本身須要方法回調,可是仍是避免不了一層callback adapter層的處理。因此與其作個adapter層還不如一步到位。因此DSL配置回調更勝一籌
其實經過上述調用寫法上看,一眼就能看出來,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() {
}
}));
}
複製代碼
使用過DSL配置回調的小夥伴們有沒有以爲寫這些代碼沒有任何技術含量的,且浪費時間, 那麼Don't Repeat Yourself從如今開始。若是整個DSL配置回調的過程能夠作成相似toString、setter、getter方法那樣自動生成,豈不美滋滋,因此來擼個插件吧。因此接下來大體介紹下DslListenerBuilder插件的開發。
開發總體思路:
實際上就是經過Swing的UI窗口配置須要信息參數,而後經過Velocity模板引擎生成模板代碼,而後經過Intellij Plugin API 將生成的代碼插入到當前代碼文件中。因此全部須要自動生成代碼的需求都相似這樣流程。下次須要生成不同的代碼只須要修改Velocity模板便可。
使用到技術點:
基本介紹和使用:
這是一款自動生成DSL ListenerBuilder回調模板代碼的IDEA插件,支持IDEA、AndroidStudio以及JetBrains全家桶。
第一步: 首先按照IDEA通常插件安裝流程安裝好DslListenerBuilder插件。
第二步: 而後打開具體某個類文件,將光標定位在具體代碼生成的位置,
第三步: 使用快捷鍵調出Generate中的面板,選擇其中的「Listener Builder」, 而後就會彈出一個面板,能夠點擊add按鈕添加一個或多個回調函數的lamba, 也能夠從面板中選擇任一一條不須要的Item進行刪除。
第四步: 最後點擊OK就能夠在指定光標位置生成須要的代碼。
這裏推薦一些有關Velocity模板引擎的學習資源,此外有關插件的更多具體實現內容請查看下面GitHub中的源碼,若是以爲不錯歡迎給個star~~~
目前插件已經上傳到JetBrains IntelliJ Plugins官方倉庫,還處於審覈,過幾天就能夠直接在AndroidStudio或者IntelliJ IDEA中搜索 DslListenerBuilder直接安裝了
到這裏有關Kotlin回調相關內容已經講得很清楚了,而後還給你們介紹瞭如何去開發一個自動生成代碼的插件。整個插件開發流程一樣適用於其餘的代碼生成需求。爲何要寫這麼個插件呢,主要是因爲最近需求太多,每次寫回調的時候都須要不斷重複去寫不少相似的代碼。有時候當咱們在重複性作一些操做的時候,不妨去思考下用什麼工具可否把整個流程給自動化。歸根結底一句話: Don't Repeat Yourself.
歡迎關注Kotlin開發者聯盟,這裏有最新Kotlin技術文章,每週會不按期翻譯一篇Kotlin國外技術文章。若是你也喜歡Kotlin,歡迎加入咱們~~~
Effective Kotlin翻譯系列
原創系列:
翻譯系列:
實戰系列: