news 2026/10/21 9:33:35

基于DialogFragment的Android通用Dialog封装:链式调用与生命周期安全实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
基于DialogFragment的Android通用Dialog封装:链式调用与生命周期安全实践

1. 为什么要封装通用Dialog

在Android项目里,Dialog是每个应用都绕不开的东西。不管是弹个确认框、展示个广告、填写反馈,还是自定义复杂的筛选面板,Dialog几乎每天都在被使用。但是你有没有发现,随着项目越做越大,Dialog相关的代码变得越来越难维护——同一个样式的对话框在每个页面里都重写一遍,窗口宽高适配因屏幕碎片化而写了一大堆丑陋代码,状态丢失问题反复出现,改一个全局按钮样式要跑到几十个文件里去动刀。

这篇文章就聊聊我这些年做Android通用Dialog封装的经验:把高重复度的对话框封装成一套通用的、链式调用的、可控生命周期的组件。内容适合有一定Android基础、想提升项目可维护性的开发者参考,也适合刚接触Dialog封装的新手做脚手架。

先说几个我亲身踩过的痛点,大家对照一下自己有没有类似的经历。

1.1 原生Dialog开发的四大痛点

第一个痛点是代码重复严重。业务里最常见的就是“提示+确认/取消”型对话框,几乎每个页面都有。很多项目里大家习惯直接在Activity里写AlertDialog.Builder,导致同一个按钮、同一个圆角弹窗背景、同一套文字样式在十几个文件里复制粘贴。后面产品经理说“弹窗圆角从8dp改成12dp”,你就要全局检索替换,开发十分钟,改样式改半天。

第二个痛点是Window参数适配问题。自定义Dialog通常会遇到背景遮罩、宽度、高度、软键盘弹起、防误触这些细节。不同机型上WindowManager.LayoutParams的适配逻辑非常恶心,尤其是全面屏、异形屏、平板横竖屏切换的场景,稍不注意就会出现“弹窗宽度超出屏幕”“圆角变形”“底部导航栏遮挡”之类的问题。

第三个痛点是生命周期和内存泄漏。直接用new Dialog(context)在异步回调里show,Context如果是Activity,在Dialog还没dismiss时页面已经销毁,轻则泄漏,重则窗口异常崩溃。特别是在网络请求回调里show弹窗,崩溃率会非常感人。

第四个痛点是状态恢复。旋屏或者系统回收Activity后,裸写的Dialog不会自动恢复,用户正在填写的输入框内容、选中的选项全部丢失,体验极差。

1.2 封装要解决哪些核心问题

基于上面这些痛点,我在项目里定下了封装目标:一套代码搞定所有弹窗场景,调用方只关心“弹什么”,不关心“怎么弹”。具体要求如下:

  • 统一管理样式和布局,改全局样式只动一处;
  • 通过链式调用快速创建不同形态的Dialog;
  • 生命周期安全,不在页面销毁后回调或弹窗;
  • 正确处理配置变更(旋屏),用户输入不丢失;
  • 支持沉浸式、圆角、遮罩透明度、进出动画等常见效果;
  • 不同业务模块可以扩展自己的内容区域,而不污染公共代码。

这些目标听起来多,但真正动手封装时,最重要的其实就两个决策:用什么载体实现和API怎么设计。接下来我详细拆解这两个关键环节。

2. 封装前的方案选型

选型这一步没做好,后面的代码就会越写越别扭。我在早期项目里吃过亏,所以这里多说几句。

2.1 三种主流实现方案对比

第一类是原生AlertDialog扩展。优点是简单,写起来快;缺点是生命周期依赖外部管理,样式定制能力弱,复杂布局时很不灵活,基本就适合“确定/取消”这种场景。

第二类是WindowManager直接添加View(也就是自定义View往Window里add)。这种方式自由度最高,但是你要自己管Window类型、LayoutParams、触摸事件、焦点、动画、WindowToken回收,代码量非常大,而且坑很多,网上大部分所谓“万能弹窗框架”都是踩平了无数坑才敢这么干。对大多数业务项目来说,可控性和维护成本不划算。

第三类是DialogFragment作为载体。DialogFragment本身是官方推荐的弹窗容器,最大的优势是FragmentManager负责它的生命周期,Activity销毁时Fragment会跟着销毁,不会出现“window leaked”这类崩溃;旋屏时Fragment会重建,你能在onSaveInstanceState里保存状态。同时它本质上还是管理一个Dialog对象,所以自定义布局、动画、Window参数都能做。

我最终选了DialogFragment。理由很朴素:生命周期安全是Dialog封装的第一优先级,而DialogFragment把这块最难啃的骨头交给了系统,剩下样式和业务扩展都是我们熟悉的老本行。至于有人说DialogFragment重、有Fragment嵌套性能问题,在普通业务场景下完全不用担心,系统本身对Fragment有优化,我们只需要注意别把业务逻辑堆在Fragment生命周期里就行。

2.2 封装基类设计思路

选DialogFragment之后,封装思路就清晰了:写一个通用基类BaseDialogFragment,它负责样式、动画、宽度、高度、遮罩、键盘弹出等公共逻辑。业务侧的普通弹窗直接链式调用这个基类即可;遇到特殊场景,就继承基类做一个自定义布局的扩展Fragment,重写布局填充方法。

整个封装分三层:

  1. 基类层:处理所有与业务无关的通用逻辑,包括Window参数、动画、背景、生命周期。
  2. API层:对外暴露链式方法,比如setTitle()、setContentText()、setOnConfirm()。
  3. 业务层:具体的弹窗场景通过API层拼装出来,不写任何样板代码。

这个分层结构的好处是,“基类层”足够稳定,改样式、改动画都在这一层完成;“业务层”新增一个弹窗只需要几行代码,不用关心底层实现。

2.3 配置与依赖准备

这次封装的代码基于androidx.fragment.app.DialogFragment,依赖也很常规:

implementation "androidx.appcompat:appcompat:1.6.1" implementation "androidx.fragment:fragment:1.5.7" implementation "androidx.recyclerview:recyclerview:1.3.0"

不用额外引入第三方弹窗库。这个问题我见很多人纠结过,总想找个开源的万能Dialog库一步到位。我的观点一直是:通用Dialog这种组件,最好自己封装。第三方库虽然省事,但样式定制、特殊需求、问题定位都会受制于人。自己封装一次,好处是永久性的。

3. 通用Dialog的核心设计

下面进入正式设计环节。我会把基类拆成几个核心模块,每个模块讲清楚“是什么”“为什么这么设计”“怎么用”。

3.1 链式调用API设计

先看调用方最终的操作方式,这是所有设计的起点。我希望业务侧使用时是这样的:

CommonDialog.newInstance() .setTitle("提示") .setContent("确定要删除这条记录吗?") .setCancelable(false) .setOnConfirm("删除") { dialog -> deleteData() } .setOnCancel("再想想", null) .show(childFragmentManager, "common_dialog")

这段代码看完,接口的设计方向就明确了:

  • newInstance():因为Fragment需要无参构造,所以我们用一个静态入口创建实例,所有参数通过Fragment的Arguments传递。
  • 每个setXXX方法返回当前实例本身,实现链式调用。
  • 按钮点击回调通过setter传入,内部保存为Fragment字段。
  • show()方法接收两个参数:FragmentManager和Tag,通过Tag可以避免重复弹窗。

链式调用需要注意的一点:不要在Fragment的onCreateView里反复读取Arguments参数。虽然读取几十次性能也不差,但代码会变得很难看。我习惯在onCreateView里一次性把参数取出来放进一个ViewHolder类,后续视图绑定和事件监听都从这个类读取。

3.2 布局与样式统一管理

通用Dialog的布局其实没必要写成一个XML然后动态隐藏控件。我在项目里会在基类里动态创建主View,因为通用弹窗的结构基本固定:背景遮罩层 + 居中内容区 + 标题区 + 内容区 + 按钮区。

常用的做法是:

override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { // 创建根布局,这里用LinearLayout作为容器 val rootLayout = LinearLayout(requireContext()).apply { orientation = LinearLayout.VERTICAL gravity = Gravity.CENTER setPadding(dp(16), dp(16), dp(16), dp(16)) background = createDialogBackground() } // 动态添加标题、内容、按钮区域 ... return rootLayout }

但实际项目里,我更推荐布局XML的写法,只不过只写一份layout_dialog_common.xml,通过include或ViewStub去控制各区域显隐。动态创建View在初期看起来灵活,但后期调整间距、改背景、微调文案排版时,XML的可读性比代码好太多,尤其团队协作时,设计师能直观看到布局结构。

这里分享一个关键样式点:背景圆角和抗锯齿。很多新手直接在shape里写圆角,结果在部分机型上出现边缘锯齿或白线。正确做法是从代码层创建GradientDrawable并设置抗锯齿属性:

private fun createDialogBackground(): Drawable { return GradientDrawable().apply { shape = GradientDrawable.RECTANGLE cornerRadius = dp(12).toFloat() setColor(Color.WHITE) // 关闭硬件加速干扰 antiAlias = true } }

Window背景要设置为透明,否则你的圆角不起作用,整个弹窗会带着黑色或白色方角底块。设置方式在onCreate里:

override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setStyle(STYLE_NO_TITLE, R.style.Theme_AppCompat_Dialog_Alert) }

setStyle的第一个参数最好用STYLE_NO_TITLE,如果你用STYLE_NORMAL,系统会在你自定义布局上再套一层默认title,烦得很。

3.3 数据绑定与事件回调

Dialog的数据绑定,最简单的是通过Argument传值。

companion object { private const val ARG_TITLE = "arg_title" private const val ARG_CONTENT = "arg_content" private const val ARG_CONFIRM_TEXT = "arg_confirm_text" fun newInstance(): CommonDialog { return CommonDialog() } } fun setTitle(title: String): CommonDialog { arguments?.putString(ARG_TITLE, title) return this }

注意一个细节:newInstance()创建实例后,系统可能在onSaveInstanceState之后把Fragment对象状态恢复,你之前传的Arguments还在,但arguments属性可能是null。所以我在所有setter里都做了判空:

fun setTitle(title: String): CommonDialog { if (arguments == null) { arguments = Bundle() } arguments?.putString(ARG_TITLE, title) return this }

事件回调上,我遇到的一个常见坑是:Activity重建后,旧Fragment持有的回调lambda持有旧Activity引用。虽然DialogFragment生命周期短,但如果你把回调直接存成字段,旋屏后Fragment重建时这些字段会变成null。解决方案有两种:一种是把回调也放进Bundle(只适合Serializable,不适合普通lambda);另一种是在onCreateView里重新从Activity拉取监听器。我采用的是第二种,定义一个接口让宿主Activity实现,但这样会让Activity承担太多接口,所以我最终选择了“回调字段 + 判空处理 + 宿主Activity强转接口”的折中方案。真实项目里大家还是要结合业务量权衡。

3.4 生命周期与状态恢复

DialogFragment的生命周期和普通Fragment有些差异,重点要记住:onActivityCreated里已经有了Activity,onCreateView里不建议做耗时操作。我们封装的基类需要在onStart里处理Window参数,因为只有进入started状态,dialog?.window才真正可用。

override fun onStart() { super.onStart() val window = dialog?.window ?: return // 设置宽度为屏幕宽度的85% val width = (resources.displayMetrics.widthPixels * 0.85f).toInt() window.setLayout(width, WindowManager.LayoutParams.WRAP_CONTENT) window.setBackgroundDrawableResource(android.R.color.transparent) // 设置遮罩透明度 window.setDimAmount(0.45f) // 关闭默认动画,由我们自己控制 window.attributes?.windowAnimations = R.style.DialogAnim }

这里有一个很关键的坑:不要使用dialog?.window?.decorView去设置background。因为Dialog的decorView自带系统Padding,直接设置background会看到阴影层的黑边。正确做法是设置Window透明背景,然后内容区的根布局用自己的圆角背景。

关于状态恢复,DialogFragment在onSaveInstanceState之后,如果你有输入框、选择列表等控件,需要在基类里做统一处理。我在基类提供一个钩子方法:

open fun onSaveState(outState: Bundle) { }

子类可以重写它保存自己的业务状态。为了统一,我干脆在CommonDialog里做了文本恢复:

override fun onSaveInstanceState(outState: Bundle) { super.onSaveInstanceState(outState) outState.putString("edit_text_content", etContent.text.toString()) } override fun onViewStateRestored(savedInstanceState: Bundle?) { super.onViewStateRestored(savedInstanceState) savedInstanceState?.getString("edit_text_content")?.let { etContent.setText(it) } }

4. 实操:完整的通用Dialog封装

下面把基类核心代码完整放出,大家可以直接抄到项目里跑起来再改。这段代码我从多个项目里抽出来的通用版本,兼容Android 8到15,目前实测表现稳定。

4.1 基类代码框架

open class BaseDialogFragment : DialogFragment() { protected var mDialog: Dialog? = null // 链式参数 protected var params = DialogParams() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setStyle(STYLE_NO_TITLE, R.style.Theme_AppCompat_Dialog_Alert) } override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) dialog?.window?.let { initWindowParams(it) } } private fun initWindowParams(window: Window) { window.setBackgroundDrawableResource(android.R.color.transparent) window.setDimAmount(params.dimAmount) window.setGravity(params.gravity) window.attributes?.windowAnimations = params.animationStyle if (params.width > 0) { window.setLayout(params.width, params.height) } else { val defaultWidth = (resources.displayMetrics.widthPixels * 0.85f).toInt() window.setLayout(defaultWidth, WindowManager.LayoutParams.WRAP_CONTENT) } // 点击外部可关闭由setCancelable控制 } override fun onDestroyView() { // 解决DialogFragment常见的View泄漏问题 dialog?.setOnDismissListener(null) super.onDestroyView() } fun show(manager: FragmentManager, tag: String) { // 避免重复添加 if (manager.findFragmentByTag(tag) != null) { return } if (!isAdded && !isVisible) { super.show(manager, tag) } } }

把链式调用参数对象单独提出来,后续扩展起来方便:

open class DialogParams { var width: Int = 0 var height: Int = WRAP_CONTENT var dimAmount: Float = 0.45f var gravity: Int = Gravity.CENTER var animationStyle: Int = R.style.DialogAnim var title: String? = null var content: String? = null var cancelable: Boolean = true }

4.2 通用业务弹窗的完整实现

基于上面的基类,写一个CommonDialog,基本能覆盖产品90%的弹窗需求。

class CommonDialog : BaseDialogFragment() { private var onConfirmListener: ((CommonDialog) -> Unit)? = null private var onCancelListener: ((CommonDialog) -> Unit)? = null companion object { @JvmStatic fun newInstance(): CommonDialog = CommonDialog() } fun setTitle(title: String): CommonDialog { params.title = title return this } fun setContent(content: String): CommonDialog { params.content = content return this } fun setDimAmount(amount: Float): CommonDialog { params.dimAmount = amount return this } fun setGravity(gravity: Int): CommonDialog { params.gravity = gravity return this } fun setCancelable(cancelable: Boolean): CommonDialog { params.cancelable = cancelable return this } fun setOnConfirm(listener: (CommonDialog) -> Unit): CommonDialog { this.onConfirmListener = listener return this } fun setOnCancel(listener: (CommonDialog) -> Unit): CommonDialog { this.onCancelListener = listener return this } override fun onCreateView(...): View? { val binding = LayoutDialogCommonBinding.inflate(inflater, container, false) return binding.root } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) val binding = LayoutDialogCommonBinding.bind(view) binding.tvTitle.text = params.title ?: "提示" binding.tvContent.text = params.content ?: "" binding.btnConfirm.setOnClickListener { onConfirmListener?.invoke(this) } binding.btnCancel.setOnClickListener { onCancelListener?.invoke(this) if (params.cancelable) dismiss() } } }

这里要多说一句:dismiss()和dialog?.dismiss()不一样。Fragment有事没事不要直接调dialog.dismiss(),一定要用Fragment.dismiss(),否则Fragment的状态和Dialog的状态会不同步,后面出现Fragment not attached to Activity的崩溃时你都不知道是哪里埋的雷。

4.3 自定义扩展场景演示

通用Dialog解决大部分场景,但产品偶尔会提些奇怪需求,比如“弹窗里要嵌一个进度条”“弹窗里要放一个列表”,这种时候就需要扩展一个新的DialogFragment。

以“列表选择Dialog”为例:

class ListDialogFragment : BaseDialogFragment() { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { return inflater.inflate(R.layout.dialog_list, container, false) } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) val recyclerView = view.findViewById<RecyclerView>(R.id.recycler_view) val items = arguments?.getStringArray("items") ?: emptyArray() recyclerView.layoutManager = LinearLayoutManager(requireContext()) recyclerView.adapter = ListAdapter(items) } companion object { fun newInstance(items: Array<String>): ListDialogFragment { val f = ListDialogFragment() f.arguments = Bundle().apply { putStringArray("items", items) } return f } } }

这种模式下,扩展一个列表弹窗只需要写布局文件、一个Adapter、几行Adapter配置代码。圆角、宽度、动画这些公共逻辑全部继承基类,不会因为某个弹窗特殊就去改公共代码。

5. 真实项目中的踩坑排查

这部分是我最想分享的内容。封装Dialog本身不难,但项目上线后你才会遇到各种真实环境下的怪问题。我挑几个有代表性的列一下。

5.1 高频问题与解决方案速查表

问题现象根因解决方案
Fragment not attached to Activity 崩溃异步回调时Fragment已detach在回调里增加isAdded判断;用ViewLifecycleOwner的协程作用域
弹窗背景有黑色方角Window背景不是透明window.setBackgroundDrawableResource(android.R.color.transparent)
点击返回键无响应设置了setCancelable(false)但没设置返回键拦截在onCreate里设置isCancelable,同时监听setOnKeyListener
弹窗圆角边缘锯齿硬件加速导致抗锯齿失效对drawable设置antiAlias = true,关闭该View的硬件加速
show()后闪一下白屏主题里没设置半透明窗口自定义主题继承Theme.AppCompat.Dialog.Alert并设置windowIsFloating=true
屏幕旋转后输入内容丢失没有保存实例状态重写onSaveInstanceState保存输入内容,恢复时回填
TabLayout中切换页签Dialog仍然显示没有用childFragmentManager用当前页面的childFragmentManager管理DialogFragment
WindowLeaked崩溃在Activity销毁后Dialog没有dismiss全部改DialogFragment,系统自动dismiss

5.2 独家避坑技巧

第一个技巧:show()前务必检查isAdded。

我见过太多人在网络回调里直接dialog.show(),回调到达时Activity可能已经没了。封装时我在show()方法里加了这么一段:

fun show(manager: FragmentManager, tag: String) { if (isAdded || isVisible) { return } if (manager.isDestroyed) { return } manager.findFragmentByTag(tag)?.let { return } super.show(manager, tag) }

这个方法虽然简单,但在多个请求并发回调的场景下能避免80%的弹窗崩溃。

第二个技巧:dismiss后要释放回调引用。

回调持有Activity引用,DialogFragment销毁后回调如果不置空,理论上有泄漏风险。Fragment对象本身被FragmentManager引用,所以释放回调字段很重要。

override fun onDestroy() { onConfirmListener = null onCancelListener = null super.onDestroy() }

第三个技巧:不要滥用沉浸式弹窗。

有些产品喜欢弹窗从状态栏下面开始全屏展示,这时候如果你把Window设置成MATCH_PARENT高度,会出现状态栏背景色和弹窗内容重叠的问题。建议做成两段式:背景遮罩层处理状态栏颜色,内容区正常居中。封装里可以加一个isFullScreen开关,特殊场景再去适配,不要默认全局设置。

第四个技巧:动画样式统一在主题里配置。

如果你在onStart里通过window.attributes?.windowAnimations设置动画,注意在部分定制ROM上属性会不生效。更稳妥的方式是在DialogFragment的Theme里配置:

<style name="DialogTheme" parent="Theme.AppCompat.Dialog.Alert"> <item name="android:windowEnterAnimation">@anim/dialog_in</item> <item name="android:windowExitAnimation">@anim/dialog_out</item> <item name="android:windowIsFloating">true</item> </style>

这样连setStyle都不用写动画样式,系统自动读取主题。

5.3 性能优化与内存治理

DialogFragment的性能问题通常不在于它本身,而在于你在弹窗里放了什么。通用Dialog里如果塞了图片列表、WebView、复用的RecyclerView,就要注意:

  • 弹窗show之后立即加载网络资源,onDismiss时取消,否则会有一段时间的资源白费;
  • 弹窗里不要用runOnUiThread做页面无关的刷新,统一用viewLifecycleOwner.lifecycleScope;
  • 频繁show和dismiss同一个Tag的Dialog,建议复用实例而不是每次newInstance,但要注意Fragment节点会累积,时间长了FragmentManager里一堆废弃节点。需要在onDismiss()里从FragmentManager remove掉自己:
override fun onDismiss(dialog: DialogInterface) { super.onDismiss(dialog) // 从FragmentManager中移除,避免栈堆积 parentFragmentManager.beginTransaction().remove(this).commitAllowingStateLoss() }

这条很容易被忽略,但很有必要,很多项目弹窗弹多了会有诡异的“黑屏闪一下”问题,源头就是Fragment堆积。

6. 封装之外:Dialog与其他弹窗形态的边界

写完核心封装后,我忍不住想多聊一个话题:并非所有弹窗都适合用DialogFragment。

项目里常见的弹窗需求还有底部弹出面板(BottomSheet)、权限引导浮层、Airbnb式的大图弹窗。以BottomSheet为例,官方推荐用BottomSheetDialogFragment,这是DialogFragment的子类,所以如果你基类封得好,可以很自然地把布局替换为BottomSheet样式,而不需要从零写一套新的。

但还有一些“类弹窗”的需求,比如新手引导浮层、按钮点击后的气泡提示,此类使用场景根本不太依赖生命周期,直接写个自定义View + WindowManager即可,没必要硬套DialogFragment。判断一个场景是否适合用DialogFragment的标准,我总结成三条:

  1. 是否需要跟随Activity生命周期销毁——需要就选DialogFragment;
  2. 是否需要旋屏后保留数据——需要就选DialogFragment;
  3. 是否用系统返回键关闭——需要且要统一管理就选DialogFragment。

如果三个答案都是“否”,你可以用更轻量的方式实现,这也是架构上“不引入不必要的复杂度”的原则。

7. 扩展思路:模块化与主题切换

通用Dialog封装是可以继续往模块化方向走的。我在后来一个项目中,把Dialog按功能模块划分成dialog-common、dialog-list、dialog-form、dialog-image-picker等子模块,每个子模块只依赖基类。模块化有个额外好处:不同业务线的UI规范不同,可以实现多套主题包,运行时按项目配置切换。

比如主题化,可以把按钮颜色、文字颜色、圆角大小、遮罩透明度都抽象到DialogThemeProvider接口里,用一个全局Provider在Application启动时设置。这样产品要出一版“青年版”或“会员版”的弹窗风格时,只需要换一套Provider实现,所有弹窗样式自动改变,而不用去动业务代码。

实现也简单,在基类里把创建背景、设置颜色的逻辑委托出去:

interface DialogThemeProvider { fun createBackground(): Drawable fun getTitleColor(): Int fun getContentColor(): Int fun getButtonColor(style: ButtonStyle): Int fun getDimAmount(): Float }

基类默认使用白底黑字风格,业务侧可以全局替换成自定义Provider:

BaseDialogFragment.themeProvider = CustomThemeProvider()

这个设计在C端项目特别有用,一个版本切换品牌色的时候,弹窗不用逐个改。但要注意,Provider一定要在Application初始化时配置好,不要等到弹窗创建时才取,否则异步场景下可能出现样式不一致。

我在实际使用这套封装时,发现编程中最值钱的部分并不是API有多简洁,而是你愿意花时间把公共边界划清楚。Dialog封装的本质不是代码的堆砌,而是“变更隔离”:

  • 样式变了,只改基类;
  • 动画变了,只改主题;
  • 交互变了,只改API方法;
  • 业务变了,只改业务层的setter回调。

把这一点想透,后续项目无论加多少个弹窗,你都不会在重复代码里游荡。

最后再分享一个小技巧:封装完成后,写一个Demo页面把所有Dialog形态列出来,每次改动公共代码后跑一遍Demo,能发现很多“只在某个特定弹窗上触发”的回归问题。我现在就在项目里保留了一个DialogShowcaseActivity,里面集成了通用弹窗、列表弹窗、输入弹窗、图片弹窗十几种形态,回归测试效率提升特别明显。这套封装方案在我参与过的几个中大型项目里都稳定运行,希望对正被Dialog折磨的开发者有点帮助。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/21 9:33:00

智能体技能操作系统:可插拔、可验证、可管理的Agent能力工程化实践

1. 项目概述&#xff1a;这不是一个“技能库”&#xff0c;而是一套可插拔的智能体能力操作系统“agent-skills”这个名称乍看像某个开源项目的子模块&#xff0c;或是某篇论文里被轻描淡写带过的一行术语。但在我过去三年深度参与多个智能体&#xff08;Agent&#xff09;系统…

作者头像 李华
网站建设 2026/10/21 9:32:13

Java字节流

一、概述1.可以直接操作字节信息的流对象2.根据流向&#xff0c;可以分为字节输入流和字节输出流3.顶层抽象父类分别是&#xff1a;InputStream 和 OutputStream4.根据交互设备的不同&#xff0c;有不同的具体子类。往下是案例&#xff0c;案例要求&#xff1a;要求现在d盘文件…

作者头像 李华
网站建设 2026/10/21 9:31:57

Elasticsearch AutoOps 免费开放:从集群诊断到运维闭环的实战解读

1. 免费开放的真正含义&#xff1a;从付费插件到默认能力1.1 几年前我们是怎么排查集群问题的我记得早期维护Elasticsearch集群的时候&#xff0c;最怕的不是查询慢&#xff0c;而是不知道它为什么慢、为什么出问题。当时我们用的是老一套的排查路径&#xff1a;先看监控大盘&a…

作者头像 李华
网站建设 2026/10/21 9:31:37

Selenium模拟登录遇验证码?从截图到打码平台全链路解决

做Web自动化或者写爬虫脚本的兄弟&#xff0c;大概率都撞过同一堵墙&#xff1a;登录页上的验证码。Selenium本身能点击、能输入、能提交表单&#xff0c;但唯独见到验证码就卡壳。这玩意儿存在的意义就是拦机器人&#xff0c;而Selenium在网站眼里就是一个“机器人”。所以想用…

作者头像 李华
网站建设 2026/10/21 9:30:35

AI视频多镜头场景怎么保持一致?状态表、道具接续与异常回归

做多镜头 AI 视频时&#xff0c;一个常见错误是把“画风差不多”当成“场景接得上”。树桩还在原位&#xff0c;人物衣服也差不多&#xff0c;但上一镜头手里拿着的锄头下一镜头突然不见&#xff1b;字幕说“日子一天天过去&#xff0c;田荒了”&#xff0c;画面前景却还长着成…

作者头像 李华
网站建设 2026/10/21 9:30:17

无人机PID调参新思路:非线性增益调度与元启发式优化

做无人机飞控这件事&#xff0c;调PID是个绕不开的环节。我见过不少开发者在初期把一个参数一个参数地试&#xff0c;试到后面自己都不记得哪组组合有效&#xff0c;更麻烦的是&#xff0c;换一个飞行工况可能又得从头再来。某个项目里我接手一台悬停性能很不稳定的四旋翼&…

作者头像 李华