做了 7 年 Android,实在受不了那些反人类的 API,于是我用野路子重构了它的开发体验 - V2EX
爱意满满的作品展示区。
fankes

做了 7 年 Android,实在受不了那些反人类的 API,于是我用野路子重构了它的开发体验

  •  
  •   fankes
    fankes May 30 2547 views

    大家好啊。

    我是个搞了 7 年多 Android 原生开发的老人了,平时常翻 Android Framework 源码,也懂点逆向 Hook 和 AOP 插桩,比如之前写过的 YukiHookAPI,偶尔还会写点后端( PHP/SpringBoot )折腾一下全栈。我一直有个习惯:在实际业务里踩了坑,就想办法通过开源工具库把能力沉淀下来,主打一个“开源反哺业务”。

    写 Android 久了,人很容易进入一种奇怪的状态:有些需求不是不会写,而是每次碰到那些极度眼熟、又长又臭的 API ,往往会在敲键盘前忍不住长叹一口气:“怎么又是你?”

    我算是个重度强迫症患者,对 Android 的很多不满,其实不是因为“它做不到”,而是“它明明能做到,但代码写起来怎么就这么别扭?”

    比如一大坨的 Builder,比如不知道怎么就被吞掉的 padding,比如永远记不清参数顺序的 dp2px

    于是,就有了 BetterAndroid

    它不是什么想去颠覆什么架构的新框架,也不仅仅是塞一堆毫无灵魂的 xxxUtils。它更像是我对自己这 7 年编码体验的一次“重构”。我的初衷很简单:在不破坏原生能力、不强行改变项目架构的前提下,把那些重复、繁琐、兼容性像盲盒一样的部分,整理成更顺手的现代 Kotlin 写法。

    这里面的很多功能,都是我在被实际业务狠狠按在地上摩擦后,抠出来的痛点。

    1. 系统栏:Android 适配里最坑人的无底洞

    如果只看文档,弄个状态栏、导航栏无非就是调个颜色、设几个 flag 。但只要你真正在项目中处理过沉浸式、Edge-to-Edge 或者在深浅色内容间跳跃,就知道这玩意有多恶心。不同系统版本表现不一样,不同国产 ROM 甚至还有私有实现。

    在 BetterAndroid 里,我搞了个 SystemBarsController。它不是简单帮你改个颜色,而是把整个系统栏当成一个独立的“界面控制对象”。 很多老哥遇到的坑是,一开启 Edge-to-Edge ,内容就被刘海、水滴或者底部的短条给挡了。BetterAndroid 默认会用 safeDrawingIgnoringIme 给根布局垫一层安全的 padding ;如果你想自己掌控一切,直接置空 edgeToEdgeInsets 就行。总之,我不止给你提供方法,更想帮你提前把布局关系给想好。

    2. Insets:把 Compose 的爽感偷回原生 View

    Insets 是个好东西,但原生的写法实在让人没法夸: WindowInsetsCompat.getInsets(WindowInsetsCompat.Type.systemBars()) 页面稍微复杂点,这就是一坨又长又绕的代码。

    在这里,我偷师了 Jetpack Compose 的思路,把 Insets 独立成了 ui-extension 的核心能力。现在你可以直接拿:

    insetsWrapper.systemBars insetsWrapper.ime 

    甚至它还支持 +-orand 运算。应用到 View 上也不用手写繁琐的 setPadding,直接:

    view.setInsetsPadding(systemBars) view.updateInsetsPadding(insetsWrapper.ime, bottom = true) 

    而且小细节是:它会保存你本来的 padding 作为基线! 再也不怕 Insets 一刷新,把你辛辛苦苦在 XML 里写的内边距全吃掉了。

    3. 发个系统通知,为什么要像造火箭一样?

    高低版本兼容、坑爹的通知渠道、Android 13 的运行时权限……写到最后,业务代码里混着一大段 Builder,看着就心烦。

    我对通知做了一整套封装,我希望发通知的代码看起来是在描述“我要发一条通知”,而不是在拼装宇宙飞船:

    context.createNotification( channel = NotificationChannel("my_channel_id") { name = "My Channel" } ) { smallIcOnResId= R.drawable.ic_my_notification cOntentTitle= "My Notification" cOntentText= "Hello World!" }.post() 

    第一次 post 自动创建渠道,遇到同 ID 自动复用,权限优先级也成了友好的枚举,少掉一堆头发。

    4. 都什么年代了,我真的不想再写 dp2px 了

    相信我,每个接手屎山的项目,都能搜出 3 个以上的 DensityUtils。在 XML 里写 10dp 那么自然,一到代码里就得搞转换。

    BetterAndroid 给 Kotlin 提供了一套很直觉的扩展:

    val px = 10.toPx(context) val dp = 36.toDp(context) 

    如果你在 Fragment / Activity 或者有 Context 的类里,只要实现一个 DisplayDensity 接口,甚至可以直接:

    val px = 10.dp val dp = 36.px 

    对嘛,写 Kotlin 就应该有写 Kotlin 的样子。 (注:如果你的项目重度依赖 Compose ,不建议混用避免命名冲突,这点我也在文档里标红了)

    5. 国产 ROM 识别:一门玄学与野路子工程学

    搞过国内适配的都知道,只判断 Android 版本那是图样图森破。 MIUI 、HyperOS 、ColorOS 、OriginOS 、MagicOS……它们都叫 Android ,但遇到 Bug 时个个都想让你怀疑人生。

    得益于平时摸爬滚打做系统级问题定位的经验,BetterAndroid 里的 RomType 没有用市面上死板的设备型号匹配,而是通过综合判断底层系统属性、类存在性等核心特征去巧妙“闻”出来的。它不仅暴露出来给你用:

    if (RomType.matches(RomType.MIUI)) { // 专门针对 MIUI 写 workaround... } 

    我在底层做系统栏适配和异形屏去坑时,也重度依赖了这套判断。这完全是在各种真实设备里滚钉板总结出来的填坑指南。

    最后,聊句大实话:关于 Compose 与 View

    现在大趋势是 Compose ,这毋庸置疑,我自己其实也是 Jetpack Compose 生态的重度使用者。我也做了 compose-extension 去支持跨平台,但现实是怎样的呢? 现实是大量的老项目、历史组件、复杂的屎山,依然在原生 View 体系里苟延残喘。大部分老哥根本没法(也没空)喊一句“全部重构为 Compose”。

    所以 BetterAndroid 这个东西,本质上是给咱们这些还在维护/编写原生 View 的人,留一条更体面的路:不逃离原生,但也不被原生 API 的历史包袱恶心死。

    其实顺带我还搞了一个极其头铁的项目叫 Hikage,一个 Android 响应式 UI 构建工具,让原生组件也能有更接近声明式布局的极致体验(有兴趣可以看看)。

    总而言之,BetterAndroid 是我这 7 年踩坑、绕路、咒骂 API 之后的一次总账。如果你也曾对着某些弱智的源码发出过“卧槽这玩意是不是能写得稍微像个人一点”的感叹,希望这个库能帮你顺顺气。

    GitHub 传送门:

    欢迎体验、提 Issue 、扔砖,如果能骗个 Star 就更好了,嘿嘿。

    12 replies    2026-06-01 12:18:08 +08:00
    AutumnVerse
        1
    AutumnVerse  
       May 30 via iPhone   2
    一年半安卓选手路过,看了楼主的帖子,我死去的记忆又开始攻击我了。

    依稀记得,无数个日夜调各种傻逼兼容,不同版本,不同厂家的设备,总有奇奇怪怪的表现,费尽无数个日夜实现出来的东西,设计走查来一句你这个间距在 xxx 设备上多了 1px ,要不就是 qa 来一句,在 xx 设备上是好的,但是 xx 设备上不对。那真是天塌了

    后来跳坑搞后端了,没有什么 api 兼容,没有什么厂商兼容,太爽了
    iAcn
        2
    iAcn  
       May 30 via Android
    你的想法在十多年前就有人做过类似的东西了..比如 xUtils
    WngShhng
        3
    WngShhng  
       May 30   1
    10.toPx(context) 这种封装其实还可以更简洁一些,
    总的来说 kotlin 刚出来那会写这些还有些新意,现在 Android 写得再花哨也没用了
    fbu11
        4
    fbu11  
       May 30
    现在有 AI ,你说的这些痛点,AI 能解决大部分
    showmethetalk
        5
    showmethetalk  
       May 30
    @iAcn 不是一类东西
    little_cup
        6
    little_cup  
       May 30
    我说话有点不客气啊…在古法时代,这种逻辑封装思路也应该是 2~3 年左右的研发的水平。7 年经验的人不应该还按这种思路做事情。
    iAcn
        7
    iAcn  
       May 30
    @showmethetalk 有什么不同?愿闻高见
    marco330
        8
    marco330  
       May 30
    @little_cup 所以正确思路是啥
    lisongeee
        9
    lisongeee  
       May 31
    第五条和 https://github.com/getActivity/DeviceCompat 有什么区别?

    对于 https://betterandroid.github.io/BetterAndroid/zh-cn/config/r8-proguard.html 这项配置,实际上可以直接将 proguard.txt 打包进库,不需要告知用户额外填写混淆配置
    fankes
        10
    fankes  
    OP
       May 31
    @lisongeee #9 这里参考了 DeviceCompat 的封装对系统类型进行了补全,之前的识别方案是我当年和借助我项目的用户群体提供的资源,自己去对各种设备的 `framework.jar` 手机一点点找的。R8 这边的配置你说的没错,后续会考虑一下,但是每个人对项目的混淆配置可能有不同的方案,默认补全配置可能对于想要局部自定义规则的人来说不是很友好,因为这个配置是向下覆盖的
    showmethetalk
        11
    showmethetalk  
       May 31
    @iAcn #7 在功能上确实有些相似吧,但给我的感觉是设计出发点不同,一个是高层次的封装,为了更容易实现某些功能; 一个是改善现有的 api 设计,为了屏蔽因历史原因等导致的 api 过于难看、反人类的接口设计
    xuld
        12
    xuld  
       2 days ago
    好项目。非常支持
    About     Help     Advertise     Blog     API     FAQ     Solana     2729 Online   Highest 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 51ms UTC 15:44 PVG 23:44 LAX 08:44 JFK 11:44
    Do have faith in what you're doing.
    ubao msn snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86