☰
AndroidProject(安卓技术中台)技术选型全解析:31 个“为什么“背后的架构取舍实录
2026/10/15 14:54:25 网站建设 项目流程
  • 移动开发
  • 示例工程
  • UI组件

【免费下载链接】AndroidProject

Android 技术中台,但愿人长久,搬砖不再有

项目地址:https://gitcode.com/gh_mirrors/an/AndroidProject
点击查看免费下载

本文以仓库根目录下的 HelpDoc.md("常见问题解答"文档)为骨架,系统梳理 AndroidProject 在七年多演进中做出的 31 个关键技术选型决策:为什么放弃 MVP、ButterKnife、DialogFragment、Retrofit、EventBus、组件化,为什么坚持findViewById、原生 WebView、sp 字体单位与 smallestWidth 屏幕适配,并逐一对照当前仓库源码给出可验证的实现证据。读完本文,你将理解"技术中台"在开发效率与后续维护之间的权衡方法,也能在自建 Android 架构时直接复用文中总结的取舍准则与代码范式。

一、代码组织哲学:为什么不用 MVP / MVVM

文档开篇给出的第一个答案是关于代码组织模式的。AndroidProject 舍弃 MVP 的最大原因是"需要写各种类、各种回调":当页面本身比较简单时,引入 MVP 反而会把原本简单的代码变复杂,导致后续开发和维护成本升高,前期付出的代价与后期维护的收益不成正比。文档作者的观点很直接:MVC、MVP、MVVM 这三种模式出现的本质目的都是"给代码做分类",解决"代码多且乱"的问题,但三种模式"都不够灵活、很生硬,像是一套套规则"。

文档给出的替代方案是:更推荐直接将代码写在 Activity 中,但前提是做好代码封装和重复代码抽取,尽量让 Activity 成为只有业务代码的类,这样大多数 Activity 的代码量能控制在 1000 行以内。

这一点在当前仓库的代码结构中可以得到印证:library/base/src/main/java/com/hjq/base/BaseActivity.java 把大量通用能力以Action接口的形式抽取到了 core 库中(ActivityAction、ClickAction、HandlerAction、BundleAction、KeyboardAction、FixOrientationAction),Activity 自身只保留getLayoutId()、initView()、initData()三个抽象钩子,业务页面继承后只需要填充真正的业务逻辑,这正是"Activity 只留业务代码"思想的实现载体。同时,仓库中的示例页面(如 HomeActivity.java、LoginActivity.java)均未引入任何 MVP/MVVM 分层文件,全部采用"基类封装 + 业务直写"的范式。

二、控件绑定:为什么不用 ButterKnife、ViewBinding、DataBinding

这一组问答集中体现了文档作者对"自动生成绑定类"这一类方案的警惕。

2.1 ButterKnife 的三个问题

  1. OnClick注解偶发不响应点击:文档记录了一个不是必现的 Bug;
  2. BindView并不能简化代码:使用BindView和findViewById的代码量是一样的,只是把 View 与 ID 的关系在视觉上变得更明显;
  3. 最大缺点是会自动生成 ViewBinding 类:哪怕类中只使用了一个BindView,也会生成该类。

此外,Android Studio 升级到 4.1 之后,由于 Gradle 5.0 之后 View ID 不再以常量形式存在,会弹出如下警告:

Resource IDs will be non-final in Android AndroidProject Gradle Plugin version 5.0, avoid using them as annotation Resource IDs will be non-final in Android AndroidProject Gradle Plugin version 5.0, avoid using them in switch case statements

因此新版 AndroidProject 移除了 ButterKnife,文档的结论是:"其实 findViewById 一直挺好,只是我们没有认真思考过而已。"

2.2 ViewBinding 的"硬伤"

文档指出 ViewBinding 与 ButterKnife 有同样的毛病——自动生成一个类,且每次生成的类名不固定,导致无法在基类中封装处理:

ActivityXxxxBinding.inflate binding = ActivityXxxxBinding.inflate(getLayoutInflater()); binding.tv_data_name.setText("字符串");

更致命的是命名规范问题:控件 id 若按 XML 规范命名,生成后在 Java 代码中的属性名就不符合 Java 命名规范;反之亦然。文档直言这些是"硬伤"。

2.3 DataBinding 的"优点即缺点"

DataBinding 最大的优势是"在 XML 中直接给 View 赋值",但它的优点正是最致命的缺点:一旦判断条件复杂起来,由于 XML 属性不能换行,会导致无法直接赋值,或者一大段逻辑堆在布局中,让 CodeReview 异常艰难。再加上学习成本高、每次都要添加layout和data节点、无法在基类中封装:

ActivityXxxxBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_xxxx);

三者的共同问题(每次都要写binding.xxx、类名不固定、无法基类化)让文档最终选择了原生findViewById。这一决策在仓库中有清晰的体现:所有示例页面(如 login_activity.xml、home_main_fragment.xml)均为普通布局,没有任何layout/data包裹,Java 侧全部通过findViewById获取控件。

三、布局与字体单位:ConstraintLayout 与 sp/dp 之辩

3.1 为什么布局几乎不用 ConstraintLayout

文档承认约束布局的优点是"没有布局嵌套、能减少测量次数、提升绘制速度",但优点同时也是缺点:正是因为没有了层级概念,每个 View 都要定义 ID 来约束相邻 View 的位置。具体弊端有四条:

  1. 每个 ViewID 都要想好名称,既要符合规范又要体现作用;
  2. 要考虑好每个 View 上下左右的约束关系,否则容易越界——例如一个 TextView 设计图上写 5 个字、后台返回 10 个字,控件宽度被拉长却没有设置右侧约束,就可能遮盖右边 View;
  3. View 之间的关系变复杂:增删任何一个 View 都可能牵动整个布局的约束关系,预览也会错乱,"拆一块瓦很可能导致房倒屋塌";
  4. 因为每个 View 几乎都定义了 ID,无法直接在布局中预判某个 View 在 Java 代码中是否被使用。

文档给出的实践建议是:大多数页面优先结合 LinearLayout 与 FrameLayout,嵌套控制在 2~3 层,超过 5 层再考虑 ConstraintLayout。对当前仓库所有布局文件(app/src/main/res/layout 与 res-common/layout)检索ConstraintLayout,结果为空,与文档结论完全一致。

3.2 字体为什么必须用 sp 而不是 dp

文档首先回顾了谷歌原生的写法:控件大小用 dp,字体大小用 sp,两者最终都会转成 px。区别在于换算基准:dp 按屏幕密度计算,sp 按手机系统设置的字体大小计算。如果用 dp 代替 sp,无论用户在系统里怎么调大字体,App 内的字体都不会变化——这对年轻人没影响,却会伤害将系统字体调大的老龄用户。文档的结论很朴素:"换位思考,我们终有一天也会变老"。

同时文档指出:网上用 dp 替代 sp 的方案,本质是为了解决"控件宽高写死导致某些字体显示不全"的问题,而把控件宽高写死本身就是不合理的,应在不得已时才写死,正常情况应让控件自适应测量。

四、屏幕适配:谷歌方案、今日头条方案与 smallestWidth 通配符方案

4.1 谷歌默认适配机制的换算基准

文档先解释了 dp 的计算本质:系统只认 px,dp 需要通过"基数"换算:

ldpi:1dp=0.75px mdpi:1dp=1px hdpi:1dp=1.5px xhdpi:1dp=2px xxhdpi:1dp=3px xxxhdpi:1dp=4px

厂商也可以修改默认基数以达到最优显示效果。但谷歌方案的设计理念是"屏幕越大显示的东西越多",这无法满足"希望根据屏幕大小对控件进行百分比压缩"的 App 需求——尤其是平板场景:手机屏幕多在 5~6 寸、平板多在 8~10 寸,只适配手机时谷歌方案是最优的;而适配平板时若不做百分比压缩,控件在平板上会被放大、屏幕剩余空间过大,显示效果反而变小。

4.2 两种第三方方案的实测体验

文档作者分享了自己的亲身经历:在一家做平板应用的公司中使用过 AutoSize 框架一年多,发现其"偶尔出现机型适配失效"等较多 Bug,重写getResources后有所减少但仍有奇怪问题;换到另一家做平板应用的公司后改用通配符适配方案,没有了那些奇怪问题,但代码侵入性比较高。文档总结:两种方案各有优缺点,按需选择。

4.3 AndroidProject 的最终选择:smallestWidth 通配符方案

文档记录,通过群投票(多数人更认同百分比适配方案),AndroidProject 在 v13.0 版本加入了通配符适配方案。当前仓库中的落地实现是独立的smallestWidth库:

  • 资源层:library/smallestWidth/src/main/res 下同时提供values基准目录与values-sw240dp、values-sw320dp、values-sw360dp、values-sw480dp、values-sw600dp、values-sw720dp、values-sw820dp、values-sw960dp等 22 个最小宽度限定目录,每个目录下都定义dimens_dp.xml与dimens_sp.xml;
  • 具体资源示例:library/smallestWidth/src/main/res/values/dimens_dp.xml 中定义了从dp_0_1、dp_1、dp_2直到dp_360的整套 dp 尺寸资源,以及负值(dp_m_1、dp_m_60等)供布局直接引用;
  • 代码层:library/smallestWidth/src/main/java/com/hjq/smallest/width/SmallestWidthAdaptation.java 提供了dp2px、px2dp、sp2px、px2sp四个方法族,分别接收View、Fragment、Context、Resources四种参数入口,底层通过resources.getDimension(R.dimen.dp_1) * value完成换算。

这套方案的本质正是文档所述"通配符/百分比"思路的工程化:以 dp 资源值为锚点,让不同最小宽度设备拿到按比例缩放后的同一套尺寸,从而在保持布局语义不变的前提下控制不同屏幕下的显示效果。

五、页面导航与模块化:单 Activity 多 Fragment、组件化与包名划分

5.1 为什么不用单 Activity 多 Fragment

文档承认"新鲜事物总能勾起人的好奇",但先抛出一个问题:这种模式和写多个 Activity 相比,真正的优点是什么?"省去在清单文件注册一句代码"并不构成价值——文档明确说:"我已经写了那么多句代码,不差那句在清单文件注册的代码。"它列举了该模式的三大弊端:

  1. 屏幕形态切换复杂:有的页面全屏、有的非全屏,有的竖屏、有的横屏,进入时怎么切换、返回时怎么切回、又怎么统一封装;
  2. Fragment 之间通讯困难:Activity 有onActivityResult可用,Fragment 只能依赖 EventBus 之类的方案,每次都要写一个 Event 类,查找不便且低效;
  3. 系统回收重建风险:Activity 被系统回收重建后,多个 Fragment 的回退栈是否正常?一个 Activity 里若有 10 个 Fragment,重建时一次性创建 10 个 Fragment 对内存和性能的冲击如何?

结论是:如果这种模式不能创造足够大的价值,就不值得做,因为"最终得到的永远抵不上付出的"。

5.2 为什么不用组件化

文档对组件化的剖析很全面:优点是"业务代码隔离、完全解耦、提升编译打包速度",但这两个优点只有在业务规模大且复杂时才产生价值。反面问题包括:

  • 解耦过程本身麻烦,会加大前期开发工作量;
  • 解耦可能导致代码重复,例如AndroidManifest清单文件要同时维护两份,后期改动要改两遍;
  • 路由跳转大多依赖 APT 生成映射表,每写一个 Activity 都要写路径注解,写起来麻烦、管理起来更麻烦;
  • 各业务模块的 SDK 初始化、数据存储交互如何解耦,都是组件化悬而未决的问题。

文档的判断是:组件化是好思想,但不适用于中小型项目——中小项目大部分业务模块都很小,做组件化弊大于利;大型项目模块多、单次打包可能半小时以上,才值得投入人力技术去做。AndroidProject 面对的是大众开发者,更倾向中小型项目的代码设计。这一取舍在当前仓库结构上清晰可见:app/src/main/java/com/hjq/demo 采用单模块(app)加少量公共库(library/base、library/core、library/customWidget、library/smallestWidth、library/umengSdk)的组合,业务全部收敛在 app 模块内,没有按业务拆分的独立 module。

5.3 为什么不按业务划分包名

文档的理由是:有些业务职责不明确,无法限定属于哪个业务模块,且大多数模块的类都很少,只有少数模块有一定数量的类。所以在中小项目中,更推荐按"类的作用"来划分包名。仓库 app/src/main/java/com/hjq/demo 下的action、aop、http、manager、other、permission、ui/activity、ui/adapter、ui/dialog、ui/fragment、ui/popup、widget等目录正是"按作用划分"的典型实现。

5.4 为什么没有列表多 type 的封装

文档指出:原生RecyclerView.Adapter本身就支持多 type,只需要重写适配器的getItemType方法即可,因此无需额外封装。仓库中 NavigationAdapter.java、StatusAdapter.java 等均直接基于RecyclerView.Adapter实现,未引入任何多 type 框架。

六、界面侧滑功能的两次引入与最终移除

这是一个"曾经拥有、两次尝试、最终放弃"的完整案例。文档记录了完整时间线:

  • v9.0 之前:曾集成第三方侧滑框架 BGASwipeBackLayout,但该框架在 Android 9.0 上会闪屏,且100% 必现、用户体验极差,作者反馈后不了了之,于是在 v9.0 版本移除;
  • v10.0:改用 SmartSwipe 重新加入侧滑,但该框架在Android 11 上,若 Activity 上有 WindowManager 正在显示再使用侧滑,会出现闪屏,具体效果如下图;
  • v12.1:由于作者三天未回复、GitHub issue 基本无人应答、最后一次提交已过去 13 个月,项目明显处于停更状态,最终在 v12.1 版本再次移除界面侧滑功能。

这个案例的启示在于:选型第三方库时,维护活跃度与 issue 响应速度是重要的决策依据,一次 Bug 可能是运气问题,两次踩坑加上项目停更就是明确的风险信号。从当前仓库 app/src/main/java/com/hjq/demo 的代码看,已无任何侧滑框架相关的封装类,与文档结论一致。

七、Dialog 生命周期:为什么放弃 DialogFragment

7.1 DialogFragment 的三个坑

DialogFragment 出现的初衷是解决 Dialog 与 Activity 生命周期不同步导致的内存泄漏,AndroidProject 曾引入并迭代多个版本,但在 v10.0 后移除。文档逐一记录了踩过的坑:

  1. 监听器被占用:DialogFragment 会占用 Dialog 的 Cancel/Dismiss 监听(用于在 Dialog 消失后把自己从 Activity 移除),导致开发者原先设置的监听被覆盖。AndroidProject 前期的解法是把 Dialog 监听器从"一对一"改造成"一对多"观察者模式——一个被观察者可以有多个观察者;
  2. 后台 show/dismiss 崩溃:show和dismiss内部使用FragmentTransaction.commit,该方法会检查 Activity 状态,若已调用onSaveInstanceState则抛出java.lang.IllegalStateException: Can not perform this action after onSaveInstanceState。典型场景是:网络请求尚未结束用户切到后台,异步回调回来后再 show/dismiss DialogFragment 即触发异常。前期解法是重写show加 Activity 状态判断、dismiss改用dismissAllowingStateLoss(该方法不检查 Activity 状态),虽然避免了崩溃但会导致 Dialog 显示不出来;
  3. 屏幕旋转空指针:外层传入的 Dialog 对象在旋转重建时被回收置空,onCreateDialog再次调用时返回空对象导致 NPE。前期解法是重写onActivityCreated,赶在onCreateDialog之前判断内部持有的 Dialog 是否为空,为空则dismissAllowingStateLoss。

7.2 最终方案:Application.registerActivityLifecycleCallbacks

文档最终放弃了 DialogFragment,不是因为又发现了新问题,而是想到了更好的替代方案:通过Application.registerActivityLifecycleCallbacks监听 Activity 生命周期来管控 Dialog 的生命周期,作用与 DialogFragment 相同,却不会引入上述任何问题。

该方案在当前仓库 library/base/src/main/java/com/hjq/base/BaseDialog.java 中有完整实现,核心是内部类DialogLifecycle:

  • 它同时实现Application.ActivityLifecycleCallbacks、OnShowListener、OnDismissListener三个接口;
  • onShow时注册生命周期回调,onDismiss时反注册;
  • onActivityPaused时记录并移除 Dialog 动画(ANIM_EMPTY),onActivityResumed时延迟 100ms 还原动画,避免"从后台返回前台再次触发动画";
  • onActivityDestroyed时反注册回调、移除监听,若 Dialog 仍在显示则直接dismiss()。

此外,BaseDialog 还印证了文档中"一对多监听器"与"监听器泄漏"两个问题的解法:内部维护mShowListeners、mCancelListeners、mDismissListeners三个 List,提供addOnXxxListener/removeOnXxxListener系列方法;同时用ListenersWrapper等SoftReference包装类承接原生DialogInterface监听,避免 Dialog 持有 Activity 导致内存泄漏。而 Builder 的create()方法末尾调用DialogLifecycle.with(mActivity, mDialog)把 Dialog 与 Activity 生命周期绑定在一起,正是文档所述方案的生产级实现。

八、网络请求:为什么不用 Retrofit + RxJava,EasyHttp 的诞生

8.1 Retrofit 的三个问题

文档逐一拆解了 Retrofit 的不足:

  1. 功能太少:常用的通用请求头/参数、请求响应日志、动态 Host 及多域名配置等,Retrofit 全都没有封装,需要开发者自己用 OkHttp 拦截器实现;
  2. 不够灵活:支持上传但拿不到进度监听,只能拿到成功/失败;网上通过拦截器二次包装RequestBody的解法实现过程十分麻烦;
  3. 学习成本高:主要来源于注解体系——@Url、@Body、@Field、@FieldMap、@FormUrlEncoded、@Header、@HeaderMap、@Headers、@HTTP、@Multipart、@Part、@PartMap、@Path、@Query、@QueryMap、@QueryName、@Streaming共 17 个注解,大多数人只记住最常用的几个,都是照抄别人的写法。

文档的选型观是:"大家用的不一定就是最好的,盲目从众不是件好事"——选用框架前先分析它在项目实战中的优缺点,缺点大于优点就不能接受。

8.2 EasyHttp 的诞生与坚守

AndroidProject 长期不引入网络框架,是因为作者没有找到合适的框架,与其被动接受 Retrofit 带来的问题,不如自己做。参考 OkGo 作者弃更事件,文档作者深知网络框架"一入坑深似海",但仍花了半年多时间设计与开发,EasyHttp 于 2019 年 12 月 7 日面世——这与仓库 RequestHandler.java 类注释中的time : 2019/12/07完全吻合。

EasyHttp 发布初期热度很低,作者选择坚守并持续打磨,后续收到不少"很好用、灵活性很高"的反馈。当前仓库对 EasyHttp 的集成位于 InitManager.java:通过EasyConfig.with(okHttpClient)依次配置日志开关、服务器(RequestServer.java 实现IRequestServer,提供getHost()与表单提交策略)、请求处理策略(RequestHandler.java 实现IRequestHandler)、缓存策略、重试次数与全局拦截器(在interceptArguments中统一添加token、deviceOaid、versionName、versionCode等请求头)。而 RequestHandler.java 的requestSuccess/requestFail/downloadFail方法还完整实现了TokenException(登录失效跳转登录页并清空 Activity 栈)、TimeoutException、NetworkException、ServerException、CancelException等异常分级处理——这正是文档所述"常用功能都由框架封装"的直接证据。在 gradle/libs.versions.toml 中,EasyHttp 版本为 13.5,且整个依赖清单中没有任何 Retrofit / RxJava 条目。

九、基础设施选型:WebView、EventBus、图片加载、Jetpack 与权限体系

9.1 为什么优先用原生 WebView 而不是腾讯 X5

文档提出了一个反直觉的问题:腾讯 X5 WebView 一定比原生好吗?作者从 Android 9/10 初期微信、QQ 网页卡顿的亲身经历得出两点结论:

  1. SDK 有自我更新功能,意味着 WebView 的掌控权在腾讯手中;
  2. SDK 需要腾讯持续维护,项目的生命周期会跟随腾讯公司的发展和决策。

因此建议优先使用原生 WebView,不满足需求时再替换成 X5,理由是 AndroidProject"目标稳中求胜",且项目中已对 WebView 做了统一封装,后续替换成本相对较低。仓库中的封装实现在 app/src/main/java/com/hjq/demo/widget/webview:BrowserView继承自NestedScrollWebView,统一处理了 JavaScript 开关、DOM 存储、混合内容(MIXED_CONTENT_ALWAYS_ALLOW,解决 Android 5.0 上默认禁止 Http/Https 混合内容的问题)、滚动条隐藏与loadUrl系列日志打印,并由 BrowserViewClient.java、BrowserChromeClient.java、BrowserFullScreenController.java 配套完成页面加载、全屏与 JS 弹窗处理。这就是文档所说"统一封装、可低成本替换"的底层保障。

9.2 为什么不加入 EventBus

文档记录 EventBus 曾在 v10.0 之前加入过,后因"它不是项目必需品,且在实际项目中会出现大量滥用"而移除。文档的建议是:能用正常方式实现通讯的,尽量不要用 EventBus。从 gradle/libs.versions.toml 的依赖清单看,仓库当前没有 EventBus 依赖,与文档一致。

9.3 为什么不对图片加载框架再次封装

文档引用了其技术调研(Glide 是最受欢迎的图片加载框架,Fresco 占比极少),然后回答了一个常见疑问:为什么不把图片加载框架抽成接口以解耦?

答案是:抽取接口不难,难的是后续扩展。Glide 开放了大量 API(加载网络/圆角/圆形图片、图形变换、加载监听、缓存策略等),直接用 Glide 可以轻松实现;一旦经过一层封装,就会把框架本身的灵活性扼杀掉。文档的结论是"可以做但没有必要,付出和收益远远不成正比,还会增加学习成本"。

当前仓库的实践是"不封装业务接口,但做全局配置":GlideConfig.java 通过@GlideModule将 Glide 的磁盘缓存设置到应用缓存目录(上限 500MB)、内存缓存与 Bitmap 池提升到默认值的 1.2 倍、统一设置加载中/加载错误占位图,并在registerComponents中用OkHttpLoader.Factory(EasyConfig.getInstance().getClient())将 Glide 默认的HttpURLConnection网络层替换为 OkHttp——所有配置都直接调用 Glide 原生 API,没有引入任何业务封装层。

9.4 Jetpack:只取 Lifecycle,不要全家桶

文档明确说明:AndroidProject 用了 Jetpack 的Lifecycle特性,在 BaseDialog 中加入了该特性,EasyHttp 也用 Lifecycle 管控网络请求;但LiveData 和 ViewModel 在 AndroidProject 中基本没有用到,原因是"有自己的代码设计思想,只会集成合适的代码库,不会一味追求全家桶"。

仓库证据充分:BaseDialog声明implements LifecycleOwner,内部持有LifecycleRegistry,在onCreate、onStart、onStop、onDismiss中分别派发ON_CREATE、ON_START、ON_PAUSE、ON_STOP、ON_DESTROY事件,并处理了 BaseDialog 对象复用时的状态异常(见 BaseDialog.java 中handleLifecycleEvent对INITIALIZED/DESTROYED两个状态的兼容逻辑);BaseActivity通过dispatchKeyEvent结合fragment.getLifecycle().getCurrentState() != Lifecycle.State.RESUMED判断 Fragment 可见性(见 BaseActivity.java)。这套用法是"按需取用 Jetpack 组件"的典型示范。

9.5 为什么不用谷歌 ActivityResultContracts

文档指出,ActivityResultContract是 Activity 1.2.0-alpha02 与 Fragment 1.3.0-alpha02 才追加的新 API,而 AndroidProject在此之前早已对onActivityResult回调做了封装。文档给出的对比代码如下:

// Google 的用法 registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), new ActivityResultCallback<ActivityResult>() { @Override public void onActivityResult(ActivityResult result) { Intent data = result.getData(); int resultCode = result.getResultCode(); } }).launch(new Intent(this, HomeActivity.class));
// AndroidProject 的用法 startActivityForResult(HomeActivity.class, new OnActivityCallback() { @Override public void onActivityResult(int resultCode, @Nullable Intent data) { } });

文档的对比结论有四条:一是谷歌原生封装"不够人性化、调用稍微麻烦";二是谷歌方案直接集成进 AndroidX,比在 BaseActivity 中封装要好;三是 AndroidProject 封装 onActivityResult 至少比谷歌早一两年;四是已有用户习惯该 API,不能删也不能改。

当前仓库的实现位于 library/base/src/main/java/com/hjq/base/BaseActivity.java,相比文档示例还做了进一步健壮化:内部用SparseArray<OnActivityCallback>保存回调,requestCode通过do { requestCode = new Random().nextInt((int) Math.pow(2, 16)); } while (mActivityCallbacks.indexOfKey(requestCode) != -1);生成,保证不与已存在的请求码冲突(文档注释亦强调"请求码必须在 2 的 16 次方以内");onActivityResult命中回调后立即remove(requestCode),未命中的情况才交给super。这正是文档所述"更早、更人性化"封装的真实代码。

9.6 为什么新版移除了权限申请的 AOP 注解

文档说明移除原因是底层切面插件存在已知问题(gradle_plugin_android_aspectjx issue #60),为避免后面的人踩同样的坑而移除,同时贴出了删除前的实现供需要的人自行斟酌:

@Retention(RetentionPolicy.RUNTIME) @Target({ElementType.METHOD}) public @interface Permissions { /** * 需要申请权限的集合 */ String[] value(); }
@SuppressWarnings("unused") @Aspect public class PermissionsAspect { /** * 方法切入点 */ @Pointcut("execution(@com.hjq.demo.aop.Permissions * *(..))") public void method() {} /** * 在连接点进行方法替换 */ @Around("method() && @annotation(permissions)") public void aroundJoinPoint(ProceedingJoinPoint joinPoint, Permissions permissions) { Activity activity = null; // 方法参数值集合 Object[] parameterValues = joinPoint.getArgs(); for (Object arg : parameterValues) { if (!(arg instanceof Activity)) { continue; } activity = (Activity) arg; break; } if (activity == null || activity.isFinishing() || activity.isDestroyed()) { activity = ActivityManager.getInstance().getTopActivity(); } if (activity == null || activity.isFinishing() || activity.isDestroyed()) { Timber.e("The activity has been destroyed and permission requests cannot be made"); return; } requestPermissions(joinPoint, activity, permissions.value()); } private void requestPermissions(ProceedingJoinPoint joinPoint, Activity activity, String[] requestPermissions) { XXPermissions.with(activity) .permission(requestPermissions) .interceptor(new PermissionInterceptor()) .description(new PermissionDescription()) .request((grantedList, deniedList) -> { boolean allGranted = deniedList.isEmpty(); if (!allGranted) { return; } try { // 获得权限,执行原方法 joinPoint.proceed(); } catch (Throwable e) { e.printStackTrace(); CrashReport.postCatchedException(e); } }); } }

值得注意的是,当前仓库仍在使用 AOP 技术,但换成了更稳定的 AndroidAOP 框架(见 gradle/libs.versions.toml 中androidAop = "2.7.6"),并只保留了与权限无关的注解,例如 CheckNet.java 通过@AndroidAopPointCut(CheckNetCut.class)实现网络检测、Log.java 用于方法耗时统计、SingleClick.java 用于防重复点击——权限申请回归到 XXPermissions 框架的显式调用方式。权限相关的文案与描述映射则沉淀在 PermissionConverter.java,它针对 Android 12/13/16 等版本对蓝牙、WIFI、传感器等权限的归属变化做了精细化文案适配。

十、技术中台的定位、边界与升级策略

10.1 模板、架构、技术中台有什么区别

文档明确:AndroidProject 已正式从"安卓架构"更名为"安卓技术中台"。它符合技术中台的特性——既能快速开发,又能保证后续维护快速迭代。文档给出的通俗定义是"技术中台 = 模板 + 架构":一般写模板代码的人做不了架构设计,做架构设计的人不想写模板代码,技术中台恰好结合了两者的优点。README 中"敏捷开发"一节也印证了这一定位:一个 App 大概率会出现的功能已经写好,可将新项目开发或旧项目重构的周期缩短近一半(见 README.md)。

10.2 为什么不拆成多个框架来做这件事

文档澄清:AndroidProject 其实一直在拆分,权限请求框架 XXPermissions、网络请求框架 EasyHttp、吐司框架 Toaster 等都抽离成了独立开源项目单独维护;没有继续拆分的主要原因有二:

  1. 与业务耦合性高:例如 Dialog 组件引用了大量项目基类(BaseDialog、BaseAdapter 等);
  2. 业务有定制化需求:Dialog 的 UI 风格要跟随项目设计走,框架不适合做 UI 定制化——每个产品的设计风格都不一样,开放再多 API 也无法满足所有人。

所以像权限请求这类与业务关联性不大、随时可替换的组件适合抽成框架;与 UI 强耦合的组件跟随 AndroidProject 一起更新更好。当前仓库 library 目录下的base、core、customWidget、smallestWidth、umengSdk五个内嵌库与 gradle/libs.versions.toml 中通过 Maven 坐标引用的 XXPermissions(28.3)、Toaster(15.0)、EasyHttp(13.5)、TitleBar(10.8)、GsonFactory(10.5)、NestedScrollLayout(3.0)等外部框架,共同构成了"外部框架 + 内嵌中台"的分层格局。

10.3 为什么不加入 XXX 功能

文档给出的定位是:AndroidProject 是"技术架构,不是什么都做的 Demo 工程",不会为个人做定制(包括作者自己)。例如多语种框架 MultiLanguages 做得很好,但 App 国际化的场景并不多,大部分国内 App 没有上架 GooglePlay,遵循"少数服从多数"原则未加入。这是"大我与小我"之间的取舍。仓库中 common.gradle 与 InitManager.java 的 SDK 初始化清单(Bugly、MMKV、友盟、Chucker、Timber 等)均为国内 App 高频场景,恰好体现了这一取舍方向。

10.4 为什么最低兼容到 Android 5.0

文档记录:从 v11.0 版本起,minSdkVersion 从 19 升级到 21。原因是 Android 4.4 的兼容性问题太多——对 dex 分包、矢量图的支持不佳(开发者处理不了),大量 API 需要做高低版本兼容(能做但没必要),且该版本机型逐步退出历史舞台。AndroidProject 一旦投入项目,minSdkVersion 基本不会变动,所以"不如一开始就不兼容"。当前仓库的 common.gradle 已将minSdk进一步配置为 23、targetSdk为 36、compileSdk为 36,说明随着 Android 生态演进,最低版本门槛在文档记录的基础上继续上移——这本身就是文档"逐步淘汰旧版本"理念的延续。

10.5 "这不就是一个模板工程,我也能写一个"与升级策略

文档坦然承认:做出来不难(当时只花了一两个星期),做好它需要无限的时间和精力(已维护七年多、持续 Review 与迭代)。文档建议:若认可这套代码,可从代码设计上学习借鉴(后面可能专门撰文讲解 AndroidProject 的代码亮点)。

关于升级,文档给出的结论非常务实:AndroidProject 严格意义上不是框架而是架构,架构升级本身就是大事且存在未知风险,不推荐已投入使用的公司项目去升级(开发测试成本极高、给业务带来的价值很低);个人项目可以尝试,公司项目最好不要,若公司决定重构则风险由公司承担、按公司安排执行。升级方式上,由于它不是单独的框架、无法通过远程依赖更新,只能通过替换代码的方式更新,且替换后必须经过严格的自测与测试,特别是主流程功能。

十一、技术价值观:如何看待层出不穷的新技术

文档收尾于一套完整的技术价值观,核心观点如下:

  1. 新技术可以学,但应用到项目中要慎重:纵观历史,技术创新虽然受欢迎,但大多数经不住时间考验,最终一个个倒下——因为很多新技术"表面看起来很美好,但实际上一入坑深似海",优秀且活下来的创新只是少数;
  2. 先思考新技术能解决什么痛点:再好的技术创新也必须创造价值,否则就是扯淡;对 Android 程序员而言价值无非两点——前期开发上能否发挥大作用,后续维护上能否体现优势;
  3. 谷歌的新技术不一定都是好的:文档直言其中有些是"KPI 产物"——为了年终奖和晋升,即便是硬伤也会推出来看市场反应;
  4. 正确的技术价值观是:不看重是否大公司出品、不看重是否是行业名人写的,而应重点关注产品质量、能带来哪些帮助、会带来哪些不好的影响。

这套价值观贯穿了本文前面所有选型决策:从放弃 ButterKnife/ViewBinding/DataBinding 到放弃 DialogFragment、Retrofit、EventBus、单 Activity 多 Fragment、组件化,本质都是用"前期开发 + 后期维护"这把尺子逐一丈量后的结果;而对 MVP/MVVM、Jetpack、X5 WebView、侧滑框架的态度,则体现了"稳定性优先于新鲜感"的取舍倾向。如果你正在自建 Android 工程架构,这份"31 个为什么"清单本身就是一份可迁移的选型检查表,建议结合仓库 library 与 app/src/main/java/com/hjq/demo 的源码逐一对照验证,再做出你自己的决策。

  • 移动开发
  • 示例工程
  • UI组件

【免费下载链接】AndroidProject

Android 技术中台,但愿人长久,搬砖不再有

项目地址:https://gitcode.com/gh_mirrors/an/AndroidProject
点击查看免费下载

相关推荐

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询