写点什么

如何让你的回调更具 Kotlin 风味,基于 h5 框架的移动 app 开发

用户头像
Android架构
关注
发布于: 刚刚
  • 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)? = nullinternal var mAudioPauseAction: ((AudioData) -> Unit)? = nullinternal 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: ListenerBuilderfun registerListener(listenerBuilder: ListenerBuilder.() -> Unit) {//带 ListenerBuilder 返回值的 lambamListener = 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 配置回调反编译 codepublic final void setListener(@NotNull Function1 listener) {Intrinsics.checkParameterIsNotNull(listener, "listener");ListenerBuilder var2


《Android学习笔记总结+最新移动架构视频+大厂安卓面试真题+项目实战源码讲义》
浏览器打开:qq.cn.hn/FTe 免费领取
复制代码


= new ListenerBuilder();listener.invoke(var2);ListenerBuilder var10000 = this.mListener;//获取 AudioPlay 方法对应的实例对象 Function0 var3 = var10000.getMAudioPlayActionCoroutine_main();if (var3 != null) {var4 = (Unit)var3.invoke();}//获取 AudioFinish 方法对应的实例对象 var3 = var10000.getMAudioFinishAction$Coroutine_main();if (var3 != null) {var4 = (Unit)var3.invoke();}}


//object 对象表达式反编译 codepublic 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~~~


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

DslListenerBuilder插件下载地址

DslListenerBuilder插件源码地址

Velocity模板基本语法

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

十、总结

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

Kotlin 系列文章,欢迎查看:

Effective Kotlin 翻译系列



原创系列:


用户头像

Android架构

关注

还未添加个人签名 2021.10.31 加入

还未添加个人简介

评论

发布
暂无评论
如何让你的回调更具Kotlin风味,基于h5框架的移动app开发