资讯动态

Android自定义Dialog工具类封装:Builder模式与生命周期安全实践

发布时间:2026/10/15 13:30:36 来源:尧图企业网站定制
做 Android 开发几年谈到自定义 Dialog 工具类这个话题我可以很实在地说一句弹窗在代码里是看着简单、写起来最容易失控的东西。你回想一下项目里那些new AlertDialog的代码是不是每个页面都长得不太一样有人用setMessage有人塞自定义布局有人直接在 Activity 里 new 一个Dialog然后getWindow()一顿操作。等页面多了、参与的人多了弹窗样式、交互细节、按钮事件就会越来越散。我后来在项目里做了一次收敛把所有弹窗统一到一个自定义 Dialog 工具类里效果非常明显业务代码少了、视觉统一了、新同学上手弹窗需求也快了很多。这篇文章就是这套工具类的完整复盘从设计思路、核心实现到业务接入、问题排查一步步讲透。提示如果你是刚接触 Android 的新手建议先熟悉Dialog和Window的基础 API再来读封装思路会更有收获。如果你已经在项目里为弹窗维护成本头疼这篇文章可以直接给你一套落地参考。1. 封装之前先想清楚为什么需要一套 Dialog 工具类1.1 三个开发痛点倒逼收敛弹窗这个东西单看一个页面确实简单无非是创建、设置内容、显示、关闭。但放到一个真实项目里不封装的代价会指数级放大。我总结下来最明显的痛点有三个第一是样式不统一。弹窗的背景、圆角、宽度、按钮颜色、进出场动画每个人写出来的都不太一样。有的开发者直接用系统AlertDialog有的自己写 layout最后设计走查时弹窗这块往往是问题最多的。第二是重复代码成本高。一个最简单的确认框从 new Dialog、inflate 布局、findView、绑定监听、点击 dismiss完整写下来十几行起步。项目里有几十个弹窗入口就是几十份高度相似的代码。第三是交互细节混乱。点击外部能不能关、返回键行为怎么样、遮罩透明度多少、动画是缩放还是滑动每个页面体验都不一样。用户可能说不清楚但割裂感是能感受到的。所以工具类的意义不在“炫技”而是把高频重复的逻辑收敛起来让业务侧调用弹窗时像写一句话一样简单同时保证全局视觉和交互的一致性。1.2 为工具类定好设计目标在设计之前我给自己列了几个明确目标避免封装过程中跑偏调用要足够轻。弹窗暴露给业务方的 API 不能超过一屏最好一行show()就能完成。布局和业务要解耦。工具类只负责“布局加载 窗口配置 生命周期”具体控件赋值和点击事件由回调交给业务方。常用参数可配置。布局 ID、宽高比例、位置、动画、取消逻辑、遮罩透明度等高频参数要有公开入口但不应该让调用方每次都写一遍。生命周期安全。Activity 销毁后弹窗不能出现 WindowLeaked这是必须做进封装里的底线能力。这些目标不是一开始就有的是踩了一些坑之后总结出来的。尤其是生命周期安全初版工具类没有做检查线上出现过几次 Activity 销毁后弹窗崩溃的问题后来补上这层逻辑才稳定下来。1.3 模块划分Creator、Binder、Controller我选择的事情顺序是先定模块边界再写具体代码。整个工具类按职责拆成三块模块职责承载形式Creator创建 Dialog配置窗口参数主类 BuilderBinder布局与业务数据的绑定OnBindListener 回调Controller显示、关闭、动画、生命周期调度Dialog 对象 工具方法很多开发者会把 Builder 和 Controller 直接写在主类内部这个没问题。我拆开是出于长期维护考虑后续加手势滑动关闭、加弹窗队列、加配置化渲染时每个模块都只需要在自己的范围内改动。2. 手写 Builder 封装核心类骨架与窗口参数解析2.1 链式写法为什么好用封装自定义 Dialog 工具类第一步是确定 API 的形态。我看到过两种主流做法一种是提供一组静态方法比如DialogUtils.showConfirm(context, title, message, listener)另一种是 Builder 链式调用。我最终选了 Builder 模式。原因很直接弹窗可配置的项实在太多。布局 ID、宽高、位置、动画、遮罩、取消逻辑、监听器如果都塞进静态方法参数列表一个方法七八个参数调用时顺序错了很难排查IDE 提示也不友好。而 Builder 链式写法读起来像配置清单每个方法名说明一项参数写代码的人和理解代码的人都不需要查文档。new DialogCreator.Builder(this) .setLayoutId(R.layout.dialog_confirm) .setWidthRatio(0.8f) .setGravity(Gravity.CENTER) .setCancelable(true) .setCanceledOnTouchOutside(false) .setAnimation(R.style.DialogAnimScale) .setOnBindListener((rootView, dialog) - { TextView tvTitle rootView.findViewById(R.id.tv_title); tvTitle.setText(确定要删除这条记录吗); }) .show();这段代码把弹窗是哪种布局、在哪里显示、点外边能不能关、动画效果是什么、内容是什么全都交代清楚了。这就是 Builder 模式最大的价值可读性。2.2 DialogCreator 核心类骨架核心类的结构不复杂但是细节很多。我这边给出一个可以直接落地的骨架版本public class DialogCreator { public interface OnBindListener { void onBind(View rootView, Dialog dialog); } private Context context; private int layoutId; private float widthRatio; private float heightRatio; private int gravity Gravity.CENTER; private boolean cancelable true; private boolean canceledOnTouchOutside true; private int animStyle; private float dimAmount 0.5f; private boolean dimEnabled true; private OnBindListener onBindListener; private Dialog dialog; private DialogCreator(Builder builder) { this.context builder.context; this.layoutId builder.layoutId; this.widthRatio builder.widthRatio; this.heightRatio builder.heightRatio; this.gravity builder.gravity; this.cancelable builder.cancelable; this.canceledOnTouchOutside builder.canceledOnTouchOutside; this.animStyle builder.animStyle; this.dimAmount builder.dimAmount; this.dimEnabled builder.dimEnabled; this.onBindListener builder.onBindListener; } public void show() { if (context null || layoutId 0) { return; } if (!isContextValid()) { return; } if (dialog ! null dialog.isShowing()) { dialog.dismiss(); } dialog new Dialog(context); dialog.setContentView(layoutId); configWindow(); if (onBindListener ! null) { onBindListener.onBind(dialog.findViewById(android.R.id.content), dialog); } dialog.show(); } public void dismiss() { if (dialog ! null dialog.isShowing()) { dialog.dismiss(); } } private void configWindow() { ... } private boolean isContextValid() { ... } public static class Builder { // 属性基本与主类一致 public Builder(Context context) { this.context context; } public Builder setLayoutId(int layoutId) { this.layoutId layoutId; return this; } public Builder setWidthRatio(float widthRatio) { this.widthRatio widthRatio; return this; } // 省略其他 Builder 方法 public DialogCreator build() { return new DialogCreator(this); } public void show() { build().show(); } } }注意一个细节setContentView之后拿到弹窗根布局的正确姿势是dialog.findViewById(android.R.id.content)不要直接拿layoutId自己 inflate 一遍再传入。android.R.id.content是 Dialog 内容区域的固定 ID官方定义好了这样拿到的 rootView 和 Dialog 内部真正显示的视图是同一个实例后续 onBind 回调里做的所有 findView 都有效。这个点我在代码 review 时看到有人写错过特别提醒一下。2.3 宽高、位置、遮罩透明度的正确设置窗口参数配置是自定义 Dialog 最容易踩坑的部分我逐个讲清楚。宽高方面最忌讳的是硬编码像素值。不同屏幕密度下1080 这个数字代表完全不同的物理尺寸。正确做法是通过WindowManager拿到屏幕宽度乘上比例系数。我平时默认宽度比例 0.75公共方法里可以覆盖private void configWindow() { Window window dialog.getWindow(); if (window null) { return; } window.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT)); if (animStyle ! 0) { window.setWindowAnimations(animStyle); } if (!dimEnabled) { window.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND); } else { window.setDimAmount(dimAmount); } window.setGravity(gravity); WindowManager wm (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); DisplayMetrics dm new DisplayMetrics(); wm.getDefaultDisplay().getRealMetrics(dm); int screenWidth dm.widthPixels; int dialogWidth (int) (screenWidth * widthRatio); int dialogHeight heightRatio 0 ? (int) (dm.heightPixels * heightRatio) : WindowManager.LayoutParams.WRAP_CONTENT; window.setLayout(dialogWidth, dialogHeight); }方法不多但是顺序有讲究。setDimAmount必须在show()之前调用放到 show 之后设置无效这是 WindowManager 内部机制决定的。另外getDefaultDisplay().getWidth()这种老 API 已经废弃不要再用。位置方面居中是最常用的。底部弹出分享面板、操作列表要把 gravity 设为Gravity.BOTTOM配合底部滑入动画。如果希望弹窗出现在屏幕顶部需要额外设置window.getAttributes().y偏移量y 是相对 gravity 方向的偏移正负值方向不一样。这块没有公式可以直接套我的经验是写完真机跑一下很快能调到合适位置。还有一个视觉重点默认 Dialog 窗口背景是一块白色圆角矩形如果你使用自定义布局且布局带了圆角背景窗口背景必须设置为透明否则四个角会出现不协调的颜色。所以上面代码里我把setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT))作为默认行为不让调用方记这个隐藏约定。2.4 Dialog 与 DialogFragment 的取舍封装方案确定时团队里也有人问为什么不用 DialogFragment这个是值得聊一下的。官方推荐 DialogFragment因为它有完整生命周期管理屏幕旋转时状态自动保存配合 FragmentManager 使用也更规范。但我这套工具类的定位是“轻量级通用弹窗层”项目里 80% 以上的弹窗属于临时提示类比如网络加载中、删除确认、活动气泡都是显示几秒就消失的瞬时弹窗。对这类场景用 Dialog 的代码量更少不依赖 FragmentManager不需要考虑 commit 时序直接 show/dismiss 就能完成。当然如果你的弹窗需要在转屏后保留状态或者需要系统帮助管理生命周期那 DialogFragment 方案更合适。这也是我文章最后提到的“另起一条路线”工具类不应该强行覆盖所有场景。封装的底线就是尊重场景差异。3. 进阶能力从通用弹窗到业务级可用基础版工具类能跑通之后你会发现实用度还差一些。比如回调接口怎么设计、按钮事件怎么绑定、动画怎么接入、弹窗多了怎么排队。这几个方面直接决定业务同学愿不愿意用。3.1 回调接口设计要留泛型弹窗不只是展示信息更多时候是要用户做选择、填内容然后把结果交出去。回调接口如果一开始就定死后面扩展会很麻烦。我用的方案是定义泛型回调public interface OnDialogActionListenerT { void onConfirmed(T data); void onCancel(); }T 可以是字符串、一个实体对象、一个集合。比如日期选择弹窗的确认回调传一个LocalDate文件选择传一个File不必为每种弹窗单独声明接口。调用方式new DialogCreator.Builder(this) .setLayoutId(R.layout.dialog_input) .setOnBindListener((root, dialog) - { EditText et root.findViewById(R.id.et_content); root.findViewById(R.id.btn_ok).setOnClickListener(v - { String text et.getText().toString(); listener.onConfirmed(text); dialog.dismiss(); }); }) .show();注意回调里dialog.dismiss()这行不要漏掉否则用户点击确认后弹窗不消失。如果希望用户点击按钮后处理完业务再关闭弹窗可以把 dismiss 放到业务回调里执行灵活性留在调用方手里。3.2 按钮事件的自动绑定每次都在 OnBindListener 里findViewById再setOnClickListener还是显得繁琐尤其很多弹窗就是简单的确认/取消两个按钮。我在工具类里增加了一个快捷方法通过按钮 ID 绑定事件public DialogCreator addButton(int viewId, View.OnClickListener listener) { mButtonMap.put(viewId, listener); return this; }在 show() 内部绑定完布局之后遍历 Map 统一处理if (mButtonMap ! null) { for (Map.EntryInteger, View.OnClickListener entry : mButtonMap.entrySet()) { View btn rootView.findViewById(entry.getKey()); if (btn ! null) { btn.setOnClickListener(entry.getValue()); } } }用 Map 而不是单独定义setOkButton/setCancelButton的原因是不同弹窗的按钮 ID 命名不一定统一有的设计稿里确认按钮叫btn_confirm有的叫tv_sure。直接用 ID 抽象调用方传递什么 ID内部就绑定什么按钮命名差异不会影响封装。3.3 自定义进出场动画的接入方式系统默认的 Dialog 动画偏平淡很多时候产品希望弹窗更有“存在感”。动画的接入思路是在 styles.xml 里定义动画样式然后工具类接收样式 IDstyle nameDialogAnimScale parentandroid:style/Animation.Dialog item nameandroid:windowEnterAnimationanim/dialog_scale_in/item item nameandroid:windowExitAnimationanim/dialog_scale_out/item /styleanim 文件放在 res/anim 目录下缩放进入的动画核心是 scale 和 alpha 叠加?xml version1.0 encodingutf-8? set xmlns:androidhttp://schemas.android.com/apk/res/android scale android:duration200 android:fromXScale0.8 android:fromYScale0.8 android:toXScale1.0 android:toYScale1.0 android:pivotX50% android:pivotY50% / alpha android:duration200 android:fromAlpha0.0 android:toAlpha1.0 / /set这里最容易被忽略的是pivotX50%pivotY50%。不设置时默认缩放轴心在左上角弹窗会从左上角开始变大看起来非常别扭。我调试缩放动画时就是卡在这个细节上一度以为是动画没生效。设置 50% 之后缩放以中心为轴视觉才正常。底部弹出的面板动画则是 translate alpha 组合样式分配给DialogAnimBottom。我提前把三种常见动画缩放、底部滑动、简单渐变定义好业务侧通过一行.setAnimation(R.style.xxx)选用不需要自己写动画文件。3.4 圆角背景、阴影与视觉细节弹窗的视觉细节主要在布局根节点上。很多刚写自定义弹窗的同学布局就是一个朴素 LinearLayout对话框出来四边是直角和设计稿对不上。标准做法是给布局根节点设置圆角 shapeLinearLayout xmlns:androidhttp://schemas.android.com/apk/res/android android:layout_widthmatch_parent android:layout_heightwrap_content android:orientationvertical android:backgrounddrawable/bg_dialog_corner android:padding24dpbg_dialog_corner.xmlshape xmlns:androidhttp://schemas.android.com/apk/res/android corners android:radius14dp / solid android:colorcolor/white / /shape如果希望弹窗有一点层次感可以在布局根节点加android:elevation10dp。注意 elevation 要配合窗口透明背景才有效果如果 Dialog 窗口还是默认不透明背景这层阴影会被边缘遮盖。圆角的大小我一般用 12dp 到 16dp 之间不同设计团队规范不一样最好是定义成资源 dimens方便整体调整。3.5 弹窗队列让多个弹窗按顺序出场真实业务中经常会有两个弹窗连续出现的情况比如首页先弹用户协议确认用户点击同意后接着弹签到提醒。直接连续调用两次 show()系统只会显示后一个前一个会被覆盖掉。我为此加了队列机制工具类内部用一个 ArrayDeque 保存弹窗private static ArrayDequeDialogCreator dialogQueue new ArrayDeque(); private static DialogCreator currentCreator; public static void showQueue(DialogCreator creator) { dialogQueue.addLast(creator); if (currentCreator null) { showNext(); } } private static void showNext() { currentCreator dialogQueue.pollFirst(); if (currentCreator null) { return; } currentCreator.show(); currentCreator.setDialogDismissListener(() - { currentCreator null; showNext(); }); }队列机制看起来多几行代码但省去了业务方自己维护“上一个弹窗关了我再用 show”这类依赖逻辑。需要注意队列入栈后要跟随页面生命周期统一清理避免 Activity 已销毁但队列里还留着弹窗引用。4. 实际落地业务接入与生命周期安全封装完成后最关键的检验是接入业务。我挑几个有代表性的场景把调用方式和硬性注意事项写清楚。4.1 删除确认弹窗的完整调用示例最典型的确认类弹窗接入后代码量很小private void showDeleteConfirm(String itemId) { new DialogCreator.Builder(this) .setLayoutId(R.layout.dialog_confirm_delete) .setWidthRatio(0.8f) .setCancelable(true) .addButton(R.id.btn_cancel, v - { }) .addButton(R.id.btn_confirm, v - deleteItem(itemId)) .setOnBindListener((root, dialog) - { TextView tvTip root.findViewById(R.id.tv_tip); tvTip.setText(确认删除这条记录吗删除后不能恢复。); }) .show(); }整套代码不到十行没有 inflate、没有 getWindow、没有手动 dismiss 逻辑。和以前那种动辄几十行的写法相比业务侧可读性和可维护性都高了一大截。4.2 常用业务场景的 Builder 配置模板我整理了项目里使用频率最高的几类弹窗和对应的配置方式业务场景布局示例关键配置加载中居中转圈 文字不可取消触外不可关闭宽度自适应普通确认标题 内容 按钮居中宽 0.8按钮回调分享面板底部图标列表Gravity.BOTTOM底部滑入动画文本输入EditText 按钮屏幕底部自动弹软键盘活动运营图片 关闭按钮宽 0.9 或全屏手动关闭这套模板我做成示例代码放到了项目文档里新来的同学做弹窗需求时先看模板再动手出错的概率明显降低了。4.3 生命周期检查与 WindowLeaked 拦截生命周期安全是工具类必须守住的底线。Activity 销毁时如果弹窗还在显示系统会抛WindowLeaked崩溃。我在 show() 入口做了强校验private boolean isContextValid() { if (context instanceof Activity) { Activity activity (Activity) context; return !activity.isFinishing() !activity.isDestroyed(); } return true; }context 如果是 ApplicationContext这个 check 会返回 true但前面也提到不推荐用 ApplicationContext 创建弹窗所以工具类里同时做了另一层判断如果传入的 context 不是 Activity 类型直接抛异常提醒开发者。4.4 输入弹窗的软键盘处理输入类弹窗有个很容易被忽略的点软键盘弹出位置。如果不做任何处理键盘把输入框遮住是常见现象。我的处理思路是先把弹窗 gravity 固定到 BOTTOM再在 show() 结束后设置软键盘模式dialog.getWindow().setSoftInputMode( WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE | WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE);设置之后EditText 获得焦点时键盘会顶起弹窗用户能看到正在输入的控件。不同 ROM 对这个模式的处理略有差异但总体行为已经符合预期。如果弹窗一定要居中可以让输入弹窗的布局根节点设置android:windowSoftInputModeadjustResize然后动态监听软键盘高度做偏移。后者会复杂一些建议默认采用底部弹窗方案。4.5 旋转屏幕后弹窗数据恢复方案纯 Dialog 在屏幕旋转后状态不会自动保存。如果弹窗里有用户填了一半的表单或选好的日期不做处理就丢失了。我的方案是 Activity 的onSaveInstanceState里记录弹窗类型和关键数据onCreate恢复时重新构建Override protected void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); if (mDialog ! null mDialog.isShowing()) { outState.putBoolean(dialog_showing, true); outState.putString(dialog_input, getDialogInputText()); } }恢复时根据标记重新 show 并把数据填回去。这个方案没有 DialogFragment 自动恢复那么“无感”但胜在实现独立、不引入 FragmentManager 复杂度。如果产品明确要求弹窗转屏后保持所有状态那这次我会认真建议业务方改用 DialogFragment 路线不要硬走这个轻量封装。5. 高频问题与排查记录一次讲清六个坑这部分是整篇文章里信息密度最高的一节全部来自实际开发和线上反馈。我按问题现象整理每个都给了排查顺序和解决方案。5.1 点击弹窗外部无法关闭排查看几个地方setCanceledOnTouchOutside(true)是否设置。注意和setCancelable是两回事触外关闭属性和返回键关闭是独立控制的。Dialog 窗口背景是否透明。不透明时用户点击弹窗外面的区域系统可能没有正确识别为“外部”触外关闭逻辑不生效。布局根节点是否设置了android:clickabletrue。如果根节点可点击触摸事件会被布局内部消费系统收不到“点击外部”的事件。我自己实践中更推荐的做法是工具类默认触外可关闭在需要强制确认的弹窗比如付费确认、二次删除确认里显式调用.setCanceledOnTouchOutside(false)关闭让不可关闭成为一种显式的用户体验决策而不是默认值。5.2 弹窗出现时页面背景变黑或遮罩太重这个问题通常由两个因素叠加引起FLAG_DIM_BEHIND默认开启加上 dimAmount 值偏大。Dialog 窗口默认会在背后加一层半透明黑色遮罩dimAmount 0.5 表示遮罩透明度 50%如果你看到页面明显发黑可以调小这个值比如 0.2。如果完全不想要遮罩清除 flag 即可dialog.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND);不过正常弹窗场景我很少完全去掉遮罩一点遮罩都没有弹窗和页面会糊在一起视觉层级不清晰。5.3 Dialog 里放 RecyclerView 滑动不流畅弹窗内嵌列表很常见比如选择城市、选择商品规格。默认 Dialog 窗口对触摸事件的处理可能会影响列表滑动表现是列表只有一屏高度滑动无响应或很卡。我的处理方式是dialog.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE);同时要检查列表父布局的高度约束。RecyclerView 外层不要用固定高度包裹数据量不确定的内容底部弹窗里应该让列表自适应剩余高度。还有一个隐蔽坑不要在 Dialog 布局里给 RecyclerView 加android:nestedScrollingEnabledfalse这个属性是为了解决嵌套滚动问题的在弹窗场景下会限制列表滑动。5.4 Context 类型不对导致主题样式丢失这个问题出现频率很高表现很隐蔽。用new Dialog(context)时如果传入的是 ApplicationContextDialog 会采用系统默认主题之前设置的动画、背景透明等都可能不生效。而传入 Activity Context弹窗才能继承 Activity 的主题风格。我在工具类构造函数里加了强制校验if (!(context instanceof Activity)) { throw new IllegalArgumentException(DialogCreator requires Activity context); }这个硬校验是为了从源头避免问题。业务方如果确实没有 Activity Context 可用应该先反思弹窗的调用时机是不是有问题而不是绕过校验。5.5 快速连续点击重复弹出多个弹窗按钮响应里直接调用 show()用户连续点两次就可能出现两个弹窗叠在一起。我在工具类 show() 方法里做了幂等处理if (dialog ! null dialog.isShowing()) { dialog.dismiss(); }简单粗暴地对同实例弹窗做单例化。如果不同页面、不同弹窗之间还需要互斥比如协议弹窗没关不允许弹活动页那要再往上一层维护全局弹窗管理列表新弹窗 show 之前统一 dismiss 已有弹窗。这个我在项目里也做了方式是给 DialogCreator 增加一个静态的当前显示引用show 时检查并清理。5.6 布局加载异常导致空白弹窗有一种少见的脏数据场景layoutId 不存在或者布局在解析时失败Dialog 会显示一片空白。工具类里加一个空判断和调试日志View rootView dialog.findViewById(android.R.id.content); if (rootView null) { Log.e(DialogCreator, rootView is null, check your layoutId); return; }别小看这段日志外用工具类时调试人员拿到报错堆栈会比面对一个空白弹窗更容易定位问题。6. 项目复盘如何进一步优化升级工具类上线运行一段时间后我做了整体复盘有收获也有下一步的思考。6.1 高频弹窗对象复用优化工具类使用频率最高的是加载中弹窗几乎每个网络请求都会创建一次 DialogCreator内部会重复执行 inflate、findView、配置 window。优化方案是全局复用一个加载弹窗实例show 的时候只改文字内容public static void showLoading(Activity activity, String message) { if (sLoadingDialog null) { sLoadingDialog new DialogCreator.Builder(activity) .setLayoutId(R.layout.layout_loading) .build(); } // 更新 message sLoadingDialog.show(); }这样 inflate 只做一次高频场景下的性能和内存表现都有提升。6.2 向小型弹窗组件库演进单靠一个工具类很难覆盖所有弹窗场景比如时间选择、级联选择、权限引导这类复杂弹窗硬塞进这套封装里会破坏 API 的简洁。下一步我打算把工具类升级成一个小型弹窗组件库在核心 Creator 之外额外提供预置布局模板和数据适配器。业务方只需要传数据对象不用关心布局细节把“弹窗”收敛成一种声明式的组件能力。另一个思路是把弹窗配置化。用配置数据描述弹窗的布局、文案、按钮和行为工具类渲染时自动生成视图。运营配置类弹窗用这种方案会很灵活但开发成本也更大属于另一个量级的工程了。6.3 封装核心心得学会做减法整个过程下来我最大的体会是封装工具类难点不在代码而在 API 设计要保持克制。第一版我恨不得把所有参数都暴露成方法结果类膨胀得吓人调用方面对几十个方法反而不知道怎么组合。后来我把高频参数暴露低频参数收敛成默认值或者单独的 Config 对象代码清爽了接受度也高了。另外不要为了封装而封装。弹窗只有一个页面的量级直接在业务里写 Dialog 完全没问题等项目里出现多个页面、多个人同时维护弹窗代码时工具类的价值才真正体现出来。这个判断标准我建议每个开发者在动手写封装之前先想清楚。最后分享一个实际工作中的小习惯每写完一个接口或一个工具类我都会拿一个真实业务场景完整走一遍包括异常分支。这套 DialogCreator 就是在一次次的“真实场景反推”中慢慢完善起来的。如果你也在维护弹窗代码量越来越大的项目不妨从最“烦”的那类弹窗开始收敛先解决痛点再逐步扩充能力这样落地的阻力会小很多。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑