当前位置: 首页 > news >正文

Kotlin 高端玩法之DSL

如何在 kotlin 优雅的封装匿名内部类(DSL、高阶函数)

匿名内部类在 Java 中是经常用到的一个特性,例如在 Android 开发中的各种 Listener,使用时也很简单,比如:

//lambda
button.setOnClickListener(v -> {//do some thing
});
//匿名内部类
button.setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View v) {//do some thing}
});

只有一个函数的接口在 Java 和 Kotlin 中都可以很方便的使用 lambda 表达式来缩略,但是如果接口含有多个函数,使用起来就比较”不优雅“了,例如:

etString.addTextChangedListener(object :TextWatcher{override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {TODO("Not yet implemented")}override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {TODO("Not yet implemented")}override fun afterTextChanged(s: Editable?) {TODO("Not yet implemented")}
})

使用起来与 Java 基本差不多,通过 object 关键字实现了一个匿名内部类,这种方法没什么大问题,例如上面的例子中,三个回调函数并非每次都要使用,很多场景可能只会用到其中一个或者几个,其余的都是空实现,每次都写这样一个匿名内部类只不过是不优雅而已。

在 Kotlin 中我们可以有两种方式实现比较优雅的使用匿名内部类:

1. DSL

2. 高阶函数

DSL

DSL 方式实现封装可以分为以下几步:

1.创建接口实现类:XxxxInterfaceDslImpl

还有上面的 TextWatcher 作为例子:

class TextWatcherDslImpl : TextWatcher {//原接口对应的kotlin函数对象private var afterTextChanged: ((Editable?) -> Unit)? = nullprivate var beforeTextChanged: ((CharSequence?, Int, Int, Int) -> Unit)? = nullprivate var onTextChanged: ((CharSequence?, Int, Int, Int) -> Unit)? = null/*** DSL中使用的函数,一般保持同名即可*/fun afterTextChanged(method: (Editable?) -> Unit) {afterTextChanged = method}fun beforeTextChanged(method: (CharSequence?, Int, Int, Int) -> Unit) {beforeTextChanged = method}fun onTextChanged(method: (CharSequence?, Int, Int, Int) -> Unit) {onTextChanged = method}/*** 实现原接口的函数*/override fun afterTextChanged(s: Editable?) {afterTextChanged?.invoke(s)}override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {beforeTextChanged?.invoke(s, start, count, after)}override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {onTextChanged?.invoke(s, start, before, count)}
}

这个实现类由三个部分组成:

1. 原接口方法对应的 Kotlin 函数对象,函数对象的签名与对应的方法签名保持一致。

2. DSL 函数,函数名称、签名都与原接口的方法一一对应,用于接收 lambda 赋值给 Kotlin 函数对象。

3. 原接口方法的实现,每个接口方法的实现,都是对实现类中 Kotlin 函数对象的调用。

2.创建与原函数同名的扩展函数,函数参数为实现类扩展函数

fun TextView.addTextChangedListenerDsl(init: TextWatcherDslImpl.() -> Unit) {val listener = TextWatcherDslImpl()listener.init()this.addTextChangedListener(listener)
}

扩展函数与原函数同名可以方便使用者调用,无需记忆其他函数名,如果担心混淆,可以在函数名后加上 Dsl 用以区分。该函数的参数是我们第一步创建的实现类的扩展函数,这是为了实现 DSL 语法。

3.使用

etString.addTextChangedListenerDsl {afterTextChanged {if (it.toString().length >= 4) {KeyboardUtils.toggleSoftInput()}}
}

使用这种方式时,可以说相当之优雅,我们只需要调用我们需要实现的接口方法即可,不需要使用的接口方法默认空实现。

高阶函数

高阶函数方式比 DSL 方式更简单一点:

inline fun TextView.addTextChangedListenerClosure(crossinline afterTextChanged: (Editable?) -> Unit = {},crossinline beforeTextChanged: (CharSequence?, Int, Int, Int) -> Unit = { charSequence, start, count, after -> },crossinline onTextChanged: (CharSequence?, Int, Int, Int) -> Unit = { charSequence, start, after, count -> }
) {val listener = object : TextWatcher {override fun afterTextChanged(s: Editable?) {afterTextChanged.invoke(s)}override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {beforeTextChanged.invoke(s, start, count, after)}override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {onTextChanged.invoke(s, start, before, count)}}this.addTextChangedListener(listener)
}

我们创建一个同名扩展函数,使用 Closure 尾缀作为区分,该函数的参数为与接口方法一一对应的 Kotlin 函数对象,并给其默认值赋值为 {} 即空实现,在函数体里通过 object 关键字构建匿名内部类实现对象,在其接口方法实现中调用与之一一对应的 Kotlin 函数对象。

使用方式上与普通的 Kotlin 高阶函数使用方式相同:

etString.addTextChangedListenerClosure(afterTextChanged = {if (it.toString().length >= 4) {KeyboardUtils.toggleSoftInput()}},
)

tips:

上面示例的扩展函数中,我们使用了 inline 与 crossinline 两个关键字,这是 Koltin 特有的。inline 关键字通常用于修饰高阶函数,用于提升性能。crossinline 声明的 lambda 不允许局部返回,用于避免调用者错误的使用 return 导致函数中断。

提供一个示例代码,亲自尝试一下也许可以更好的理解:

@Test
fun testInline() {testClosure {return}
}
private inline fun testClosure(test: (String) -> String ) {println("step 1")println(test("step test"))println("step 2")
}

在 Kotlin 中巧妙的使用 DSL 封装 SpannableStringBuilder

源从何来

在 Android 开发中 Spannable 实现富文本显示,也算是一个比较常见的使用场景,例如在登录页显示《隐私政策》、《服务协议》,通常这是一个有自定义颜色与点击事件的 Span,使用起来大致需要写如下代码:

private fun agreePrivate() {val tv = findViewById<TextView>(R.id.tv_agree)val builder = SpannableStringBuilder()val text = "我已详细阅读并同意《隐私政策》"builder.append(text)//设置span点击事件val clickableSpan = object :ClickableSpan(){override fun onClick(widget: View) {//do some thing}}builder.setSpan(clickableSpan, 9, 15, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)//设置span无下划线val noUnderlineSpan = NoUnderlineSpan()builder.setSpan(noUnderlineSpan, 9, 15, Spanned.SPAN_MARK_MARK)//设置span文字颜色val foregroundColorSpan = ForegroundColorSpan(Color.parseColor("#0099FF"))builder.setSpan(foregroundColorSpan, 9, 15, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)//设置可点击tv.movementMethod = LinkMovementMethod.getInstance()tv.setText(builder)
}class NoUnderlineSpan : UnderlineSpan() {override fun updateDrawState(ds: TextPaint) {ds.color = ds.linkColords.isUnderlineText = false}
}

用起来还是比较麻烦的,就像上面的代码只是一个 span 就写了三个 setSpan,如果需要使用 Span 的地方比较多,这些代码看起来实在是不够优雅。有没有更优雅方式呢,答案就是 DSL,上面的代码最终通过 DSL 封装后如下:

tvTestDsl.buildSpannableString {addText("我已详细阅读并同意")addText("《隐私政策》"){setColor("#0099FF")onClick(false) {//do some thing}}
}

他们的显示效果是完全一致的,无疑 DSL 的方式更加优雅,对于调用者而言也更加方便。

实现思路

当我有用 DSL 封装 Spannable 这个想法时,我首先写的是我应该如何去使用它,当时我在纸上胡乱的写下了上面的那段代码。

1. 它应该是 TextView的一个扩展函数。

2. 它的内部是 DSL 风格的代码。

3. 它的每段文字都有设置颜色 & 点击事件的函数。

所以就有了如下的两个接口与扩展函数:

interface DslSpannableStringBuilder {//增加一段文字fun addText(text: String, method: (DslSpanBuilder.() -> Unit)? = null)
}interface DslSpanBuilder {//设置文字颜色fun setColor(color: String)//设置点击事件fun onClick(useUnderLine: Boolean = true, onClick: (View) -> Unit)
}//为 TextView 创建扩展函数,其参数为接口的扩展函数
fun TextView.buildSpannableString(init: DslSpannableStringBuilder.() -> Unit) {//具体实现类val spanStringBuilderImpl = DslSpannableStringBuilderImpl()spanStringBuilderImpl.init()movementMethod = LinkMovementMethod.getInstance()//通过实现类返回SpannableStringBuildertext = spanStringBuilderImpl.build()
}

上一篇文章我们说了, 在 DSL 风格的函数中,其参数应当是某个接口(或者他的实现类)的扩展函数,这样我们相当于通过接口来限定了在 DSL 中可调用的函数。上一篇中使用的是实现类,本文中使用的是接口,原因很简单,上文是扩展原有接口变成 DSL 风格,本文是直接从无至有,实现的 DSL 风格。

实现相应接口

其实对于像我这样初次接触DSL 的新手而言,思路是最难的,有了接口,有了 DSL 层级,剩下的就是相对简单的实现了。直接看代码:

class DslSpannableStringBuilderImpl : DslSpannableStringBuilder {private val builder = SpannableStringBuilder()//记录上次添加文字后最后的索引值var lastIndex: Int = 0var isClickable = falseoverride fun addText(text: String, method: (DslSpanBuilder.() -> Unit)?) {val start = lastIndexbuilder.append(text)lastIndex += text.lengthval spanBuilder = DslSpanBuilderImpl()method?.let { spanBuilder.it() }spanBuilder.apply {onClickSpan?.let {builder.setSpan(it, start, lastIndex, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)isClickable = true}if (!useUnderLine) {val noUnderlineSpan = NoUnderlineSpan()builder.setSpan(noUnderlineSpan, start, lastIndex, Spanned.SPAN_MARK_MARK)}foregroundColorSpan?.let {builder.setSpan(it, start, lastIndex, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)}}}fun build(): SpannableStringBuilder {return builder}
}class DslSpanBuilderImpl : DslSpanBuilder {var foregroundColorSpan: ForegroundColorSpan? = nullvar onClickSpan: ClickableSpan? = nullvar useUnderLine = trueoverride fun setColor(color: String) {foregroundColorSpan = ForegroundColorSpan(Color.parseColor(color))}override fun onClick(useUnderLine: Boolean, onClick: (View) -> Unit) {onClickSpan = object : ClickableSpan() {override fun onClick(widget: View) {onClick(widget)}}this.useUnderLine = useUnderLine}
}class NoUnderlineSpan : UnderlineSpan() {override fun updateDrawState(ds: TextPaint) {ds.color = ds.linkColords.isUnderlineText = false}
}

总结

想要使用 DSL 离不开接口与扩展函数,需要先创建想要在 DSL 中使用的函数的接口,然后声明函数参数为该接口的扩展函数。

如果 DSL 中存在像我这样的嵌套,那么就需要为这个嵌套再创建一个用于嵌套调用的接口(本文的嵌套是故意为之,使用单个接口传参也可以实现这样的效果)。

相关文章:

Kotlin 高端玩法之DSL

如何在 kotlin 优雅的封装匿名内部类&#xff08;DSL、高阶函数&#xff09;匿名内部类在 Java 中是经常用到的一个特性&#xff0c;例如在 Android 开发中的各种 Listener&#xff0c;使用时也很简单&#xff0c;比如&#xff1a;//lambda button.setOnClickListener(v -> …...

理光M2701复印机载体初始化方法

理光M2701基本参数&#xff1a; 产品类型&#xff1a;数码复合机 颜色类型&#xff1a;黑白 复印速度&#xff1a;单面&#xff1a;27cpm 双面&#xff1a;16cpm 涵盖功能&#xff1a;复印、打印、扫描 网络功能&#xff1a;支持无线、有线网络打印 接口类型&#xff1a;USB2.0…...

2.25Maven的安装与配置

一.Mavenmaven是一个Java世界中,非常知名的"工程管理工具"/构建工具"核心功能:1.管理依赖在进行一个A 操作之前,要先进行一个B操作.依赖有的时候是很复杂的,而且是嵌套的2.构建/编译(也是在调用jdk)3. 打包把java代码给构建成jar或者warjar就是一个特殊的压缩包…...

《英雄编程体验课》第 12 课 | 递归

文章目录 零、写在前面一、搜索算法的原理二、深度优先搜索三、基于DFS的记忆化搜索四、基于DFS的剪枝五、基于DFS的A*(迭代加深,IDA*)零、写在前面 该章节节选自 《夜深人静写算法》,主要讲解最基础的搜索算法,其中用到的思想就是递归,当然,如果已经对本套体验课了如指…...

35测试不如狗?是你自己技术不够的怨怼罢了

一、做软件测试怎么样&#xff1f; 引用著名软件测试专家、清华大学郑人杰教授的说法&#xff1a;软件测试工程师是一个越老越吃香的职业。 其中就表达了软件测试工作相对稳定、对年龄没有限制、而且随着项目经验的不断增长和对行业背景的深入了解&#xff0c;会越老越吃香。…...

【代码训练营】day42 | 1049. 最后一块石头的重量 II 494. 目标和 474.一和零

所用代码 java 最后一块石头的重量II LeetCode 1049 题目链接&#xff1a;最后一块石头的重量II LeetCode 1049 - 中等 思路 无。 把石头分成重量总和近似两堆&#xff0c;然后两堆石头相撞&#xff0c;剩下的就是最小的石头。每个石头只能用一次&#xff0c;01背包&#xf…...

Golang协程常见面试题

协程面试题交替打印奇数和偶数N个协程打印1到maxVal交替打印字符和数字交替打印字符串三个协程打印ABCChannel练习交替打印奇数和偶数 下面让我们一起来看看golang当中常见的算法面试题 使用两个goroutine交替打印1-100之间的奇数和偶数, 输出时按照从小到大输出. 方法一&…...

种群多样性:智能优化算法求解基准测试函数F1-F23种群动态变化图(视频)

智能优化算法求解基准测试函数F1种群动态变化图智能优化算法求解基准测试函数F2种群动态变化图智能优化算法求解基准测试函数F3种群动态变化图智能优化算法求解基准测试函数F4种群动态变化图智能优化算法求解基准测试函数F5种群动态变化图智能优化算法求解基准测试函数F6种群动…...

Qt 中的XML

XML的基本介绍&#xff1a; 在前端开发中&#xff1a;HTML是用来显示数据&#xff0c;而XML是用来传输和存储数据的 XML 指可扩展标记语言&#xff08;EXtensible Markup Language&#xff09;XML 是一种标记语言&#xff0c;很类似 HTMLXML 的设计宗旨是传输数据&#xff0c;而…...

网络应用之URL

URL学习目标能够知道URL的组成部分1. URL的概念URL的英文全拼是(Uniform Resoure Locator),表达的意思是统一资源定位符&#xff0c;通俗理解就是网络资源地址&#xff0c;也就是我们常说的网址。2. URL的组成URL的样子:https://news.163.com/18/1122/10/E178J2O4000189FH.html…...

【Linux】重定向原理dup2缓冲区

文章目录重定向原理输出重定向关于FILE解释输出重定向原理追加重定向输入重定向dup2缓冲区语言级别的缓冲区内核缓冲区重定向原理 重定向的本质就是修改文件描述符下标对应的struct file*的内容 输出重定向 输出重定向就是把本来应该输出到显示器的数据重定向输出到另一个文…...

ROG配置ubuntu20.04.5双系统要点

win11ubuntu20.04.5 1. BIOS设置 开机长按F2进入bios设置&#xff0c;修改advanced参数&#xff1a; boot -> 关闭fast bootsecurity -> 关闭secure boot设置VMD controller为Disabled&#xff08;其他电脑是修改硬盘的SATA和ACHI模式&#xff09;。但是改了之后windo…...

机械革命旷世G16电脑开机变成绿屏了无法使用怎么办?

机械革命旷世G16电脑开机变成绿屏了无法使用怎么办&#xff1f;最近有用户使用的机械革命旷世G16电脑一开机之后&#xff0c;电脑屏幕就变成了绿色的&#xff0c;无法进行任何的操作。出现这个问题可能是因为电脑中病毒了&#xff0c;或者是系统出现故障。我们可以通过U盘来重新…...

python中关于time模块的讲解---指定格式时间字符串转为时间戳

本文章可以解决任意字符串格式时间转为时间戳 返回json格式 可以在此基础上进行修改 时间格式控制符 说明 %Y 四位数的年份&#xff0c;取值范围为0001~9999,如1900 %m 月份&#xff08;01~12&#xff09;&#xff0c;例如10 %d 月中的一天&#xff08;01~31&#xff09;例…...

MySql存储引擎与索引

MySql引擎 存储引擎是具体操作数据的地方&#xff0c;是一种对数据存储的技术与其配套的功能 不同存储引擎所采用存储的方式的不同&#xff0c;并且索引技巧与锁定水平也不同 根据业务的需求灵活的选择存储引擎即可满足的实际的需要 Innodb Innodb是MySql中的默认安装的引擎…...

typing库

typing 库 引入 在日常代码编写中&#xff0c;由于python语言特性&#xff0c;不用像go等编译性语言一样&#xff0c;在定义函数时就规范参数和放回值的类型。 def demo(a, b):return "ab" 此时 a 和 b 可以传入任意类型参数毫无疑问&#xff0c;这一特性&#…...

linux shell 入门学习笔记10内置shell命令

bash基础的内置命令 echoevalexecexportreadshift echo命令 -n 不换行输出 -e 解析字符串中的特殊符号\n 换行 \r 回车 \t 制表符 四个空格 \b 退格-n参数演示 xiao123xiao123:~/Downloads$ echo 你真胖;echo 你还挺可爱; 你真胖 你还挺可爱 xiao123xiao123:~/Downloads$ ec…...

[动手写操作系统]-02-开机运行系统并打印‘hello‘

文章目录 理解三个概念: 中断interrupts, CPU,寄存器registers 目标:让上一个静默的界面打印一些文本 我们将改进我们的无限循环引导扇区并在屏幕上打印一些东西。我们将为此提出中断。 我们尝试将"Hello"写到寄存器al, 字节0x0e写到ah (the higher part of ax),并…...

Delete `␍`eslint(prettier/prettier) in vscode 的解决方案

错误描述从 Github 仓库拉取代码&#xff0c;使用 vscode 打开&#xff0c;页面报错&#xff0c;每一行都爆红 &#xff08;如下图&#xff09;问题原因由于历史原因&#xff0c;windows下和linux下的文本文件的换行符不一致。Windows在换行的时候&#xff0c;使用了换行符CRLF…...

gof23 设计模式 各个模式代码demo

Gof23 设计模式&#xff0c;也叫Gang of Four&#xff08;GoF&#xff09;设计模式&#xff0c;是由四位设计模式大师&#xff08;Erich Gamma、Richard Helm、Ralph Johnson 和 John Vlissides&#xff09;撰写的一本书——《设计模式&#xff1a;可复用面向对象软件的基础》所…...

React hook之useRef

React useRef 详解 useRef 是 React 提供的一个 Hook&#xff0c;用于在函数组件中创建可变的引用对象。它在 React 开发中有多种重要用途&#xff0c;下面我将全面详细地介绍它的特性和用法。 基本概念 1. 创建 ref const refContainer useRef(initialValue);initialValu…...

练习(含atoi的模拟实现,自定义类型等练习)

一、结构体大小的计算及位段 &#xff08;结构体大小计算及位段 详解请看&#xff1a;自定义类型&#xff1a;结构体进阶-CSDN博客&#xff09; 1.在32位系统环境&#xff0c;编译选项为4字节对齐&#xff0c;那么sizeof(A)和sizeof(B)是多少&#xff1f; #pragma pack(4)st…...

线程与协程

1. 线程与协程 1.1. “函数调用级别”的切换、上下文切换 1. 函数调用级别的切换 “函数调用级别的切换”是指&#xff1a;像函数调用/返回一样轻量地完成任务切换。 举例说明&#xff1a; 当你在程序中写一个函数调用&#xff1a; funcA() 然后 funcA 执行完后返回&…...

【快手拥抱开源】通过快手团队开源的 KwaiCoder-AutoThink-preview 解锁大语言模型的潜力

引言&#xff1a; 在人工智能快速发展的浪潮中&#xff0c;快手Kwaipilot团队推出的 KwaiCoder-AutoThink-preview 具有里程碑意义——这是首个公开的AutoThink大语言模型&#xff08;LLM&#xff09;。该模型代表着该领域的重大突破&#xff0c;通过独特方式融合思考与非思考…...

Springcloud:Eureka 高可用集群搭建实战(服务注册与发现的底层原理与避坑指南)

引言&#xff1a;为什么 Eureka 依然是存量系统的核心&#xff1f; 尽管 Nacos 等新注册中心崛起&#xff0c;但金融、电力等保守行业仍有大量系统运行在 Eureka 上。理解其高可用设计与自我保护机制&#xff0c;是保障分布式系统稳定的必修课。本文将手把手带你搭建生产级 Eur…...

现代密码学 | 椭圆曲线密码学—附py代码

Elliptic Curve Cryptography 椭圆曲线密码学&#xff08;ECC&#xff09;是一种基于有限域上椭圆曲线数学特性的公钥加密技术。其核心原理涉及椭圆曲线的代数性质、离散对数问题以及有限域上的运算。 椭圆曲线密码学是多种数字签名算法的基础&#xff0c;例如椭圆曲线数字签…...

QT: `long long` 类型转换为 `QString` 2025.6.5

在 Qt 中&#xff0c;将 long long 类型转换为 QString 可以通过以下两种常用方法实现&#xff1a; 方法 1&#xff1a;使用 QString::number() 直接调用 QString 的静态方法 number()&#xff0c;将数值转换为字符串&#xff1a; long long value 1234567890123456789LL; …...

rnn判断string中第一次出现a的下标

# coding:utf8 import torch import torch.nn as nn import numpy as np import random import json""" 基于pytorch的网络编写 实现一个RNN网络完成多分类任务 判断字符 a 第一次出现在字符串中的位置 """class TorchModel(nn.Module):def __in…...

【分享】推荐一些办公小工具

1、PDF 在线转换 https://smallpdf.com/cn/pdf-tools 推荐理由&#xff1a;大部分的转换软件需要收费&#xff0c;要么功能不齐全&#xff0c;而开会员又用不了几次浪费钱&#xff0c;借用别人的又不安全。 这个网站它不需要登录或下载安装。而且提供的免费功能就能满足日常…...

PHP 8.5 即将发布:管道操作符、强力调试

前不久&#xff0c;PHP宣布了即将在 2025 年 11 月 20 日 正式发布的 PHP 8.5&#xff01;作为 PHP 语言的又一次重要迭代&#xff0c;PHP 8.5 承诺带来一系列旨在提升代码可读性、健壮性以及开发者效率的改进。而更令人兴奋的是&#xff0c;借助强大的本地开发环境 ServBay&am…...