Kotlin带接收者的Lambda介绍和应用(封装DialogFragment)
方法的调用封装到了方法里面,这样重复的代码就只写一次,不用每次都写了。这里,对于 “确定” 和 “取消” 按钮也是很常用的,而且不管你是点了确定还是取消,点击之后对话框都会自动取消,所以上面代码中的。从对比上来看,长得差不多,带接收者就是把括号中的参数移到括号前面,并加了一个 “.” ,带。参数大多数情况下都是用不到的,但是每次都要写也很麻烦,而且函数名。这样做是可以的,但是真的很麻烦,能不能像。
先来看一个具体应用:假设我们有一个App,App中有一个退出应用的按钮,点击该按钮后并不是立即退出,而是先弹出一个对话框,询问用户是否确定要退出,用户点了确定再退出,点取消则不退出,示例代码如下:
button.setOnClickListener {
AlertDialog.Builder(this)
.setTitle("提示")
.setMessage("是否要退出应用?")
.setPositiveButton("确定") { _, _ -> exitApp() }
.setNegativeButton("取消", null)
.show()
}
我们可能经常需要用到对话框,每次都要写很多重复代码,所以就会想要简化一下,比如,首先把链式调用中的 “.” 给去掉,通过使用apply扩展函数,这就是一个带接收者的扩展函数,所以在apply中的接收者this可以省略不写,这样就能不写 “.” 了,如下:
button.setOnClickListener {
AlertDialog.Builder(this).apply {
setTitle("提示")
setMessage("是否要退出应用?")
setPositiveButton("确定") { _, _ -> exitApp() }
setNegativeButton("取消", null)
show()
}
}
这里面有一些代码是每次都一模一样的,所以还可以优化,Builder每次都要创建,show()函数每次都要调用,这些可以封装到函数中,如下:
button.setOnClickListener {
showDialog(this) { builder ->
builder
.setTitle("提示")
.setMessage("是否要退出应用?")
.setPositiveButton("确定") { _, _ -> exitApp() }
.setNegativeButton("取消", null)
}
}
fun showDialog(context: Context, init: (AlertDialog.Builder) -> Unit) {
val builder = AlertDialog.Builder(context)
init(builder)
builder.show()
}
如上代码所示,我们把Builder的创建和show()方法的调用封装到了方法里面,这样重复的代码就只写一次,不用每次都写了。但是感觉还不是很优雅,每次调用方法都要写 “.”,通过apply可以消除 “.” 的调用,如下:
showDialog(this) { builder ->
builder.apply {
setTitle("提示")
setMessage("是否要退出应用?")
setPositiveButton("确定") { _, _ -> exitApp() }
setNegativeButton("取消", null)
}
}
感觉还是不够优雅,每次都要从builder上调用apply,这也是重复代码,能不能优化成如下这样:
showDialog(this) {
setTitle("提示")
setMessage("是否要退出应用?")
setPositiveButton("确定") { _, _ -> exitApp() }
setNegativeButton("取消", null)
}
这就需要用到带接收者的Lambda了,让我们的Lambda接收Builder对象,这样Lambda代码块中默认就有this,这个this对象就是Builder,修改代码如下:
showDialog(this) {
setTitle("提示")
setMessage("是否要退出应用?")
setPositiveButton("确定") { _, _ -> exitApp() }
setNegativeButton("取消", null)
}
fun showDialog(context: Context, init: AlertDialog.Builder.() -> Unit)
val builder = AlertDialog.Builder(context)
init(builder)
builder.show()
}
OK,这样就完美了,对比showDialog中的参数:
init: (AlertDialog.Builder) -> Unit // 带参数的Lambda
init: AlertDialog.Builder.() -> Unit // 带接收者的Lambda
从对比上来看,长得差不多,带接收者就是把括号中的参数移到括号前面,并加了一个 “.” ,带 参数 和带 接收者 两种方式能都能实现把Builder的共同代码封装起来,把Builder的不同部分留给调用者设置,但是明显带接收者的Lambda的方式更优雅,带接收者的Lambda还有一点不同,它可像扩展函数那样调用,如下:
fun showDialog(context: Context, init: AlertDialog.Builder.() -> Unit) {
val builder = AlertDialog.Builder(context)
builder.init()
builder.show()
}
如上代码,init是一个Lambda参数的变量名,但是使用时可以像扩展函数一样在接收者Builder的身上直接调用,所以上面代码再结合apply可以简化成一行,如下:
fun showDialog(context: Context, init: AlertDialog.Builder.() -> Unit) {
AlertDialog.Builder(context).apply(init).show()
}
啊,这实在是太优雅了,读起来也好理解,对于代码:AlertDialog.Builder(context).apply(init).show(),按顺序理解就行了:创建一个Builder对象,并用init这个Lambda对Builder对象进行初始化,最后调用show进行显示。
总结一下:
-
无参数的Lambda,调用Lambda时无需传参数,如下:
val printMessage: () -> Unit = { println("Hello Kotlin") } printMessage() -
带参数的Lambda,调用Lambda时需要传参数,如下:
val printMessage: (String) -> Unit = { message -> println(message) } printMessage("Hello Kotlin")这里的
message ->可以省略不写,不写默认参数名为it -
带接收者的Lambda,调用Lambda时需要传参数,如下:
val printMessage: String.() -> Unit = { println(this) } printMessage("Hello Kotlin")接收者 和 参数 的区别,我个人感觉功能上是一样的,都是给
Lambda使用的一个参数,只不过叫 接收者 的参数有点特别,它在Lambda的代码块中使用this来访问这个参数,而this又是可以省略不写的,基于这个特点,我们不能给带接收者的Lambda起别的参数名,比如下面是错误的,IDE会直接报错:val printMessage: String.() -> Unit = { message -> println(message) }如果这样允许的话,那
this就不见了,这跟带参数的Lambda就没有区别了,那你为何不直接使用带参数的Lambda呢,所以不允许这样写,带接收者的Lambda就只能用this代表参数,无需多此一举,如果你真的有特殊需求即要用到this,又要同时用到另起一个参数名的话,只能这样:val printMessage: String.() -> Unit = { val message = this println(message) println(this) }另外,带接收者的
Lambda在调用时可以像扩展函数那样在接收者对象上调用,但是我们知道它实际上就是给Lambda传递了一个this参数,如:val printMessage: String.() -> Unit = { val message = this println(message) } "Hello Kotlin".printMessage() // 等同于:printMessage("Hello Kotlin")
现代开发中,显示对话框推荐的做法是使用DialogFragment,但是用了这个感觉变得麻烦了许多,还得先创建一个类继承DialogFragment,如果有多种类型的对话框,那是不是就得创建多个类去继承DialogFragment,以实现多种不同形式?这样做是可以的,但是真的很麻烦,能不能像AlertDialog.Builder那样,一个类就通用,那样多好,学会了带接收者的Lambda后,实现这个功能就太简单了,代码如下:
class MyDialog(private val builder: AlertDialog.Builder) : DialogFragment() {
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
return builder.create()
}
companion object {
fun show(context: Context, fragmentManager: FragmentManager, tag: String, init: AlertDialog.Builder.() -> Unit) {
MyDialog(AlertDialog.Builder(context).apply(init)).show(fragmentManager, tag)
}
fun dismiss(fragmentManager: FragmentManager, tag: String) {
(fragmentManager.findFragmentByTag(tag) as? MyDialog)?.dismissAllowingStateLoss()
// dismiss()函数在状态保存期间调用会抛异常,比如屏幕旋转时系统会保存界面UI状态,此时调用dismiss()就会抛出异常,
// 而调用dismissAllowingStateLoss()则不会抛异常。但是你既然都走到要dismiss的
// 地步了,说明不需要对话框了,也不需要系统去恢复了。所以有点搞不懂为什么需要两种取消对话枉的函数
}
}
}
虽然需要声明一个类继承DialogFragment,但是我们只需要声明一次就行,这个类创建对话框时也可以使用AlertDialog.Builder,所以我们通过封装方法把AlertDialog.Builder设置通过带接收者的Lambda交给用户处理即可,使用上也很简单,示例如下:
MyDialog.show(this, supportFragmentManager, "ExitAppTipDialog") {
setTitle("提示")
setMessage("确定要退出App吗?")
setPositiveButton("确定") { dialog, which -> }
setNegativeButton("取消") { dialog, which -> }
}
这里,对于 “确定” 和 “取消” 按钮也是很常用的,而且不管你是点了确定还是取消,点击之后对话框都会自动取消,所以上面代码中的dialog和which参数大多数情况下都是用不到的,但是每次都要写也很麻烦,而且函数名setPositiveButton和setNegativeButton也不好记,常常因为不记得方法名而浪费很多时间去查找方法名,所以我们可以给AlertDialog.Builder增加两个扩展函数来解决这个问题,代码如下:
fun AlertDialog.Builder.setOk(name: String, listener: (() -> Unit)? = null) {
setPositiveButton(name) { _, _ -> listener?.invoke() }
}
fun AlertDialog.Builder.setNo(name: String, listener: (() -> Unit)? = null) {
setNegativeButton(name) { _, _ -> listener?.invoke() }
}
当需要显示对话框时代码就简单多了,如下:
MyDialog.show(this, supportFragmentManager, tag) {
setTitle("提示")
setMessage("确定要退出App吗?")
setOk("确定") { exitApp() }
setNo("取消")
}
还能再优化,比如每次要传supportFragmentManager,又长又占空间,它的获取基本上就是从FragmentActivity或者Fragemnt中获取,那就增加重载函数接收这两个类型即可。
还有每次要传tag,早期我们使用AlertDialog的时候也没用过tag啊,所以tag应该可空,当我们没传tag时自动生成一个。示例代码如下:
class MyDialog(private val builder: AlertDialog.Builder) : DialogFragment() {
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
return builder.create()
}
companion object {
private var count = 0
fun show(activity: FragmentActivity, tag: String? = null, init: AlertDialog.Builder.() -> Unit) {
show(activity, activity.supportFragmentManager, tag, init)
}
fun show(fragment: Fragment, tag: String? = null, init: AlertDialog.Builder.() -> Unit) {
val context = fragment.context ?: return
show(context, fragment.parentFragmentManager, tag, init)
}
fun show(context: Context, fragmentManager: FragmentManager, tag: String? = null, init: AlertDialog.Builder.() -> Unit) {
val dialogTag = tag ?: "DialogTag_${count++}"
MyDialog(AlertDialog.Builder(context).apply(init)).show(fragmentManager, dialogTag)
}
fun dismiss(fragmentManager: FragmentManager, tag: String) {
(fragmentManager.findFragmentByTag(tag) as? MyDialog)?.dismissAllowingStateLoss()
}
}
}
fun AlertDialog.Builder.setOk(name: String, listener: (() -> Unit)? = null) {
setPositiveButton(name) { _, _ -> listener?.invoke() }
}
fun AlertDialog.Builder.setNo(name: String, listener: (() -> Unit)? = null) {
setNegativeButton(name) { _, _ -> listener?.invoke() }
}
调用:
MyDialog.show(this) {
setTitle("提示")
setMessage("确定要退出App吗?")
setOk("确定") { exitApp() }
setNo("取消")
}
Ok,事已至此,已经无可挑剔了!
2025-05-26,还能再优化,既然DialogFragment一般用在FragmentActivity或Fragment,那就可以给这两个对象添加扩展函数,使用起来更简洁,而且Fragment中的parentFragmentManager 有可能抛出异常,所以也要try一下,完整代码如下:
class MyDialog(private val builder: AlertDialog.Builder) : DialogFragment() {
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
return builder.create()
}
companion object {
private var count = 0
fun show(context: Context, fragmentManager: FragmentManager, tag: String? = null, init: AlertDialog.Builder.() -> Unit) {
val dialogTag = tag ?: "DialogTag_${count++}"
MyDialog(AlertDialog.Builder(context).apply(init)).show(fragmentManager, dialogTag)
}
fun dismiss(fragmentManager: FragmentManager, tag: String) {
(fragmentManager.findFragmentByTag(tag) as? MyDialog)?.dismissAllowingStateLoss()
}
}
}
fun AlertDialog.Builder.setOk(name: String, listener: (() -> Unit)? = null) {
setPositiveButton(name) { _, _ -> listener?.invoke() }
}
fun AlertDialog.Builder.setNo(name: String, listener: (() -> Unit)? = null) {
setNegativeButton(name) { _, _ -> listener?.invoke() }
}
fun FragmentActivity.showMyDialog(tag: String? = null, init: AlertDialog.Builder.() -> Unit) {
MyDialog.show(this, this.supportFragmentManager, tag, init)
}
fun Fragment.showMyDialog(tag: String? = null, init: AlertDialog.Builder.() -> Unit) {
val context = this.context ?: return
val fragmentManager = try { this.parentFragmentManager } catch (e: Exception) { return }
MyDialog.show(context, fragmentManager, tag, init)
}
fun FragmentActivity.dismissMyDialog(tag: String) {
MyDialog.dismiss(this.supportFragmentManager, tag)
}
fun Fragment.dismissMyDialog(tag: String) {
val fragmentManager = try { this.parentFragmentManager } catch (e: Exception) { return }
MyDialog.dismiss(fragmentManager, tag)
}
在FragemntActivity或Fragment中要显示对话框时:
showMyDialog {
setTitle("提示")
setMessage("确定要退出App吗?")
setOk("确定") { exitApp() }
setNo("取消")
}
OK,这次真的没得再优化了吧!
2025-09-26:哎,世事无绝对,还是发现了问题,示例如下:
showMyDialog {
setTitle("提示")
setMessage("确定要退出App吗?")
setOk("确定") { exitApp() }
setCancelable(false)
}
这里我们调用了setCancelable(false),目的是为了不让用户取消对话框,比如不能按对话框外的位置来取消对话框,也不能按返回键取消对话框,只能按确定按钮才能取消,但是结果并未生效,这是因为现在的Dialog是封装在Fragment中的,按返回时取消的是Fragment,所以Dialog的禁止取消属性对Fragment是没有作用的,必须是调用Fragment的setCancelable(false)方法来禁止取消Fragment,实现也很简单,我们把Dialog的取消属性设置给Fragment即可,如下:
class MyDialog(private val builder: AlertDialog.Builder) : DialogFragment() {
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val alertDialog = builder.create()
// Dialog本身的setCancelable方法无效,调用DialogFragment的setCancelable方法才有用。
isCancelable = Dialog::class.java.getDeclaredField("mCancelable").apply { isAccessible = true }.getBoolean(alertDialog)
return alertDialog
}
... // 其它代码保持不变
}
2025-10-27:
又发现新问题,对于Dialog::class.java.getDeclaredField("mCancelable")代码,IDE发出红色警告:
Reflective access to mCancelable will throw an exception when targeting API 36 and abov
翻译为:
当针对API 36及以上版本时,对mCancelable的反射访问将抛出异常
所以,要换过思路,增加一个子类,以保存canceable属性,如下:
class DialogBuilder(context: Context) : AlertDialog.Builder(context) {
var cancelable = true
override fun setCancelable(cancelable: Boolean): AlertDialog.Builder? {
this.cancelable = cancelable
return super.setCancelable(cancelable)
}
}
然后使用这个自定义的Builder即可,如下:
class MyDialog(private val builder: DialogBuilder) : DialogFragment() {
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val alertDialog = builder.create()
// Dialog本身的setCancelable方法无效,调用DialogFragment的setCancelable方法才有用。
isCancelable = builder.cancelable
return alertDialog
}
companion object {
fun show(context: Context, fragmentManager: FragmentManager, tag: String? = null, init: DialogBuilder.() -> Unit) {
val dialogTag = tag ?: "DialogTag_${count++}"
MyDialog(DialogBuilder(context).apply(init)).show(fragmentManager, dialogTag)
}
}
}
完整代码如下:
import android.app.Dialog
import android.content.Context
import android.os.Bundle
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import androidx.fragment.app.FragmentManager
class MyDialog(private val builder: DialogBuilder) : DialogFragment() {
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val alertDialog = builder.create()
// Dialog本身的setCancelable方法无效,调用DialogFragment的setCancelable方法才有用。
isCancelable = builder.cancelable
return alertDialog
}
companion object {
private var count = 0
fun show(context: Context, fragmentManager: FragmentManager, tag: String? = null, init: DialogBuilder.() -> Unit) {
val dialogTag = tag ?: "DialogTag_${count++}"
MyDialog(DialogBuilder(context).apply(init)).show(fragmentManager, dialogTag)
}
fun dismiss(fragmentManager: FragmentManager, tag: String) {
(fragmentManager.findFragmentByTag(tag) as? MyDialog)?.dismissAllowingStateLoss()
}
}
}
class DialogBuilder(context: Context) : AlertDialog.Builder(context) {
var cancelable = true
override fun setCancelable(cancelable: Boolean): AlertDialog.Builder? {
this.cancelable = cancelable
return super.setCancelable(cancelable)
}
}
fun AlertDialog.Builder.setOk(name: String, listener: (() -> Unit)? = null) {
setPositiveButton(name) { _, _ -> listener?.invoke() }
}
fun AlertDialog.Builder.setNo(name: String, listener: (() -> Unit)? = null) {
setNegativeButton(name) { _, _ -> listener?.invoke() }
}
fun FragmentActivity.showMyDialog(tag: String? = null, init: DialogBuilder.() -> Unit) {
MyDialog.show(this, this.supportFragmentManager, tag, init)
}
fun Fragment.showMyDialog(tag: String? = null, init: DialogBuilder.() -> Unit) {
val context = this.context ?: return
val fragmentManager = try { this.parentFragmentManager } catch (e: Exception) { return }
MyDialog.show(context, fragmentManager, tag, init)
}
fun FragmentActivity.dismissMyDialog(tag: String) {
MyDialog.dismiss(this.supportFragmentManager, tag)
}
fun Fragment.dismissMyDialog(tag: String) {
val fragmentManager = try { this.parentFragmentManager } catch (e: Exception) { return }
MyDialog.dismiss(fragmentManager, tag)
}
2025年10月31日 今天又发现了Bug:
当对话框显示后,如果旋转屏幕,则系统会保存对话框的args对象,并且在Activity重建后重新创建DialogFragment,使用的是无参构造函数创建的,所以正确的做法应该是创建无参构造函数,并且在这个构造函数中使用args来恢复之前的数据。但是我们的数据是AlertDialog.Builder,这个不知道能不能保存到args中?好像是不行,需要实现序列化接口或者基本数据类型的才可以。
另一个Bug是,当对话框显示后,如果此时把app切到后台,此时Activity会马上调用onSaveInstanceState方法(即使Activity不被系统销毁),这个方法调用后无法调用dialogFragment.show(fragmentManager, dialogTag)方法来显示对话框,会抛出异常,解决办法是使用commitAllowingStateLoss()来显示对话框,它可以在后台运行时还可以正常调用,再次贴一下完整代码:
import android.app.Dialog
import android.content.Context
import android.os.Bundle
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import androidx.fragment.app.FragmentManager
class MyDialog(private val builder: DialogBuilder) : DialogFragment() {
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val alertDialog = builder.create()
// Dialog本身的setCancelable方法无效,调用DialogFragment的setCancelable方法才有用。
isCancelable = builder.cancelable
return alertDialog
}
companion object {
private var count = 0
fun show(context: Context, fragmentManager: FragmentManager, tag: String? = null, init: DialogBuilder.() -> Unit) {
val dialogTag = tag ?: "DialogTag_${count++}"
// 不要用这个方式,当应用被切到后台时,Activity会马上调用onSaveInstanceState方法(即使Activity不被系统销毁),这个方法调用后无法调用下面的方法来显示对话框
// MyDialog(DialogBuilder(context).apply(init)).show(fragmentManager, dialogTag)
// 使用commitAllowingStateLoss(),即使onSaveInstanceState已被调用,也可以显示对话框
val dialog = MyDialog(DialogBuilder(context).apply(init))
fragmentManager
.beginTransaction()
.add(dialog, dialogTag)
.commitAllowingStateLoss()
}
fun dismiss(fragmentManager: FragmentManager, tag: String) {
(fragmentManager.findFragmentByTag(tag) as? MyDialog)?.dismissAllowingStateLoss()
}
}
}
class DialogBuilder(context: Context) : AlertDialog.Builder(context) {
var cancelable = true
override fun setCancelable(cancelable: Boolean): AlertDialog.Builder? {
this.cancelable = cancelable
return super.setCancelable(cancelable)
}
}
fun AlertDialog.Builder.setOk(name: String, listener: (() -> Unit)? = null) {
setPositiveButton(name) { _, _ -> listener?.invoke() }
}
fun AlertDialog.Builder.setNo(name: String, listener: (() -> Unit)? = null) {
setNegativeButton(name) { _, _ -> listener?.invoke() }
}
fun FragmentActivity.showMyDialog(tag: String? = null, init: DialogBuilder.() -> Unit) {
MyDialog.show(this, this.supportFragmentManager, tag, init)
}
fun Fragment.showMyDialog(tag: String? = null, init: DialogBuilder.() -> Unit) {
val context = this.context ?: return
val fragmentManager = try { this.parentFragmentManager } catch (e: Exception) { return }
MyDialog.show(context, fragmentManager, tag, init)
}
fun FragmentActivity.dismissMyDialog(tag: String) {
MyDialog.dismiss(this.supportFragmentManager, tag)
}
fun Fragment.dismissMyDialog(tag: String) {
val fragmentManager = try { this.parentFragmentManager } catch (e: Exception) { return }
MyDialog.dismiss(fragmentManager, tag)
}
火山引擎开发者社区是火山引擎打造的AI技术生态平台,聚焦Agent与大模型开发,提供豆包系列模型(图像/视频/视觉)、智能分析与会话工具,并配套评测集、动手实验室及行业案例库。社区通过技术沙龙、挑战赛等活动促进开发者成长,新用户可领50万Tokens权益,助力构建智能应用。
更多推荐
所有评论(0)