新闻详情

新闻详情

首页 / 资讯中心 / 详情

BaseActivity封装实战:统一生命周期、ViewBinding与权限回调,告别重复代码

发布时间:2026/10/1 16:30:00来源:尧图网络
BaseActivity封装实战:统一生命周期、ViewBinding与权限回调,告别重复代码
接手一个老项目我习惯第一件事先翻Activity。如果每个Activity开头都是几十行的findViewById、setOnClickListener、初始化工匠等待框往后再翻还能看到一模一样的权限回调、一模一样的Toast封装基本可以断定这个项目正在经历“复制粘贴地狱”。BaseActivity这个词在Android圈子里被念叨了很多年说白了就是用一个抽象父类把Activity之间高度重合的公共逻辑收拢起来子类只负责自己的那部分差异。做好这一层基类封装不单是少写代码更是在团队协作里统一规范、减少低级Bug。这篇内容适合刚入行想搭项目骨架的新人也适合被复制粘贴折磨了一两年的中级开发——我会从设计思路讲到完整实现再讲我踩过的坑尽量做到能直接照抄。1. 为什么要封装BaseActivity先解决三个老问题1.1 重复代码像杂草一样蔓延很多人对BaseActivity的第一印象是“省代码”但我更愿意把它当作一种代码治理手段。一个App正常会有三四十个页面登录页、列表页、详情页、个人中心页这些页面看起来各不相同但仔细抽出来看每个页面都会做这几件事拿到上下文、设置布局、初始化状态栏、绑定权限回调、注册和注销EventBus、显示和隐藏Loading。这些事情如果每个Activity各自写一遍带来的不只是行数膨胀而是修Bug的难度直线上升。比如状态栏深浅色图标适配Android 6.0以上才有那个API权限回调在Android 6.0和Android 11的FlAG策略都不一样。如果每个页面各写各的你是没法保证40个页面全部用上了正确API的。把公共逻辑下沉到BaseActivity最大的价值是“只修一次全局生效”。1.2 生命周期与业务逻辑的绑定到底由谁负责Activity本身有一套生命周期而业务逻辑也经常要跟着生命周期走。最常见的例子是网络请求的回调回调时机和页面销毁时机的冲突页面已经finish了网路请求才回来这时如果直接调UI操作轻则闪一下、重则闪退。传统做法是每个页面加一个isFinishing()判断但这种判断散落在每个调用点很容易漏。基类里可以做一件事提供一个统一的、感知生命周期的页面安全回调入口在页面销毁后自动拦截回调。这类问题如果不靠基类统一收口写代码就只能靠个人自觉而“个人自觉”在工作里是最不可靠的。1.3 封装不是银弹什么时候别用基类这个必须泼一盆冷水。并不是所有页面都适合走BaseActivity。如果你的页面使用了非常特殊的逻辑比如异常复杂的WebView容器、游戏引擎承载页、或者极度独立的插件化页面把它们硬塞进基类继承体系里只会逼着你在基类里放一堆if (isSpecialPage)最后基类变成垃圾场。还有一种情况是项目里BaseActivity已经膨胀到几千行子类继承后光看方法列表就得翻半天这时候与其继续加方法不如考虑重构。基类要解决的是“80%页面的公共问题”剩下20%的另类页面不要强行收编。2. 基类的设计边界哪些能力值得下沉2.1 先从页面生命周期里挖公共逻辑设计BaseActivity的第一步不是急着写代码而是把自己项目的页面打开看一遍把所有页面共有的生命周期动作列成一张清单。我自己的项目当时列出来的是这几项设置布局、初始化绑定、创建ViewModel、注册观察者、初始化View、加载数据、注册EventBus、注销EventBus。前三个动作必然每个页面都要做后面几个大约九成页面要做这就可以定出基类的基本骨架把“必然要做”的动作放到基类onCreate里由父类完成把“不一定做但有固定顺序”的动作抽象成子类可重写的方法。这里有个很重要的顺序问题绑定View必须在setContentView之后observe必须在绑定View之后否则一定会出现空指针。顺序这件事放在基类里就保证了子类想乱来都难。2.2 工具类与系统能力的统一入口BaseActivity还应该承载一部分统一入口的职责比如Toast、加载对话框、状态栏、权限请求。你可能会问这些不是可以直接在子类里调用工具类吗确实可以但问题是不同人写的工具类风格可能不一样。有人直接Toast.makeText有人封装了showToast有人喜欢在页面上铺一个自定义Toast布局最后整个项目里的弹窗丑得五花八门。基类提供showToast(String)、showLoading(String)、dismissLoading()这些受控方法子类只管调用底层实现想换就换。这样做的另一个好处是将来升级UI框架、换Theme、改Loading动画时只需要改动基类这一个小地方全项目都能跟着变。2.3 与Jetpack组件的分工ViewModel不是基类的替代品新人容易有一个误区既然有了ViewModel是不是就不用BaseActivity了。这两者解决的是两个维度的问题。ViewModel负责帮你保住数据、处理跨配置变更的状态Activity还是得自己负责“创建View、绑定View、控制页面级别交互”这些事。你可以把BaseActivity理解成一个页面骨架把ViewModel理解成这个骨架上挂数据的格子二者是配合关系而不是替代关系。在封装BaseActivity时我反而建议把ViewModel的获取也放进基类通过getViewModel()模板方法统一返回子类就不用在自己代码里写一遍new ViewModelProvider(this).get()。3. 核心细节逐个拆从布局绑定到权限回调3.1 布局绑定DataBinding和ViewBinding相比findViewById好在哪布局绑定是BaseActivity里最值得认真处理的一环。旧项目里大量出现TextView tvTitle findViewById(R.id.tv_title)这种代码然后一不小心在onCreate里用到了还没初始化的View报空指针。现在新的项目基本都开了ViewBinding或DataBinding。用DataBinding做基类的好处是DataBindingUtil.setContentView(this, layoutId)会返回一个泛型绑定的根节点对象你在基类里把它存成protected VB mBinding子类拿到的就是已经和布局绑定的对象直接通过mBinding.tvTitle访问控件再也没有findViewById这一层。不过要留意一个前提使用DataBinding时必须在布局根节点外面包一层layout标签否则生成的Binding类不存在编译直接报错。ViewBinding则不需要额外的标签只要Android Studio版本够新即可。两者通常可以并行开启但要注意同时开启时布局会同时生成两类Binding命名规则略有差异。3.2 状态栏与标题栏一次配置全App生效状态栏适配是Android开发里一个老生常谈的话题。你需要处理的是状态栏颜色和深浅色图标。简单说深浅色图标这个能力是Android 6.0才有的如果目标版本低于6.0只能接受默认白色图标。加上Android 11以后又有了系统对窗口Insets的管控老一套的window.setStatusBarColor配合SYSTEM_UI_FLAG_LIGHT_STATUS_BAR逐渐被WindowInsetsControllerCompat取代。把这些逻辑放进基类后子类只需要重写getStatusBarColor()和getStatusBarDarkMode()两个方法其他页面不用关心API版本差异。标题栏也一样通过基类统一初始化Toolbar、设置标题、返回按钮如果某个页面不需要标题栏重写hasTitleBar()返回false即可。3.3 权限请求把回调封装成模板方法权限请求是最容易写出重复代码的地方而且Google的API一直在变startActivityForResult已经废弃推荐使用ActivityResultLauncher。如果在每个页面单独写这个Launcher光是注册就够烦还容易在onResume之后才注册导致崩溃。我的做法是在基类里统一注册一个ActivityResultLauncherString[]所有页面共用。子类调用requestPermissionsCompat(android.permission.CAMERA, android.permission.WRITE_EXTERNAL_STORAGE)基类先自己检查一遍已经授权的权限如果全部已授权就立刻回调否则启动Launcher申请。回调方法onPermissionResult(MapString, Boolean result)是基类定义的一个抽象方法子类重写后统一处理结果。这类封装的核心价值在于权限策略如果以后要升级比如Android 13引入了细粒度媒体权限基类里只需要改一行判断所有用到的页面都会跟着用上新策略再也不怕某个页面漏改。3.4 进度框、Toast、防重复点击最容易被忽略的公共能力这类小能力单独看都很简单但做得不好会很影响App质感。进度框我在基类里维护了一个引用防止连续调用showLoading时打开多个对话框。Toast我统一走了ToastUtils并且加了一个队列缓存避免快速连弹时Toast排队时间过长。防重复点击这个更实用基类里维护一个毫秒时间戳暴露isFastClick()方法子类在onClick里先判断一下超过600毫秒才放行专门对付用户连点双击按钮导致重复网络请求的经典Bug。还有一个我强烈推荐放进基类的页面安全回调。基类提供postOnUiThread(Runnable)内部先判断isFinishing()和isDestroyed()再真正runOnUiThread把所有“页面销毁后回调UI”的风险挡在外面。4. 完整实现一个可以直接抄作业的BaseActivity4.1 类定义与抽象方法设计这里直接给一个我目前在用的完整骨架Java版本配DataBinding兼容老项目改造也方便理解思路public abstract class BaseActivityVB extends ViewDataBinding extends AppCompatActivity { protected VB mBinding; protected Context mContext; private long lastClickTime; private ActivityResultLauncherString[] permissionLauncher; private AlertDialog loadingDialog; Override protected void onCreate(Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); mContext this; // 权限Launcher必须在页面可用后注册放最前面 permissionLauncher registerForActivityResult( new ActivityResultContracts.RequestMultiplePermissions(), this::onPermissionResult); if (getLayoutId() ! 0) { mBinding DataBindingUtil.setContentView(this, getLayoutId()); // 关键Binding需要感知生命周期否则LiveData观察会异常 mBinding.setLifecycleOwner(this); } initStatusBar(); initTitleBar(); initView(); initObserver(); initData(); initListener(); } protected abstract int getLayoutId(); protected abstract void initView(); protected void initObserver() { } protected void initData() { } protected void initListener() { } protected void initTitleBar() { } protected void onPermissionResult(MapString, Boolean result) { } protected boolean getStatusBarDarkMode() { return true; } protected int getStatusBarColor() { return R.color.white; } }几个关键点先解释一下。onCreate的执行顺序是硬编码死的注册权限Launcher、绑定布局、设置状态栏、初始化View、注册观察者、加载数据、绑定监听。子类在initView里想用mBinding时布局已经绑定成功在initObserver里想observe某个LiveData时View已经初始化完毕。这个顺序如果乱掉运行期很容易空指针。带泛型的写法有个好处子类声明成extends BaseActivityActivityMainBinding之后mBinding自动就是ActivityMainBindingIDE补全能直接带出字段名不用强转。这时DataBindingUtil.setContentView返回的是ActivityMainBinding实例如果getLayoutId()被某个子类写错、和声明的Binding不匹配运行时通常会抛出ClassCastException所以子类里getLayoutId()建议直接返回对应布局资源不要加别的判断逻辑。4.2 注册与解绑EventBus、RxJava、Lifecycle兜底如果你的项目还在用EventBus基类里可以做一个开关式封装Override protected void onDestroy() { if (useEventBus()) { EventBus.getDefault().unregister(this); } if (mBinding ! null) { mBinding.unbind(); } if (loadingDialog ! null loadingDialog.isShowing()) { loadingDialog.dismiss(); } super.onDestroy(); } protected boolean useEventBus() { return false; }这里有个细节注册和解绑必须成对出现。很多内存泄漏就是只注册不注销或者注销时机太晚。我习惯在onCreate里根据useEventBus()决定是否注册在onDestroy里无条件注销生命周期完全对称。mBinding.unbind()这一步可以及时释放Binding里持有的View引用减少泄漏概率尤其是页面里还有大量图片和自定义View时效果能感觉到差异。如果项目用了RxJavaBaseActivity也可以统一添加CompositeDisposable在onDestroy里clear()。这比每个页面自己维护一个接收器要干净得多。不过现在新项目大多转向协程和Flow协程的viewModelScope天然和生命周期解耦那这一项就可以不做了。4.3 页面状态管理加载、空态、出错一次做全App里最常见的场景是进入页面先显示加载中网络回来后要么显示内容、要么显示空态、要么弹错误提示。这东西在每个页面写一遍真的很痛苦我建议在BaseActivity里内置一个顶层的状态切换机制。具体做法是布局里预留一个FrameLayout作为页面根容器BaseActivity提供showContent()、showEmpty()、showLoading()、showError(String)四个方法内部切换不同View的显示隐藏。子类初始化时只需要调用showLoading()数据回来后调用对应方法切换状态。因为是基类统一管理空态和出错的图标文案也可以由基类统一配置页面数量多了以后特别省事。如果某个页面不想使用这四个方法重写一个enableStateLayout()返回false回退到每个页面自己控制UI的方式即可。这一层封装会让Activity的代码结构往前走一大步用户体验也更统一。4.4 给子类留出足够的扩展点封装基类最忌讳的事情就是“父类管得太死”。子类可能需要在onCreate最前面更新一些变量也可能需要在布局绑定后做一些特殊处理所以我在基类里留了几个可重写的钩子方法严格来说都是空实现子类按需覆盖。同时在onCreate一开始还有一个小方法initParams(Intent intent)用于处理从Intent里取参数。基类在绑定布局前会调用它子类就能先拿到参数再初始化页面避免在initView里还临时读Intent的尴尬。这种“模板方法钩子方法”的组合本质上是把流程固定化、把变化点显性化。子类只需要知道“我该重写谁”不用关心“系统什么时候会调我”心智负担会小很多。5. 常见问题与排查技巧实录5.1 ViewBinding在include场景下符号冲突怎么解BaseActivity用ViewBinding后最常遇到的坑是include布局。如果主布局里写了include layoutlayout/layout_empty /而且你没有给include设置android:id那么Binding类里可能不会生成对应的EmptyLayout字段就会出现“明明布局已经include了代码里却找不到对象”的诡异情况。解决办法是给include加一个android:id比如android:idid/emptyLayout重新编译后就能通过binding.emptyLayout访问到那个子布局根View。如果是DataBinding并且子布局里用了表达式还需要在include对应的根布局里写一个data标签接收外部传入的Binding变量这属于DataBinding的进阶用法刚上手的人很容易被这里卡住。5.2 基类持有Activity导致的泄漏问题基类本身也是一个Activity它持有Context是正常的怕的是外部类和回调偷偷持有基类对象。最常见的泄漏场景是这样的子类里起了一个Handler往主线程投递延迟消息消息里带了Activity引用页面已经销毁消息还在队列里。BaseActivity能帮上忙的做法是在onDestroy里面统一移除所有Callback并且提供一个safeRunOnUiThread方法让子类发消息时走基类方法基类在真正分发前先判断isFinishing或isDestroyed。另外如果你在基类里给第三方SDK注册了全局回调一定要在onDestroy里清理别只想着页面打开时的注册。用LeakCanary跑一遍全项目基本能把这类问题暴露出来。5.3 onBackPressed 与 Android 13 返回手势的适配旧的onBackPressed()方法在新版本里已经不再推荐Android 13开始启用预测性返回手势。基类如果还固守旧写法在预期返回动画上会出现不协调的体验。我在基类里会统一注册OnBackPressedCallbackgetOnBackPressedDispatcher().addCallback(this, new OnBackPressedCallback(true) { Override public void handleOnBackPressed() { if (!onBeforeBackPressed()) { setEnabled(false); getOnBackPressedDispatcher().onBackPressed(); setEnabled(true); } } });子类只需要重写onBeforeBackPressed()返回true表示消费掉这次返回、不退出页面返回false则执行默认返回行为。好处是把返回处理的入口统一了以后再适配预测性返回动画基类里面改一处就行。5.4 基类越写越臃肿怎么拆出去BaseActivity用一段时间后会不由自主地越来越胖。你今天觉得“既然大家都要用就塞进基类”明天又觉得“这个功能八成页面用得到也塞进去吧”。基类最终变得比普通Activity还复杂。我的习惯是给BaseActivity设一条线只放“所有页面都用得上”的东西90%页面用得上但并非全量的能力单独拆成一个工具类或者一个组合类再通过基类给子类暴露一个便捷入口。拿状态管理来说基础状态切换可以放基类但网络请求失败的重试策略、分页加载逻辑这些就应该抽成独立的控制器或StateMachine基类只负责初始化它。这样即使将来某个页面不走BaseActivity也能很方便地组合这一套能力。5.5 一段时间高频踩坑速查表问题现象大概率原因排查方向mBinding为 null在setContentView之前访问了mBinding检查是否在基类的initView之前做了绑定操作include 布局找不到子 Viewinclude 没加android:id给 include 补 id 后重新编译权限授权成功但回调没执行Launcher 在onCreate之前被调用确认permissionLauncher.launch在注册之后调用EventBus 收不到消息注册和注销顺序不对称或注销过早基类统一管理注册注销保持成对页面销毁后 Toast 仍然弹出子类直接调用 Toast 工具未做页面安全判断走基类showToast统一做生命周期检查返回时闪一下白色状态栏状态栏深浅色切换时机不对用WindowInsetsControllerCompat统一适配泛型 Binding 强转异常getLayoutId()返回的布局和声明的 VB 不匹配核对布局资源 id5.6 我保留的几个封装习惯回头总结这几年的经验我觉得BaseActivity封装最值得记住的一点是“克制”。好的基类不是塞满方法让子类调而是像一张路线图规定好每个页面该走的流程同时不堵塞子类的车道。我每次新建一个Activity都会先问自己一句这个Activity有没有用上基类的大部分能力如果只用了其中一两个我会重新考虑这个页面是不是真的需要继承BaseActivity。最后再分享一个小习惯BaseActivity里每个抽象方法都写上注释说明“这个方法什么时候会被调用、子类需不需要调用super”因为团队里总会有人继承你的基类一份清楚的注释比把基类写成文档还管用。
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

AI工程从零到上线:构建稳定可交付的LLM应用 2026/10/1 17:07:53

AI工程从零到上线:构建稳定可交付的LLM应用

我给自己定过一条规矩:每进入一个新领域,都要强迫自己整理一份"能从零讲起"的笔记。这条规矩最终催生了一个叫 ai-engineering-from-scratch 的系列项目——它不研究怎么训练大模型,也不逼你从推导 Transformer 结构开始&#xff0…

阅读更多 →
学习笔记 techfile 2026/10/1 17:07:53

学习笔记 techfile

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
代码知识图谱选型:Codebase Memory MCP部署验证与初评 2026/10/1 17:07:53

代码知识图谱选型:Codebase Memory MCP部署验证与初评

本文属于「AI代码治理与团队规范」系列。代码知识图谱是 AI 辅助编码从文本理解走向结构理解的重要技术方向。2026 年上半年该赛道工具密集涌现,本文选取 Codebase Memory MCP 进行部署级验证,从部署门槛、架构设计、功能覆盖三个维度做初步评估&#xf…

阅读更多 →
大模型预训练数据集构建全指南:从选型清洗到配比落地 2026/10/1 17:07:53

大模型预训练数据集构建全指南:从选型清洗到配比落地

做预训练这几年,我最深的体会是:模型架构大家都能抄,训练技巧论文里也写得很明白,但大模型预训练数据集构建这件事,很少有一篇文章能把里面的坑和细节讲透。我见过太多团队把精力花在调结构、琢磨学习率上,…

阅读更多 →
策略梯度完全指南:从核心原理到PPO等主流算法实战 2026/10/1 17:07:53

策略梯度完全指南:从核心原理到PPO等主流算法实战

策略梯度这个方向,我前前后后啃了快六年的强化学习,赌上头发跟你保证:它是整个深度强化学习里最绕、但也最值得弄懂的一根主线。网上讲策略梯度的文章多如牛毛,但要么只丢一堆公式让你自己悟,要么就贴一段代码让你跑完…

阅读更多 →
现代C++职责链模式实战:从基础链表到可排序异步过滤链 2026/10/1 17:07:47

现代C++职责链模式实战:从基础链表到可排序异步过滤链

写C的时间长了,你会发现设计模式这个工具箱里,有的模式在C里用起来特别顺手,有的却非常别扭。职责链模式属于前者,但前提是你得知道怎么用。Java和C#里,职责链通常是一串对象指针,顺着链走下去,…

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞 ✉