内容简介:简述:这应该是2019年的第一篇文章了,临近过年回家一个月需求是真的很多,正如康少说的那样,一年的需求几乎都在最后一两月写完了。所以写文章也搁置了很久,当然再忙每天都会刷掘金。很久就一直在使用Kotlin写项目,说实话到目前为止Kotlin用的是越来越顺手了(心里只能用美滋滋来形容了)。当然这次依然讲的是Kotlin,说下我这次需求开发中自己一些思考和实践。其中让自己感受最深的就是:今天我们来讲个非常非常简单的东西,那就是回调俗称Callback, 在Android开发以及一些客户端开发中经常会使用回调。其
简述:这应该是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 .
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:- NewSQL体系比Hadoop更具效率
- Flowblade 2.8 发布,更具可配置性
- Docker和Kubernetes如何让DevOps更具效力
- AI公司的练级之道:如何更具扩展性?
- GCC 8 的可用性改进:让错误信息和提示更具帮助性
- 参会见闻系列:ACL 2018,在更具挑战的环境下理解数据表征及方法评价
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。