简述: 这应该是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翻译系列
原创系列:
翻译系列:
实战系列: