Kotlin之美——DSL篇 和 如何让你的回调更具Kotlin风味

 

如何让你的回调更具Kotlin风味
https://mp.weixin.qq.com/s?__biz=MzAwOTQ4Mzk2Nw==&mid=2458585682&idx=1&sn=641073020fae0050f3a4523bd107c6c2&chksm=8c214982bb56c094c2e0c1d3d3decd111d9241006d4fdf80e5b71c4e502a5c61936eaab80910&scene=0&xtrack=1#rd

Kotlin之美——DSL篇 - 简书
https://www.jianshu.com/p/f5f0d38e3e44

 

 

 

Kotlin之美——DSL篇

96 geniusmart 关注

2018.03.02 11:54* 字数 2618 阅读 4214评论 5喜欢 41赞赏 1

Kotlin 系列:

Kotlin DSL 把 Kotlin 的语法糖演绎得淋漓尽致,这些语法糖可谓好吃、好看又好玩,但是,仅痴迷于语法糖只会对语言的理解游离于表面,了解其实现原理,是我们阅读优秀源码、设计整洁代码和理解编程语言的必经之路,本文我们通过 DSL 来感受 Kotlin 之美。

理解 DSL

DSL(domain specific language),即领域专用语言:专门解决某一特定问题的计算机语言,比如大家耳熟能详的 SQL 和正则表达式。

通用编程语言 vs DSL

通用编程语言(如 Java、Kotlin、Android等),往往提供了全面的库来帮助开发者开发完整的应用程序,而 DSL 只专注于某个领域,比如 SQL 仅支持数据库的相关处理,而正则表达式只用来检索和替换文本,我们无法用 SQL 或者正则表达式来开发一个完整的应用。

API vs DSL

无论是通用编程语言,还是领域专用语言,最终都是要通过 API 的形式向开发者呈现。良好的、优雅的、整洁的、一致的 API 风格是每个优秀开发者的追求,而 DSL 往往具备独特的代码结构和一致的代码风格,从 SQL 和正则表达式的语法风格便可感受一二。

下文我们也将提到,Kotlin 构建的 DSL,代码风格更具表现力和想象力,也更加优雅。

内部 DSL

但是,如果为解决某一特定领域问题就创建一套独立的语言,开发成本和学习成本都很高,因此便有了内部 DSL 的概念。所谓内部 DSL,便是使用通用编程语言来构建 DSL。比如,本文提到的 Kotlin DSL,我们为 Kotlin DSL 做一个简单的定义:

“使用 Kotlin 语言开发的,解决特定领域问题,具备独特代码结构的 API 。”

下面,我们就来领略下千变万化的 Kotlin DSL 。

有趣的 Kotlin DSL

如果说 Kotlin 是一位魔术师,那么 DSL 便是其赖以成名,令人啧啧称赞的魔术作品,我们先来看下 Kotlin 在各个特定领域的有趣实现。

  1. 日期
val yesterday = 1.days.ago // 也可以这样写: val yesterday = 1 days ago
val twoMonthsLater = 2 months fromNow

以上日期处理的代码,真正做到见名知意,深谙代码整洁之道,更多细节可参考此库:kxdate 。

如果不考虑规范,基于该库的设计思路,我们甚至可以设计出如下的 api:

val yesterday = 1 天 前
val twoMonthsLater = 2 月 后

这个日期处理领域的 DSL 体现出来的代码结构是链式的,并且近似于我们日常使用的英语

  1. 单元测试
val str = "kotlin"
str should startWith("kot")
str.length shouldBe 6

与上述日期库的 api 风格类似,该单元测试的代码也是赏心悦目,更多细节可参考此库:kotlintest 。

基于该库的设计思路,我们甚至可以实现如下的代码风格,如同写英语句子一般简洁:

"kotlin" should start with "kot"
"kotlin" should have substring "otl"

这个 DSL 的代码结构近似于我们日常使用的英语。

  1. HTML 构建器
fun createTable() = 
    table{
        tr{
            td{
                
            }
        }
    }
    
>>> println(createTable())
<table><tr><td></td></tr></table>

这个 DSL 的代码结构使用了 lambda 嵌套,并且语义清晰,一目了然。更多详情参考此库:kotlinx.html

  1. SQL
(Users innerJoin Cities).slice(Users.name, Cities.name).
            select {(Users.id.eq("andrey") or Users.name.eq("Sergey")) and
                    Users.id.eq("sergey") and Users.cityId.eq(Cities.id)}.forEach {
            println("${it[Users.name]} lives in ${it[Cities.name]}")
        }

这类 SQL api 的风格,如果有用过 ORM 的框架,如 ActiveAndroid 或者 Realm 就不会陌生。以上代码来自于此库:Exposed 。

  1. Android 布局

Anko Layouts 是一套帮助我们更简洁的开发和复用 Android 布局的 DSL ,它的代码风格如下:

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
       
        super.onCreate(savedInstanceState)
        verticalLayout {
            val name = editText()
            button("Say Hello") {
                onClick { toast("Hello, ${name.text}!") }
            }
        }

    }
   
}

相比于笨重的 XML 布局方式,Anko DSL 显然是更先进和更高效的解决方案。

  1. Gradle 构建

Gradle 的构建脚本是 groovy,对 Android 程序员有一定的学习成本,目前,Gradle 官方也提供了基于 Kotlin 的构建脚本:Gradle Kotlin DSL , 并提供了类 groovy 的代码风格:

dependencies {
    compile("com.android.support:appcompat-v7:27.0.1")
    compile("com.android.support.constraint:constraint-layout:1.0.2")
}

完整代码请参考:build.gradle.kts

综上,Kotlin DSL 所体现的代码结构有如下特点:链式调用,大括号嵌套,并且可以近似于英语句子。

实现原理

看了那么多 Kotlin DSL 的风格和使用场景,相较于刻板的、传统的 Java 而言,更加神奇和富有想象力。要理解 Kotlin DSL 这场魔术盛宴,就必须了解其背后用到的魔术道具——扩展函数、lambda、中缀调用和 invoke 约定。

扩展函数(扩展属性)

对于同样作为静态语言的 Kotlin 来说,扩展函数(扩展属性)是让他拥有类似于动态语言能力的法宝,即我们可以为任意对象动态的增加函数或属性。

比如,为 String 扩展一个函数: lastChar():

package strings

fun String.lastChar(): Char = this.get(this.length - 1)

调用扩展函数:

>>> println("Kotlin".lastChar())
n

与 JavaScript 这类动态语言不一样,Kotlin 实现原理是: 提供静态工具类,将接收对象(此例为 String )做为参数传递进来,以下为该扩展函数编译成 Java 的代码

/* Java */
char c = StringUtilKt.lastChar("Java");

回顾前文讲到的日期的 DSL:

val yesterday = 1.days.ago

为配合扩展函数,我们先降低 api 的整洁程度,先实现一个扩展函数的版本:

val yesterday = 1.days().ago()

1 为 Int 类型,显然 Int 并没有 days() 函数,因此days() 为扩展函数,伪代码如下:

fun Int.days() = {//逻辑实现}

结合 Java8 的 Time api,此处将会涉及到两个扩展函数,完整实现如下:

fun Int.days() = Period.ofDays(this)
fun Period.ago() = LocalDate.now() - this

若要实现最终的效果,实际上就是将扩展函数修改为扩展属性的方式即可(扩展属性需提供getter或setter,本质上等同于扩展函数):

val Int.days:Period
    get() = Period.ofDays(this)

val Period.ago:LocalDate
    get() = LocalDate.now() - this

代码虽少,却天马行空,妙趣横生。

lambda

lambda 为 Java8 提供的新特性,于2014年3月18日发布。在2018年的今天我们依然无法使用或者要花很大的代价才能在 Android 编程中使用,而 Kotlin 则帮助我们解决了这一瓶颈,这也是我们拥抱 Kotlin 的原因之一。

lambda 是构建整洁代码的一大利器。

1. lambda 表达式

下图是 lambda 表达式,他总是用一对大括号包装起来,可以作为值传递给下节要提到的高阶函数。

图片来自 Kotlin in Action

2. 高阶函数

关于高阶函数的定义,参考《Kotlin 实战》:

高阶函数就是以另一个函数作为参数或返回值的函数

如果用 lamba 来作为高价函数的参数(此时为形参),就必须先了解如何声明一个函数的形参类型,如下:

 

图片来自于 Kotlin in Action

相对于上一小节,我们应该弄清楚 lambda 作为实参和形参时的表现形式:

// printSum 为高阶函数,定义了 lambda 形参
fun printSum(sum:(Int,Int)->Int){
        val result = sum(1, 2)
        println(result)
}

// 以下 lambda 为实参,传递给高阶函数 printSum
val sum = {x:Int,y:Int->x+y}
printSum(sum)

有了高阶函数,我们可以很轻易地做到一个 lambda 嵌套另一个 lambda 的代码结构

3. 大括号放在最后

Kotlin 的 lambda 有个规约:如果 lambda 表达式是函数的最后一个实参,则可以放在括号外面,并且可以省略括号,如:

person.maxBy({ p:Person -> p.age })

// 可以写成
person.maxBy(){
    p:Person -> p.age
}

// 更简洁的风格:
person.maxBy{
    p:Person -> p.age
}

这个规约是 Kotlin DSL 实现嵌套结构的本质原因,比如上文提到的 anko Layout:

verticalLayout {
            val name = editText()
            button("Say Hello") {
                onClick { toast("Hello, ${name.text}!") }
            }
        }

这里 verticalLayout 中 嵌套了 button,想必该库定义了如下函数:

fun verticalLayout( ()->Unit ){
    
}

fun button( text:String,()->Unit ){
    
}

verticalLayout 和 button 均是高阶函数,结合大括号放在最后的规约,就形成了 lambda 嵌套的语法结构。

4. 带接收者的 lambda

lambda 作为形参函数声明时,可以携带接收者,如下图:

图片来自于 Kotlin in Action

带接收者的 lambda 丰富了函数声明的信息,当传递该 lambda值时,将携带该接收者,比如:

// 声明接收者
fun kotlinDSL(block:StringBuilder.()->Unit){
  block(StringBuilder("Kotlin"))
}

// 调用高阶函数
kotlinDSL {
  // 这个 lambda 的接收者类型为StringBuilder
  append(" DSL")
  println(this)
}

>>> 输出 Kotlin DSL

总而言之,lambda 在 Kotlin 和 Kotlin DSL 中扮演着很重要的角色,是实现整洁代码的必备语法糖。

中缀调用

Kotlin 中有种特殊的函数可以使用中缀调用,代码风格如下:

"key" to "value"

// 等价于
"key.to("value")

而 to() 的实现源码如下:

infix fun Any.to(that:Any) = Pair(this,that)

这段源码理解起来不难,infix 修饰符代表该函数支持中缀调用,然后为任意对象提供扩展函数 to,接受任意对象作为参数,最终返回键值对。

回顾下我们上文提到的不太规范的中文 api:

val yesteraty = 1 天 前

使用扩展函数和中缀调用便可实现:

object 前
infix fun Int.天(ago:前) = LocalDate.now() - Period.ofDays(this)

再比如上文提到的:

"kotlin" should start with "kot"

// 等价于
"kotlin".should(start).with("kot")

使用两个中缀调用便可实现,以下是伪代码:

object start
infix fun String.should(start:start):String = ""
infix fun String.with(str:String):String = ""

所以,中缀调用是实现类似英语句子结构 DSL 的核心。

invoke 约定

Kotlin 提供了 invoke 约定,可以让对象向函数一样直接调用,比如:

class Person(val name:String){
    operator fun invoke(){
        println("my name is $name")
    }
}

>>>val person = Person("geniusmart")
>>> person()
my name is geniusmart

回顾上文提到的 Gradle Kotlin DSL:

dependencies {
    compile("com.android.support:appcompat-v7:27.0.1")
    compile("com.android.support.constraint:constraint-layout:1.0.2")
}

// 等价于:
dependencies.compile("com.android.support:appcompat-v7:27.0.1")
dependencies.compile("com.android.support.constraint:constraint-layout:1.0.2")

这里,dependencies 是一个实例,既可以调用成员函数 compile,同时也可以直接传递 lambda 参数,后者便是采用了 invoke 约定,实现原理简化如下:

class Dependencies{

    fun compile(coordinate:String){
        println("add $coordinate")
    }

    operator fun invoke(block:Dependencies.()->Unit){
        block()
    }
}

>>>val dependencies = Dependencies()
>>>// 以两种方式分别调用 compile()

invoke 约定让对象调用函数的语法结构更加简洁。

总结

细细品味 Kotlin,你会发现她将代码整洁之道(Clean Code)和高效 Java 编程(Effective Java)中的部分精华融入到的语法和默认的规约中,因此她可以让开发者无形中写出整洁和高效的代码。

而更进一步, Kotlin DSL 则是对 Kotlin 所有语法糖的一个大融合,她的代码结构通常是链式调用、lambda 嵌套,并且接近于日常使用的英语句子,我们可以愉悦的使用 DSL 风格的 API,同时,也可以以此为思路,为社区贡献各种 Kotlin DSL。

Kotlin DSL 体现了代码的整洁之道,体现了天马行空的想象力,在 DSL 的点缀下,Kotlin 显示出整洁的美,自由的美。

Kotlin 有趣的外表之下,是一个更有趣的灵魂。

参考资料

  • 《Kotlin 实战》

 

 

 

 

 

 

 

 

 

 

=====================================================

=====================================================

=====================================================

 

 

 

 

 

 

 

 

 

 

 

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

原创: mikyou 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实现

 1//OnClickListener的定义
 2public interface OnClickListener {
 3    void onClick(View v);
 4}
 5
 6public void setOnClickListener(OnClickListener listener) {
 7    this.clickListener = listener;
 8}
 9
10//OnClickListener的使用
11mBtnSubmit.setOnClickListener(new View.OnClickListener() {
12    @Override
13    public void onClick(View v) {
14        //add your logic code
15    }
16});
  • 2、TextWatcher回调的Java实现

 1//TextWatcher的定义
 2public interface TextWatcher extends NoCopySpan {
 3    public void beforeTextChanged(CharSequence s, int start,int count, int after);
 4
 5    public void onTextChanged(CharSequence s, int start, int before, int count);
 6
 7    public void afterTextChanged(Editable s);
 8}
 9
10public void addTextChangedListener(TextWatcher watcher) {
11    if (mListeners == null) {
12        mListeners = new ArrayList<TextWatcher>();
13    }
14
15    mListeners.add(watcher);
16}
17
18//TextWatcher的使用
19mEtComment.addTextChangedListener(new TextWatcher() {
20    @Override
21    public void beforeTextChanged(CharSequence s, int start, int count, int after) {
22             //add your logic code
23    }
24
25    @Override
26    public void onTextChanged(CharSequence s, int start, int before, int count) {
27            //add your logic code
28    }
29
30    @Override
31    public void afterTextChanged(Editable s) {
32            //add your logic code
33    }
34});

二、使用Kotlin来改造Java中的回调

针对上述Java中的回调写法,估计大部分人转到Kotlin后,估计会做如下处理:

1、如果接口只有一个回调函数可以直接使用lamba表达式实现回调的简写。

2、如果接口中含有多个回调函数,都会使用object对象表达式来实现的。

以改造上述代码为例:

  • 1、(只有一个回调函数简写形式)OnClickListener回调Kotlin改造

 1//只有一个回调函数普通简写形式: OnClickListener的使用
 2mBtnSubmit.setOnClickListener { view ->
 3    //add your logic code
 4}
 5
 6//针对OnClickListener监听设置Coroutine协程框架中onClick扩展函数的使用
 7mBtnSubmit.onClick { view ->
 8    //add your logic code
 9}
10
11//Coroutine协程框架: onClick的扩展函数定义
12fun android.view.View.onClick(
13        context: CoroutineContext = UI,
14        handler: suspend CoroutineScope.(v: android.view.View?) -> Unit
15) {
16    setOnClickListener { v ->
17        launch(context) {
18            handler(v)
19        }
20    }
21}
  • 2、(多个回调函数object表达式)TextWatcher回调的Kotlin改造(object对象表达式)

 1mEtComment.addTextChangedListener(object: TextWatcher{
 2    override fun afterTextChanged(s: Editable?) {
 3       //add your logic code
 4    }
 5
 6    override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
 7       //add your logic code
 8    } 
 9
10    override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
11       //add your logic code
12    }
13
14 })

关于object对象表达式实现的Kotlin中回调,有不少的Kotlin的小伙伴在公众号留言向我吐槽过,感觉这样的写法是直接从Java中的翻译过来的一样,完全看不出Kotlin的优势在哪。问我有没有什么更加具有Kotlin风味的写法,当然是有的,请接着往下看。

三、进一步让你的回调更具Kotlin风味(DSL配置回调)

其实如果你看过很多国外大佬的有关Koltin项目的源码,你就会发现他们写回调很少去使用object表达式去实现回调,而是采用另一种方式去实现,并且整体写法看起来更具有Kotlin风味。即使内部用到object表达式,暴露给外层中间都会做一层DSL配置转换,让外部调用起来更加Kotlin化。以Github中的MaterialDrawer项目(目前已经有1W多star)中官方指定MatrialDrawer项目Kotlin版本实现的MaterialDrawerKt项目中间一段源码为例:

  • 1、DrawerImageLoader 回调定义

 1//注意: 这个函数参数是一个带返回值的lambda表达式
 2public fun drawerImageLoader(actions: DrawerImageLoaderKt.() -> Unit): DrawerImageLoader.IDrawerImageLoader {
 3    val loaderImpl = DrawerImageLoaderKt().apply(actions).build() //
 4    DrawerImageLoader.init(loaderImpl)
 5    return loaderImpl
 6}
 7
 8//DrawerImageLoaderKt: DSL listener Builder类
 9public class DrawerImageLoaderKt {
10    //定义需要回调的函数lamba成员对象
11    private var setFunc: ((ImageView, Uri, Drawable?, String?) -> Unit)? = null
12    private var placeholderFunc: ((Context, String?) -> Drawable)? = null
13
14    internal fun build() = object : AbstractDrawerImageLoader() {
15
16        private val setFunction: (ImageView, Uri, Drawable?, String?) -> Unit = setFunc
17                ?: throw IllegalStateException("DrawerImageLoader has to have a set function")
18
19        private val placeholderFunction = placeholderFunc
20                ?: { ctx, tag -> super.placeholder(ctx, tag) }
21
22        override fun set(imageView: ImageView, uri: Uri, placeholder: Drawable?, tag: String?) = setFunction(imageView, uri, placeholder, tag)
23
24        override fun placeholder(ctx: Context, tag: String?) = placeholderFunction(ctx, tag)
25
26    }
27
28    //暴露给外部调用的回调函数,在构建类中类似setter,getter方法
29    public fun set(setFunction: (imageView: ImageView, uri: Uri, placeholder: Drawable?, tag: String?) -> Unit) {
30        this.setFunc = setFunction
31    }
32
33    public fun placeholder(placeholderFunction: (ctx: Context, tag: String?) -> Drawable) {
34        this.placeholderFunc = placeholderFunction
35    }
  • 2、DrawerImageLoader回调使用

 1 drawerImageLoader {
 2   //内部的回调函数可以选择性重写
 3    set { imageView, uri, placeholder, _ ->
 4        Picasso.with(imageView.context)
 5               .load(uri)
 6               .placeholder(placeholder)
 7               .into(imageView)
 8        }
 9
10    cancel { imageView ->
11        Picasso.with(imageView.context)
12               .cancelRequest(imageView)
13    }
14}

可以看到使用DSL配置的回调更加具有Kotlin风味,让整个回调看起来非常的舒服,那种效果岂止丝滑。

四、DSL配置回调基本步骤

在Kotlin的一个类中实现了DSL配置回调非常简单主要就三步:

  • 1、定义一个回调的Builder类,并且在类中定义回调lamba表达式对象成员,最后再定义Builder类的成员函数,这些函数就是暴露给外部回调的函数。个人习惯把它作为一个类的内部类。类似下面这样

 1class AudioPlayer(context: Context){
 2     //other logic ...
 3
 4     inner class ListenerBuilder {
 5        internal var mAudioPlayAction: ((AudioData) -> Unit)? = null
 6        internal var mAudioPauseAction: ((AudioData) -> Unit)? = null
 7        internal var mAudioFinishAction: ((AudioData) -> Unit)? = null
 8
 9        fun onAudioPlay(action: (AudioData) -> Unit) {
10            mAudioPlayAction = action
11        }
12
13        fun onAudioPause(action: (AudioData) -> Unit) {
14            mAudioPauseAction = action
15        }
16
17        fun onAudioFinish(action: (AudioData) -> Unit) {
18            mAudioFinishAction = action
19        }
20    }
21}
  • 2、然后,在类中声明一个ListenerBuilder的实例引用,并且暴露一个设置该实例对象的一个方法,也就是我们常说的注册事件监听或回调的方法,类似setOnClickListenter这种。但是需要注意的是函数的参数是带ListenerBuilder返回值的lamba,类似下面这样:

1class AudioPlayer(context: Context){
2      //other logic ...
3
4     private lateinit var mListener: ListenerBuilder
5     fun registerListener(listenerBuilder: ListenerBuilder.() -> Unit) {//带ListenerBuilder返回值的lamba
6        mListener = ListenerBuilder().also(listenerBuilder)
7     }
8}     
  • 3、最后在触发相应事件调用Builder实例中lamba即可

 1class AudioPlayer(context: Context){
 2      //other logic ...
 3     val mediaPlayer = MediaPlayer(mContext)
 4        mediaPlayer.play(mediaItem, object : PlayerCallbackAdapter() {
 5            override fun onPlay(item: MediaItem?) {
 6                if (::mListener.isInitialized) {
 7                    mListener.mAudioPlayAction?.invoke(mAudioData)
 8                }
 9            }
10
11            override fun onPause(item: MediaItem?) {
12                if (::mListener.isInitialized) {
13                    mListener.mAudioPauseAction?.invoke(mAudioData)
14                }
15            }
16
17            override fun onPlayCompleted(item: MediaItem?) {
18                if (::mListener.isInitialized) {
19                    mListener.mAudioFinishAction?.invoke(mAudioData)
20                }
21            }
22        })  
23}     
  • 4、外部调用

 1val audioPlayer = AudioPlayer(context)
 2    audioPlayer.registerListener {
 3       //可以任意选择需要回调的函数,不必要完全重写
 4        onAudioPlay {
 5            //todo your logic
 6        }
 7
 8        onAudioPause {
 9           //todo your logic
10        }
11
12        onAudioFinish {
13           //todo your logic
14        }
15    }

相比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、代码拆解
    为了更加清楚论证上面的阐述,我们可以把代码拆解一下:

 1mAudioPlayer.registerListener({
 2    //registerListener参数是个带ListenerBuilder实例返回值的lambda
 3    //所以这里this就是内部指代为ListenerBuilder实例
 4    this.onAudioPlay ({  
 5        //logic block 
 6    })
 7    this.onAudioPause ({ 
 8        // logic block
 9    })
10    this.onAudioFinish({ 
11        // logic block
12    })
13  })

onAudioPlay为例其他同理,调用ListenerBuilderonAudioPlay函数,并传入block块来赋值初始化ListenerBuilder类中的mAudioPlayActionlambda实例,当AudioPlayer中的onPlay函数被回调时,就执行mAudioPlayActionlambda。

貌似看起来object对象表达式回调相比DSL回调表现那么一无是处,是不是完全可以摒弃object对象表达式这种写法呢?其实不然,object对象表达式这种写法也是有它优点的,具体有什么优点,请接着看它们两种形式对比。

六、object对象表达式回调和DSL回调对比

  • 1、调用写法上对比

 1//使用DSL配置回调
 2val audioPlayer = AudioPlayer(context)
 3    audioPlayer.registerListener {
 4       //可以任意选择需要回调的函数,不必要完全重写
 5        onAudioPlay {
 6            //todo your logic
 7        }
 8
 9        onAudioPause {
10           //todo your logic
11        }
12
13        onAudioFinish {
14           //todo your logic
15        }
16    }
17
18//使用object对象表达式回调
19val audioPlayer = AudioPlayer(context)
20    audioPlayer.registerListener(object: AudioPlayListener{
21        override fun onAudioPlay(audioData: AudioData) {
22                    //todo your logic
23        }
24        override fun onAudioPause(audioData: AudioData) {
25                    //todo your logic
26        }
27        override fun onAudioFinish(audioData: AudioData) {
28                    //todo your logic
29        }
30    })

调用写法对比明显感觉DSL配置更加符合Kotlin风格,所以DSL配置回调更胜一筹

  • 2、使用上对比

使用上DSL有个明显优势就是对于不需要监听的回调函数可以直接省略,而对于object表达式是直接实现一个接口回调必须重写,虽然它也能做到任意选择自己需要方法回调,但是还是避免不了一层callback adapter层的处理。所以与其做个adapter层还不如一步到位。所以DSL配置回调更胜一筹

  • 3、性能上对比

其实通过上述调用写法上看,一眼就能看出来,DSL配置回调这种方式会针对每个回调函数都会创建lambda实例对象,而object对象表达式不管内部回调的方法有多少个,都只会生成一个匿名对象实例。区别就在这里,所以在性能方面object对象表达式这种方式会更优一点,但是通过问过一些Kotlin社区的大佬们他们还是更倾向于DSL配置这种写法。所以其实这两种方式都挺好的,看不同需求,自己权衡选择即可, 反正我个人挺喜欢DSL那种。为了验证我们上述所说的,不妨来看下两种方式下反编译的代码,看看是否是我们所说的那样:

 1//DSL配置回调反编译code
 2   public final void setListener(@NotNull Function1 listener) {
 3      Intrinsics.checkParameterIsNotNull(listener, "listener");
 4      ListenerBuilder var2 = new ListenerBuilder();
 5      listener.invoke(var2);
 6      ListenerBuilder var10000 = this.mListener;
 7      //获取AudioPlay方法对应的实例对象
 8      Function0 var3 = var10000.getMAudioPlayAction$Coroutine_main();
 9      Unit var4;
10      if (var3 != null) {
11         var4 = (Unit)var3.invoke();
12      }
13      //获取AudioPause方法对应的实例对象
14      var3 = var10000.getMAudioPauseAction$Coroutine_main();
15      if (var3 != null) {
16         var4 = (Unit)var3.invoke();
17      }
18      //获取AudioFinish方法对应的实例对象
19      var3 = var10000.getMAudioFinishAction$Coroutine_main();
20      if (var3 != null) {
21         var4 = (Unit)var3.invoke();
22      }
23   }
24
25//object对象表达式反编译code
26 public static final void main(@NotNull String[] args) {
27      Intrinsics.checkParameterIsNotNull(args, "args");
28      int count = true;
29      PlayerPlugin player = new PlayerPlugin();
30      //new Callback一个实例
31      player.setCallback((Callback)(new Callback() {
32         public void onAudioPlay() {
33         }
34
35         public void onAudioPause() {
36         }
37
38         public void onAudioFinish() {
39         }
40      }));
41   }

七、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~~~

DslListenerBuilder插件下载地址(https://github.com/BayMikyou/DslListenerBuilder/releases/tag/1.0.0)

DslListenerBuilder插件源码地址(https://github.com/BayMikyou/DslListenerBuilder)

Velocity模板基本语法(http://www.blogjava.net/alexwan/archive/2008/06/07/206473.html)

使用 Velocity 模板引擎快速生成代码(https://www.ibm.com/developerworks/cn/java/j-lo-velocity1/)

十、总结

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

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值