ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Android开发现状与未来:Kotlin、Compose、模块化与性能优化

Android开发现状与未来:Kotlin、Compose、模块化与性能优化

十几年前我第一次在IDE里配好环境、跑通一个只带按钮和文本框的页面,那种兴奋劲儿到现在还记得。那时候Android开发的门槛低得离谱,会写布局XML,会findViewById,会开个后台线程更新UI,就能攒出一个能装到手机上的东西。十几年过去,Android开发这四个字的内涵被彻底重写了。现在再聊Android开发现状与未来,绕不开Kotlin、声明式UI、模块化、跨端方案、端侧智能这一堆话题,也绕不开岗位数量收缩、能力要求上移这些现实。这篇东西不打鸡血也不唱衰,我想把自己这些年做过的技术选型、带团队做栈升级时踩的坑、判断依据,一条一条摊开讲。

适合两类人看:入行三年以内、想知道力气该往哪使的;以及做了很多年、手里那套View体系经验不知道还能吃多久的。文章里的所有案例、数据、项目背景都做了脱敏处理,只保留可复用的方法论和配置。

1. 这十几年,Android开发的三个断层式变化

我习惯把Android开发的变化分成"渐变"和"断层"两类。渐变是每年加几个API、换个库、调个规范,不影响你吃饭;断层是那种一两年内把整个工作方式换掉的东西。十几年里真正算得上断层的只有三次:语言换血、UI范式换血、工程结构换血。搞清楚这三次断层的逻辑,你对"现状"的判断基本就不会跑偏。

1.1 从Java到Kotlin:语言层面的迁移账要算清

语言迁移这件事,很多团队当年是被"官方推荐"推着走的,我见过不少团队迁移完只是把Java的写法翻译成Kotlin语法,!!满天飞,扩展函数一个没写,协程碰都不碰,最后得出结论"Kotlin也就那样"。这是典型的只换了语法没换思维。

Kotlin真正改变工作方式的是三件事。第一是空安全,把一大类线上崩溃从"运行时随机炸"提前到了"编译期报错"。我做过一个统计,一个中型项目迁移完成后,NPE相关的崩溃占比从崩溃总量的三成左右降到了一成以内,剩下的基本都是平台层或者反射带进来的。第二是扩展函数和标准库,让工具类这种东西基本消失了,String.isValidPhone()这种写法比TextUtils.isEmpty(s)加一堆静态方法要清爽得多。第三是协程,把回调地狱和线程切换的样板代码压掉了大半。

但迁移有实打实的成本,这个必须提前算。最直接的是编译时间,混合编译阶段项目里同时存在Java和Kotlin,增量编译的收益会被稀释,我当时负责的一个项目,全量编译从两分半涨到接近四分钟。缓解手段是控制混合期长度,别让Java和Kotlin长期共存,定一个明确的迁移窗口,比如三个月内业务代码全部转完,剩下的老模块单独隔离。第二个成本是团队习惯,协程的作用域、异常传播、取消机制跟线程池完全是两套心智模型,直接让人上手写很容易出问题。

// 别这么写,异常直接被吞掉,调用方一无所知 fun load() { GlobalScope.launch { repo.fetch() } } // 推荐:绑定生命周期的作用域 + 显式异常处理 fun load() { viewModelScope.launch { runCatching { repo.fetch() } .onSuccess { _state.update { s -> s.copy(data = it) } } .onFailure { e -> _state.update { s -> s.copy(error = e.message) } } } }

注意:协程不是"更轻量的线程",它是可挂起的任务。把阻塞式IO直接塞进Dispatchers.Default里,池子会被占满,表现就是整个App莫名其妙卡住,而且日志里什么都看不到。所有阻塞调用都得显式切到IO调度器。

1.2 从命令式UI到声明式UI:Compose到底改变了什么

老View体系的心智模型是"拿到控件引用、改它的属性"。页面状态和数据是两份东西,靠开发者手动同步,同步漏了就是bug。当年做列表刷新,notifyDataSetChanged和局部刷新搞错位置导致状态错乱的事故,我经手的项目里至少有三起。

声明式UI把这件事反过来了:你只描述"当前状态长什么样",框架负责把界面刷成那样。听起来只是写法变化,实际影响的是整个调试思路——出了问题你不再去追"谁改了这个控件的属性",而是去看"这个状态为什么是这个值"。这个转变对老开发者来说反而是个坎,因为排查路径完全变了。

Compose真正让人头疼的是重组(Recomposition)。很多人写了半年还在抱怨"Compose卡",一查全是重组范围失控。核心判断标准只有一条:传给组件的参数是不是稳定类型。List、Map这类接口类型在编译器眼里是不稳定的,每次父组件重组都会认为参数变了,子组件就跟着白重组一遍。

// 不稳定:每次重组都被判定为参数变化 @Composable fun UserList(users: List<User>) { /* ... */ } // 稳定:使用不可变集合,配合 @Immutable 标注数据类 @Immutable data class User(val id: String, val name: String) @Composable fun UserList(users: ImmutableList<User>) { /* ... */ }

更隐蔽的一类问题是lambda捕获。onClick = { doSomething(item) }这种写法每次重组都会创建新的lambda实例,如果接收方没做记忆化,就会连带触发下游重组。正确做法是把lambda提到外面用remember包起来,或者直接用方法引用。

1.3 从单模块到多模块:工程结构被逼着进化

模块化不是架构洁癖,是被逼出来的。我印象最深的一个项目,单模块时期代码量大概二十万行,改一行代码全量编译六分钟,加上安装和冷启动验证,改一个文案到看见效果要八分钟。这种节奏下没人愿意做重构,技术债只会越滚越大,最后形成死循环。

拆模块的直接收益是编译提速和边界约束。编译提速好理解,只改动业务模块时,其他模块走缓存直接跳过。边界约束的价值更大:模块之间的依赖是显式的,谁引了谁一目了然,不会出现"改A模块把B模块搞崩了"这种灵异事件。

拆的粒度我一般按"变更频率"来定,而不是按"业务重要性"。高频变动的模块拆细,比如首页、详情页;低频但被广泛依赖的模块合并,比如各种工具类、网络封装、埋点。一个常见的错误拆法是按页面拆,最后拆出几十个模块,每个模块只有三五个类,构建脚本的维护成本比代码本身还高。

app(壳工程,只做装配) ├── feature-home 首页业务 ├── feature-detail 详情业务 ├── feature-profile 个人中心 ├── core-network 网络层封装 ├── core-storage 本地存储 ├── core-design 设计系统与通用组件 └── core-common 工具、扩展、常量

依赖方向必须是单向的:feature依赖core,core之间不互相依赖,core绝不反向依赖feature。这条规则一旦破了,模块化就名存实亡。我见过最离谱的情况是有人为了让core里的工具类能拿到某个业务实体,直接在core里引了feature,编译是过了,但整个依赖图彻底乱掉,后来排查循环依赖花了两天。

2. Android开发现状盘点:岗位、技术栈与真实工作内容

聊现状不能只看技术,还得看市场。这两件事在从业者身上是绑在一起的:技术栈的迁移节奏决定了岗位需求的画像,岗位画像又反过来决定你该学什么。我这几年参与过不少次技术面试,也帮朋友看过简历,有些现象挺能说明问题。

2.1 招聘市场上的能力画像拆解

现在的招聘要求跟五年前比,最大的变化是"单一技能"不再值钱。以前会写页面、会用几个主流框架就能找到不错的坑位,现在同样的简历会被一个问题打回原形:"你这个页面为什么这么写?换一种方案会怎样?"

我梳理了一下不同层级实际被考察的重点,这个表是我按近两年参与面试的经验总结的,不一定适用于所有团队,但方向应该差不多:

层级核心考察点常见失分项
初级(1-3年)语言基础、常用组件、能独立完成页面只会调API,说不出为什么这么选
中级(3-5年)架构设计、性能定位、模块划分优化只停留在"加了缓存",没有数据支撑
高级(5年以上)技术选型、跨端方案权衡、工程效率讲不清成本收益,方案缺乏落地路径

有一点很值得说:面试里问到"你做过什么优化",八成的人回答"启动优化、内存优化",然后就没有然后了。真正能过的是那种能把指标讲清楚的人——优化前冷启动多少毫秒,用什么工具测的,瓶颈定位在哪一段,改完降了多少,有没有副作用。数字比形容词有说服力得多。

2.2 现在主流App的技术栈长什么样

我给一个我实际用过的组合,基本覆盖了中大型App的常规需求,不是什么前沿试验品,都是跑了很久比较稳的东西:

层次选型选择理由
语言Kotlin空安全、协程、生态完整
UICompose + 少量View新页面全用Compose,老页面渐进迁移
状态ViewModel + 单向数据流状态集中,便于测试和回放
依赖注入编译期DI框架运行期反射方案启动开销大
网络OkHttp + 序列化库拦截器机制成熟,调试方便
存储关系型数据库 + 键值存储结构化数据和非结构化数据分开
图片主流图片加载库别自己写,缓存策略坑太多
构建Gradle + 版本目录依赖统一管理,避免版本打架

这里我想多说一句"少量View"。很多团队一上来就全量迁移Compose,结果被地图、播放器、相机预览这些重度依赖原生View的组件卡住,最后还是要走互操作,白白浪费了几个月。比较务实的做法是:新页面用Compose写,老页面按"改动频率"排序,改到哪个迁哪个,不专门开迁移项目。

2.3 跨端方案的真实定位与取舍

跨端这个话题每年都要吵一遍。我的看法比较朴素:跨端解决的是"人力复用"问题,不是"技术先进"问题。判断要不要上,只看三个变量——团队规模、页面同质化程度、性能敏感度。

方案适用场景不适合的场景
原生双端团队规模够、性能要求高、交互复杂人力紧张、页面高度同质
自绘引擎方案多端UI一致性要求高、动效多需要深度调用系统能力
桥接式方案已有前端团队、页面偏展示型高频交互、长列表滚动
多平台共享逻辑只想复用业务逻辑、UI各写各的团队没有Kotlin积累

我个人最推荐的是"共享逻辑层,UI各写各的"这条路。理由很实际:业务逻辑(数据模型、网络、缓存、状态机)的复用收益是确定的,而UI层的复用往往要在"一致性"和"平台体验"之间反复妥协,沟通成本极高。把逻辑层抽到多平台共享模块里,双端各写各的界面,这个方案的投入产出比最稳,风险也最低。

3. 核心细节解析:迁移与架构落地中的关键判断

前面讲的偏宏观,这一节落到具体判断上。这些年我带团队做技术栈升级,发现真正难的从来不是"会不会写",而是"什么时候写、写到什么程度停手"。选型失误的代价往往要一两年后才显现,那时候改起来的成本是当初的十倍。

3.1 Compose和View混用的边界怎么划

互操作有两个方向:在View里嵌Compose,在Compose里嵌View。两个方向的成本和坑完全不一样。

View嵌Compose相对简单,用ComposeView挂上去就行,注意生命周期的owner要正确传,否则rememberSaveable会失效、页面重建后状态丢失。这个问题我在两个项目里都遇到过,表现是旋转屏幕后输入框内容清空,查了半天以为是配置问题。

Compose嵌View就麻烦多了,用AndroidView包一层,但里面涉及测量冲突、焦点传递、触摸事件拦截。地图组件、视频播放器这类东西嵌进去以后,经常出现手势冲突——地图想拖拽,外层列表也想滚动,两边抢事件。解决思路是给外层容器加手势判断,或者干脆在嵌原生组件时禁用外层滚动。

我的经验边界是这样的:纯展示、纯交互的页面用Compose;强依赖Surface、需要精确控制渲染的组件用View;两者之间用一层薄封装隔开,别让业务代码直接感知到互操作的存在。这层封装的作用是将来某个组件准备好了要换成Compose实现时,只改封装层,业务不用动。

3.2 状态管理:MVI不是万能药

MVI这几年被吹得有点过。它的核心是单一状态源加单向数据流,好处是状态可追溯、可回放、便于测试。但它有个明显的代价:状态对象会膨胀。一个复杂页面把所有UI状态塞进一个数据类,很快就变成几十个字段的大对象,每次更新都要copy一份,字段多了以后性能和可读性都难看。

我现在的做法是分场景:

  • 表单类、列表类、状态机明确的页面,用严格MVI,一个UiState数据类加Event密封类,逻辑清晰,测试好写。
  • 纯展示、状态简单的页面,用StateFlow装几个独立字段就够了,没必要为了架构统一硬套。
  • 复杂动画、临时UI状态,不要塞进全局状态,用remember在组件内部管,页面重建时能恢复的用rememberSaveable。
// 严格MVI的骨架,注意副作用和状态是分开的 sealed interface UiEvent { data class ClickItem(val id: String) : UiEvent data object Refresh : UiEvent } data class UiState( val loading: Boolean = false, val items: List<Item> = emptyList(), val error: String? = null, ) fun onEvent(event: UiEvent) { when (event) { is UiEvent.ClickItem -> navigate(event.id) UiEvent.Refresh -> load() } }

提示:别把导航动作写进状态对象里。导航是一次性副作用,塞进状态会导致重组时重复触发,表现就是点一次跳两个页面。这个坑很经典,几乎所有刚上MVI的团队都会踩一遍。

3.3 模块化拆分粒度与依赖治理

拆模块最怕两件事:拆得太碎、依赖乱引。前者让构建脚本维护成本飙升,后者让模块化失去意义。

粒度这块我有个可操作的判断标准:一个模块的代码量如果少于五千行,且变更频率跟其他模块差不多,就别单独拆。五千行是个经验值,低于这个量级,拆出来的收益(编译提速、边界约束)抵不过维护成本(构建脚本、依赖配置、发布管理)。

依赖治理主要靠工具兜底。我一般会在CI里加两道检查:一是禁止core模块依赖feature模块,用脚本扫依赖图;二是禁止业务模块之间横向依赖,需要通信就走事件总线或者路由表。这两条规则如果不机器化执行,靠代码评审是守不住的,人一忙就会图省事直接引。

4. 实操过程:一个中型项目的技术栈升级全流程

这一节讲一个我实际主导过的升级项目,背景是:一个跑了四年多的App,单模块、Java和Kotlin混编、View体系、构建时间接近七分钟,团队六个人。目标是把技术栈拉到一个可持续的状态,不是追求最新,是追求"改得动"。

4.1 第一步:先量体温,做技术债盘点

任何升级动作之前都必须先有基线数据,否则你无法证明改动有效,也无法说服别人。我当时的盘点清单是这样的:

维度采集方式当时的结果
全量构建耗时干净环境跑三次取中位数6分52秒
增量构建耗时改一行业务代码2分10秒
冷启动耗时真机三次取中位数2.4秒
崩溃率线上统计0.31%
代码总量静态扫描约21万行
Java占比按文件统计38%

这组数字后来成了整个项目的验收标准。我当时的判断是:构建时间必须压到三分钟以内,冷启动压到1.8秒以内,Java占比清零。崩溃率没有硬指标,因为影响因素太多,不作为本次升级的考核项。

盘点阶段还有个隐性收益:你会真正看清自己的项目烂在哪。我在这个阶段发现了一个藏了两年的问题——有个工具模块被二十多个页面依赖,里面居然引了图片加载库,导致整个图片库无法升级。

4.2 第二步:依赖统一与构建提速

依赖版本打架是中型项目的通病,表现是"明明升级了却还是旧版本",因为传递依赖把版本拉回去了。解决办法是用版本目录统一管理,所有版本号集中在一个文件里,谁也不能在模块里写死版本。

# gradle/libs.versions.toml [versions] kotlin = "2.0.21" composeBom = "2024.10.01" coroutines = "1.9.0" [libraries] compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" } coroutines-core = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-core", version.ref = "coroutines" }
// build.gradle.kts dependencies { implementation(platform(libs.compose.bom)) implementation(libs.coroutines.core) }

构建提速这块,我做的改动其实不复杂,但收益很明显:

# gradle.properties org.gradle.jvmargs=-Xmx4g -XX:+UseParallelGC org.gradle.parallel=true org.gradle.caching=true org.gradle.configuration-cache=true kotlin.incremental=true

这几行里,配置缓存带来的提升最大,我们项目从2分10秒降到了1分20秒左右。但它对构建脚本的写法有要求,任何在配置阶段读取环境变量、执行外部命令的脚本都会让它失效。第一次开启大概率会失败,报错信息会直接告诉你哪个脚本不兼容,照着改就行。

并行构建和构建缓存的效果取决于模块化程度,单模块项目开了也没用。这也是我为什么把模块化排在构建提速前面——先拆模块,再开这些开关,收益才是叠加的。

4.3 第三步:增量迁移的灰度与回滚策略

迁移这件事最忌讳"大爆炸式"重构。我采取的策略是分层推进,每一层都有独立的回滚方案。

第一层是语言迁移,纯机械替换,风险最低。做法是先加Kotlin插件,新代码全部用Kotlin,老代码按模块排期翻译。翻译过程中严禁顺手重构逻辑,改逻辑和改语言必须分开提交,否则出问题时你根本分不清是哪一步引入的。这条纪律我当时盯得很死,代码评审看到夹杂逻辑变更的直接打回。

第二层是UI迁移,风险最高。我的策略是选一个独立性强的新需求页面作为试点,用Compose完整实现,跑一个版本观察。观察指标是页面卡顿率、崩溃率、用户停留时长,跟同类老页面做对比。试点跑稳了再按页面逐个推。

第三层是模块拆分,按前面说的粒度标准执行。拆分过程中有一个容易被忽略的细节:资源冲突。多个模块里同名的字符串、图片资源合并后可能互相覆盖,处理方式是给每个模块加资源前缀。

android { resourcePrefix = "home_" }

回滚方案我要求每个阶段都必须有:语言迁移靠Git分支隔离,出问题直接回退整个模块;UI迁移靠开关控制,服务端下发配置决定走新页面还是老页面;模块拆分靠依赖图快照,拆分前后对比依赖关系,异常直接回滚到拆分前。

4.4 第四步:性能基线与监控埋点

升级完成后如果没有持续监控,三个月内一切都会回到原点。我在项目里加了两类埋点:一类是构建指标,CI每次构建记录耗时,超过阈值告警;另一类是运行时指标,启动耗时、页面首帧、卡顿次数,按版本聚合。

启动耗时的采集我用的是系统提供的时间戳打点,在主进程的Application和首屏Activity的关键节点埋,输出到日志和上报通道。这个方案精度够用,成本几乎为零,比接第三方监控库要轻。有一段时间我们启动耗时突然涨了三百毫秒,就是靠这个埋点定位到的——某个初始化任务被挪到了主线程,代码评审时没人注意到。

5. 性能优化:从"能用"到"好用"的几个关键指标

性能优化这个话题容易写得很虚,我尽量给可量化的东西。Android上的性能问题,八成集中在启动、卡顿、内存三块,其他都是衍生。

5.1 启动耗时的拆解与优化路径

冷启动可以拆成三段:进程创建到Application创建、Application初始化、首屏渲染。定位方法是打时间戳,看时间花在哪一段。

我见过最常见的问题是Application里堆初始化任务。项目跑久了,谁都能往Application的onCreate里加一行初始化,加的时候都是"就一行,很快",加起来就是几百毫秒。处理方式是引入启动任务调度,把任务分成"必须主线程同步"、"可以异步"、"可以延迟到首屏后"三类。

必须同步的其实很少:崩溃上报、核心配置加载、依赖注入容器初始化。网络库、图片库、统计、推送这些全部可以异步或者延迟。我做过一个对比,把非必要初始化挪走以后,冷启动从2.4秒降到1.7秒,改动量不到两百行。

还有一个收益很大的手段是启动配置预编译。系统在新版本上支持生成启动配置文件,把类加载的热点路径提前编译好,能省掉一部分解释执行的开销。接入方式不复杂,用基准测试模块生成配置文件,随安装包发布就行。

// 基准测试模块里的写法,生成启动配置 @RunWith(AndroidJUnit4::class) class StartupBenchmark { @Test fun generateBaselineProfile() { rule.collect(packageName = "com.example.app") { startActivityAndWait() device.waitForIdle() } } }

5.2 卡顿与内存:工具链和判断标准

卡顿的定位我一般分两步走:先看现象是掉帧还是主线程阻塞。

掉帧用系统自带的帧率工具看,能直接看到每一帧的耗时。如果连续多帧超过16.6毫秒,就是渲染压力大,去查布局层级、过度绘制、图片解码。如果是偶发的长卡顿,用系统级的追踪工具抓一段,看主线程在哪个方法上停了超过50毫秒。

主线程阻塞最常见的原因我列几个:主线程读写数据库、主线程做JSON解析、主线程加载大图、锁等待。这几个占了我们线上卡顿归因的绝大多数。有一个特别隐蔽的是锁等待——某个单例的初始化用了同步锁,而初始化里又做了网络请求,表现就是首次进入某个页面必卡一秒。

内存这块,判断标准比工具更重要。我的经验是看两个数:常驻内存和GC频率。常驻内存持续上涨不回落,基本就是泄漏;GC频繁触发(尤其是年轻代),说明对象分配速率太高,通常是循环里创建对象或者字符串拼接。

泄漏排查用内存分析工具抓堆转储,重点看Activity和Fragment的引用链。最常见的三类泄漏:静态变量持有Context、匿名内部类持有外部引用、注册了监听没反注册。第三类最难查,因为代码上完全看不出问题,只有从引用链上才能看到是哪个管理器忘了摘监听。

注意:别在优化阶段盲信工具给的"建议"。有些工具会建议你把某个方法内联、把某个字段改成静态,这些微观优化在真实场景里收益接近于零,反而可能引入问题。优化的顺序永远是:先减少工作量,再优化执行方式,最后才是微观调整。

6. 常见问题与排查技巧实录

这一节是我这些年攒下来的问题清单,都是文档里不太会写、但实际会反复遇到的东西。

6.1 高频问题速查表

现象大概率原因处理方向
页面旋转后状态丢失状态没走可保存容器,或宿主生命周期不对检查状态容器与宿主绑定关系
点击一次跳两个页面导航写在状态里,重组重复触发导航改成一次性事件
列表滑动卡顿图片解码在主线程,或条目内创建对象异步解码 + 复用池
首次进入页面必卡单例初始化含阻塞操作初始化异步化 + 预加载
构建突然变慢配置缓存失效,或新增模块破坏了并行看构建扫描报告定位
内存只涨不降监听未反注册、静态持有上下文抓堆转储看引用链
升级依赖后崩溃传递依赖版本冲突用依赖树命令排查

6.2 文档里不会写的坑

坑一:协程作用域选错导致任务被意外取消。用页面级作用域发起"上报日志"这种不该被取消的任务,用户快速退出页面,任务就没了。正确做法是把这类任务放到应用级作用域,或者在页面作用域里用不会随取消而中断的方式。

坑二:Compose里的副作用执行次数和你以为的不一样。LaunchedEffect(Unit)只执行一次,但如果它所在的组件因为key变化被重建,就会再执行一次。我遇到过一个埋点重复上报的问题,排查半天发现是列表项没加稳定key,滚动时组件反复重建,副作用跟着重复。

坑三:模块化以后资源ID错乱。多模块合并资源时,如果不同模块里有同名资源,后合并的会覆盖前面的,而且不会有任何警告。表现是某个页面的图标突然变成了另一个页面的图标,查起来毫无头绪。解决方案就是前面说的资源前缀,早加早省事。

坑四:增量编译的假象。有时候改了代码没生效,不是代码问题,是增量编译的缓存出错了。判断方法很简单,清一次构建再跑,如果生效了就是缓存问题。别在这种事情上浪费半小时。

坑五:真机调试和模拟器表现差异巨大。尤其是启动耗时和内存,模拟器的数据基本没有参考价值。性能相关的验证必须在真机上做,而且要覆盖低端机,我吃过一次亏——高端机上优化效果明显,低端机上反而更卡,因为优化引入了额外的线程切换。

7. 未来三到五年:Android开发者的能力该往哪补

聊未来容易变成空谈,我只讲我能看到的、有实际信号的方向。

7.1 端侧智能与新形态设备带来的增量

端侧模型推理这件事,从"概念"变成"可用"的速度比我想象的快。现在已经有项目在本地跑轻量模型做图片分类、文本摘要、语音唤醒,好处是隐私不出设备、响应快、不依赖网络。对Android开发者的影响是:多了"模型集成与推理优化"这一层工作,需要懂模型量化、内存占用控制、推理线程调度。这些技能组合目前市场上很稀缺,会的人不多。

另一个增量是新形态设备。折叠屏、车机、大屏、穿戴,这些设备的共同特点是屏幕尺寸和交互方式差异大,而声明式UI恰好擅长处理"同一份状态、多种布局"的场景。我用Compose做过一个适配折叠屏的页面,用窗口尺寸断点切换布局,代码量比用传统方案少了一半。这个方向的需求是实打实增长的。

7.2 能力结构的重新排序

如果把Android开发者的能力拆成几块,我个人的排序是这样的:

第一梯队是工程能力——构建体系、模块划分、依赖治理、CI流水线。这块能力决定你能不能在一个大项目里长期高效工作,而且它跟具体技术栈无关,迁移成本最低。

第二梯队是性能与稳定性——能定位问题、能给出数据、能做权衡。这是拉开差距的地方,也是面试里最能体现深度的地方。

第三梯队是UI实现能力——包括声明式UI和新形态适配。这是基本功,但正在被工具和框架不断降低门槛。

第四梯队是单一框架的熟练度。说实话,这个的重要性在快速下降。框架三年一换,把精力全押在某个框架的API细节上,性价比越来越低。

我个人的体会是,过去十年里真正保值的从来不是"会用某个库",而是"能判断该不该用某个库"。前者靠看文档,后者靠踩坑。所以如果让我给建议,我会说:多参与技术选型的决策过程,哪怕只是旁观;多做一些需要权衡的取舍,而不是只做执行;多记录数据,别让优化停留在感觉上。

最后分享一个我一直在用的习惯:每做完一个项目,花半小时写一份"如果重来一次我会怎么做"的清单,只写三条,写具体的事,不写感想。这个习惯坚持下来,比看十篇架构文章管用。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进