如何让你的回调更具Kotlin风味

作者 : 开心源码 本文共12171个字,预计阅读时间需要31分钟 发布时间: 2022-05-12 共176人阅读

原文链接:https://juejin.im/post/5c4f106a6fb9a049de6dc410

如何让你的回调更具Kotlin风味

简述: 这应该是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为例其余同理,调用ListenerBuilderonAudioPlay函数,并传入block块来赋值初始化ListenerBuilder类中的mAudioPlayActionlambda实例,当AudioPlayer中的onPlay函数被回调时,就执行mAudioPlayActionlambda。

貌似看起来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即可以在指定光标位置生成需要的代码。

<figcaption style="display: block; text-align: center; font-size: 1rem; line-height: 1.6; color: rgb(144, 144, 144); margin-top: 2px;"></figcaption>

九、DslListenerBuilder插件源码和Velocity模板引擎学习资源

这里推荐少量有关Velocity模板引擎的学习资源,此外有关插件的更多具体实现内容请查看下面GitHub中的源码,假如觉得不错欢迎给个star~~~

目前插件已经上传到JetBrains IntelliJ Plugins官方仓库,还处于审核,过几天即可以直接在AndroidStudio或者者IntelliJ IDEA中搜索 DslListenerBuilder直接安装了

DslListenerBuilder插件下载地址

DslListenerBuilder插件源码地址

Velocity模板基本语法

使用 Velocity 模板引擎快速生成代码

十、总结

到这里有关Kotlin回调相关内容已经讲得很清楚了,而后还给大家详情了如何去开发一个自动生成代码的插件。整个插件开发流程同样适用于其余的代码生成需求。为什么要写这么个插件呢,主要是因为最近需求太多,每次写回调的时候都需要不断重复去写很多相似的代码。有时候当我们在重复性做少量操作的时候,不妨去思考下用什么工具是否把整个流程给自动化。归根结底一句话: Don’t Repeat Yourself.

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)赛跑

实战系列:

  • 用Kotlin撸一个图片压缩插件ImageSlimming-导学篇(一)
  • 用Kotlin撸一个图片压缩插件-插件基础篇(二)
  • 用Kotlin撸一个图片压缩插件-实战篇(三)
  • 浅谈Kotlin实战篇之自己设置View图片圆角简单应用

进阶大纲


【附】相关资料

进群【940780115】免费领取,欢迎各位一起来讨论在Android开发上面所遇见的问题。

说明
1. 本站所有资源来源于用户上传和网络,如有侵权请邮件联系站长!
2. 分享目的仅供大家学习和交流,您必须在下载后24小时内删除!
3. 不得使用于非法商业用途,不得违反国家法律。否则后果自负!
4. 本站提供的源码、模板、插件等等其他资源,都不包含技术服务请大家谅解!
5. 如有链接无法下载、失效或广告,请联系管理员处理!
6. 本站资源售价只是摆设,本站源码仅提供给会员学习使用!
7. 如遇到加密压缩包,请使用360解压,如遇到无法解压的请联系管理员
开心源码网 » 如何让你的回调更具Kotlin风味

发表回复