内容简介:简述:这应该是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,在更具挑战的环境下理解数据表征及方法评价
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Data Mining
Jiawei Han、Micheline Kamber、Jian Pei / Morgan Kaufmann / 2011-7-6 / USD 74.95
The increasing volume of data in modern business and science calls for more complex and sophisticated tools. Although advances in data mining technology have made extensive data collection much easier......一起来看看 《Data Mining》 这本书的介绍吧!