Jetpack 架构组件 ★
你的重点短板。 Jetpack 是应用开发日常,中级面试必问 ViewModel/LiveData/Room/Hilt 原理。
一、ViewModel
- 作用:持有 UI 相关数据,在配置变更(旋转)时存活,与 UI 解耦。
- 存活原理:对外契约是同一
ViewModelStoreOwner在配置变更后可取回同一个 ViewModel,直到该 owner 真正结束才调用onCleared()。实现层面可理解为ViewModelStore被保留并在重建后重新关联,历史实现中会经过 Activity 的 NonConfigurationInstance 保留链路;面试中不要把某个内部方法名当成稳定 API 保证。 - 不要持有 View/Context 引用(会泄漏);需要 Context 用 AndroidViewModel(持 Application)。
SavedStateHandle:在进程被杀重建后恢复关键数据(配合 savedState)。viewModelScope:绑定 ViewModel 的协程作用域,onCleared 时取消。
二、Lifecycle
- LifecycleOwner:Activity/Fragment 实现它,暴露 Lifecycle。
- LifecycleObserver:观察生命周期事件,把生命周期相关逻辑(如开始/停止定位)从组件中解耦出去。
- 现代用
DefaultLifecycleObserver或lifecycleScope.launch { repeatOnLifecycle(STATE) { } }。
三、LiveData
- 生命周期感知的可观察数据持有者:只在活跃状态(STARTED/RESUMED)通知观察者,组件销毁自动移除观察,避免泄漏和崩溃。
setValue(主线程)/postValue(任意线程)。- 粘性问题:新观察者会立即收到最新值,用于“事件“场景会重复触发(用 SingleLiveEvent/Flow 替代)。
- MediatorLiveData:合并多个源。
- 现代趋势:新项目用 StateFlow/SharedFlow 替代,但老项目仍大量使用,必须懂。
四、Room
- SQLite 之上的 ORM,编译期校验 SQL。
- 三要素:
@Entity(表)、@Dao(数据访问接口)、@Database(数据库持有者)。 - 支持 协程 suspend 和 Flow 返回(数据变化自动推送)。
- 数据库迁移 Migration:版本升级写 Migration,否则崩溃;开发期可
fallbackToDestructiveMigration(清库)。 - 查询返回 Flow 时,表数据变化会自动重新发射,天然支持响应式 UI。
五、Navigation
- 定位:单 Activity 多 Fragment/Composable 架构的导航框架,把“目的地、参数、Action、Deep Link、回退栈”集中声明,减少手写 FragmentTransaction/Intent flag 的分散逻辑。
- 对象关系:
NavHost:承载导航内容的容器,Fragment 场景常见NavHostFragment,Compose 场景是NavHostComposable。NavController:执行navigate/popBackStack的控制器,维护当前目的地和 back stack。NavGraph:目的地和跳转关系的图,可来自 XML,也可在 Compose 中用 Kotlin DSL 声明。NavBackStackEntry:回退栈条目,携带 arguments、Lifecycle、SavedStateHandle,也可作为 ViewModel 作用域边界。
- 导航调用流程:
View 点击/Intent → 调用 NavController.navigate(route/action) → 按 NavGraph 匹配目的地与参数 → 创建或恢复目的地 → 新 NavBackStackEntry 入栈 → 目标 Fragment/Composable 渲染 → 返回时 popBackStack 出栈并恢复上一 entry。 - 回退栈 API:
navigate()入栈新目的地;可用popUpTo清理中间页面,登录后清空登录流常用。popBackStack()返回上一目的地;指定 destination/route 时可弹到某个节点。launchSingleTop避免重复点击把同一目的地压多份。
- Deep Link:可以在图中声明 URI/Action/MIME 匹配。外部 Intent 进入后由 Navigation 匹配目标目的地并补齐必要的 back stack;面试要强调参数校验和未登录拦截,不要只说“配置一个 deepLink”。
@Composable
fun AppNavHost(navController: NavHostController = rememberNavController()) {
NavHost(navController = navController, startDestination = "home") {
composable("home") {
HomeScreen(onOpenDetail = { id -> navController.navigate("detail/$id") })
}
composable(
route = "detail/{id}",
arguments = listOf(navArgument("id") { type = NavType.StringType }),
) { entry ->
DetailScreen(id = entry.arguments?.getString("id") ?: return@composable)
}
}
}
常见错误:
- 在多个页面散落手写跳转字符串,导致参数名/route 不一致;可用常量、封装 route builder 或 Safe Args 降低风险。
- 登录、首页、详情都压入同一栈后不清理,返回路径混乱;需要明确
popUpTo和inclusive。 - 把导航事件放进持久 State,旋转后重复 navigate;一次性导航应使用 Effect/SharedFlow。
- Deep Link 只做跳转不做鉴权和参数兜底,容易打开非法状态页面。
面试答题结构:先说 “NavGraph + NavHost + NavController” 三件套 → 再讲 back stack entry 的入栈/出栈流程 → 补 Compose/XML 两种写法 → 最后说 deep link、参数安全、重复导航与登录栈清理。
六、Hilt(依赖注入)
- 基于 Dagger 的 Android 专用 DI 封装,大幅减少模板代码。
- 核心注解:
@HiltAndroidApp(Application)、@AndroidEntryPoint(注入到组件)、@Inject(构造注入)、@Module+@Provides/@Binds(提供无法构造注入的依赖)、@HiltViewModel(注入 ViewModel)。 - 作用域:
@Singleton、@ActivityScoped、@ViewModelScoped等,控制实例生命周期。 - Hilt vs Dagger:Hilt 预定义了 Android 组件的标准注入点和组件层级,开箱即用;Dagger 更灵活但配置繁琐。
- DI 的意义:解耦、可测试(可替换 mock 实现)、统一管理对象创建。
七、DataStore / WorkManager / Paging
- DataStore:替代 SharedPreferences。基于协程 + Flow,异步、事务安全、无 ANR 风险。两种:Preferences DataStore(键值)、Proto DataStore(类型安全)。SP 的问题:同步 apply 仍可能阻塞、
getXXX可能主线程 IO、无类型安全。 - WorkManager:可延迟、保证执行的后台任务(即使 App 退出/重启)。支持约束(网络、充电)、链式、周期任务。底层根据系统版本选 JobScheduler/AlarmManager。适合上传日志、同步数据。
- Paging3:分页加载大列表,核心是
PagingSource → Pager → Flow<PagingData<T>> → UI Adapter/Compose。PagingSource<Key, Value>:定义load(params)如何按页加载数据,返回LoadResult.Page/Error;getRefreshKey()决定刷新时从哪里继续。Pager:接收PagingConfig和pagingSourceFactory,把分页请求包装成Flow<PagingData<T>>。PagingData:一次分页会话的数据流,UI 侧通过collectAsLazyPagingItems()或PagingDataAdapter.submitData()消费。RemoteMediator:网络 + 本地数据库场景的协调器,负责判断何时从网络拉新页、写入 Room,UI 实际从 Room 的 PagingSource 读取,形成单一数据源。
class UserPagingSource(private val api: UserApi) : PagingSource<Int, User>() {
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, User> = try {
val page = params.key ?: 1
val users = api.users(page = page, size = params.loadSize)
LoadResult.Page(
data = users,
prevKey = if (page == 1) null else page - 1,
nextKey = if (users.isEmpty()) null else page + 1,
)
} catch (t: Throwable) {
LoadResult.Error(t)
}
override fun getRefreshKey(state: PagingState<Int, User>): Int? =
state.anchorPosition?.let { anchor ->
state.closestPageToPosition(anchor)?.prevKey?.plus(1)
?: state.closestPageToPosition(anchor)?.nextKey?.minus(1)
}
}
val users: Flow<PagingData<User>> = Pager(
config = PagingConfig(pageSize = 20, prefetchDistance = 5),
pagingSourceFactory = { UserPagingSource(api) },
).flow.cachedIn(viewModelScope)
RemoteMediator 流程:UI 触发刷新/滑到底 → Paging 判断需要加载 → RemoteMediator.load(REFRESH/PREPEND/APPEND) 请求网络 → 事务写入 Room + remote keys → Room 的 PagingSource 失效并重新发射 → UI 收到新的 PagingData。这种方式适合离线缓存、列表恢复、网络与本地一致性要求高的页面。
UI 集成要点:
- RecyclerView 用
PagingDataAdapter+LoadStateAdapter展示刷新、加载更多、错误重试。 - Compose 用
collectAsLazyPagingItems()和LazyColumn.items(count);根据loadState.refresh/append渲染首屏 loading、尾部 loading、错误重试。 - 在 ViewModel 中
cachedIn(viewModelScope),避免旋转屏幕后重新创建分页流导致重复请求。
常见错误:
PagingSource同时读网络和本地且没有一致性策略,刷新/翻页状态难维护;复杂缓存场景应考虑RemoteMediator + Room。- 忘记实现合理的
getRefreshKey,刷新后列表跳到错误位置。 - 不处理
LoadState,用户看不到加载、错误、重试状态。 - 每次 UI 重组/重建都新建 Pager,导致重复加载;应让分页流由 ViewModel 持有并缓存。
面试答题结构:先说 “PagingSource 负责加载一页,Pager 产出 PagingData Flow,UI 订阅渲染” → 再补 “RemoteMediator 用于网络 + 数据库单一数据源” → 最后讲 cachedIn、LoadState、刷新 key、错误重试这些工程坑。
高频面试题
Q1:ViewModel 为什么能在旋转屏幕后存活? 旋转是配置变更,Activity/Fragment 会重建,但同一 ViewModelStoreOwner 的 ViewModelStore 会在配置变更后重新关联,因此可取回同一个 ViewModel 实例。历史实现可从 NonConfigurationInstance 保留链路理解,但对外稳定契约是“配置变更存活、owner 真正结束时 onCleared”,不要依赖内部方法名。
Q2:ViewModel 能持有 Context 吗? 不能持有 Activity/View Context(泄漏)。需要 Application Context 时用 AndroidViewModel。原则:ViewModel 不感知 UI。
Q3:LiveData 的粘性事件问题是什么?怎么解决? 新观察者注册时会立即收到最新值,对“一次性事件“(导航/Toast)会重复触发。解决:SingleLiveEvent、事件包装类、或改用 SharedFlow(replay=0)。
Q4:LiveData 和 StateFlow 怎么选? 新项目优先 StateFlow(纯 Kotlin、操作符丰富、与协程统一),需配 repeatOnLifecycle 做生命周期安全收集;老项目/简单场景 LiveData 自带生命周期感知更省事。
Q5:Room 相比直接用 SQLite 的优势? 编译期校验 SQL、减少样板代码、对象映射、原生支持协程和 Flow(数据变化自动推送)、迁移管理。
Q6:为什么用 DataStore 替代 SharedPreferences? SP 有主线程 IO 风险、apply 仍可能阻塞、无类型安全、无错误信号。DataStore 基于协程+Flow,异步、事务、类型安全(Proto)。
Q7:Hilt 和 Dagger 关系?DI 解决什么问题? Hilt 是 Dagger 的 Android 封装,预置组件层级和注入点。DI 解决对象创建与依赖管理:解耦、便于替换实现做单元测试、避免手动 new 的耦合。
Q8:什么任务适合 WorkManager?和协程/Service 怎么区分? 适合“可延迟但必须保证执行“的后台任务(日志上传、数据同步),即使进程被杀/重启也能续。即时 UI 相关异步用协程;需要持续运行的用前台 Service。
Q9:Navigation 的回退栈怎么理解? NavController 根据 NavGraph 导航时会把目的地包装成 NavBackStackEntry 入栈,entry 携带参数、生命周期和状态。返回时 popBackStack 弹出当前 entry 并恢复上一个;登录流、首页清栈要用 popUpTo/launchSingleTop 控制,避免重复页面和错误返回路径。
Q10:Paging3 的三件套是什么?RemoteMediator 解决什么? PagingSource 负责按 key 加载一页数据,Pager 把它包装成 PagingData 流,UI 用 PagingDataAdapter 或 Compose LazyPagingItems 渲染。RemoteMediator 用于网络 + 本地数据库:网络结果写入 Room,UI 从 Room 读,实现离线缓存和单一数据源。
进阶补充:Hilt、Room、WorkManager 与 Compose 生命周期
Hilt 组件层级与作用域
常见层级:SingletonComponent → ActivityRetainedComponent → ViewModelComponent → ActivityComponent → FragmentComponent。作用域要和生命周期匹配,不要把短生命周期对象注入成单例。
Qualifier 与 Assisted Injection
同类型多个实现用 @Qualifier 区分;运行时参数可用 assisted injection 或 ViewModel 的 SavedStateHandle。
Room 迁移与 TypeConverter
Room migration 要显式描述 schema 变化,不能依赖删除重建。复杂类型用 TypeConverter,关系查询要注意 N+1 和事务一致性。
collectAsStateWithLifecycle
Compose 收集 Flow 推荐用 lifecycle-aware API,避免页面不可见时仍持续收集。
WorkManager 约束和退避
WorkManager 适合可延迟、需保证执行的后台任务。约束包括网络、充电、空闲;失败可配置 backoff 和 unique work policy。
**追问:**为什么 Hilt scope 不能乱用?因为对象生命周期过长会造成泄漏,过短会导致重复创建和状态丢失。