先來看一個具體應用:假設我們有一個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 = thisprintln(message)println(this) }
另外,帶接收者的
Lambda
在調用時可以像擴展函數那樣在接收者對象上調用,但是我們知道它實際上就是給Lambda
傳遞了一個this
參數,如:val printMessage: String.() -> Unit = {val message = thisprintln(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 = 0fun 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 ?: returnshow(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
,那就可以給這兩個對象添加擴展函數,使用起來更簡潔,如下:
class MyDialog(private val builder: AlertDialog.Builder) : DialogFragment() {override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {return builder.create()}companion object {private var count = 0fun 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 ?: returnMyDialog.show(context, this.parentFragmentManager, tag, init)
}
在FragemntActivity
或Fragment
中要顯示對話框時:
showMyDialog {setTitle("提示")setMessage("確定要退出App嗎?")setOk("確定") { exitApp() }setNo("取消")
}
OK,這次真的沒得再優化了吧!