Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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:观察生命周期事件,把生命周期相关逻辑(如开始/停止定位)从组件中解耦出去。
  • 现代用 DefaultLifecycleObserverlifecycleScope.launch { repeatOnLifecycle(STATE) { } }

三、LiveData

  • 生命周期感知的可观察数据持有者:只在活跃状态(STARTED/RESUMED)通知观察者,组件销毁自动移除观察,避免泄漏和崩溃
  • setValue(主线程)/ postValue(任意线程)。
  • 粘性问题:新观察者会立即收到最新值,用于“事件“场景会重复触发(用 SingleLiveEvent/Flow 替代)。
  • MediatorLiveData:合并多个源。
  • 现代趋势:新项目用 StateFlow/SharedFlow 替代,但老项目仍大量使用,必须懂。

四、Room

  • SQLite 之上的 ORM,编译期校验 SQL。
  • 三要素:@Entity(表)、@Dao(数据访问接口)、@Database(数据库持有者)。
  • 支持 协程 suspendFlow 返回(数据变化自动推送)。
  • 数据库迁移 Migration:版本升级写 Migration,否则崩溃;开发期可 fallbackToDestructiveMigration(清库)。
  • 查询返回 Flow 时,表数据变化会自动重新发射,天然支持响应式 UI。

五、Navigation

  • 定位:单 Activity 多 Fragment/Composable 架构的导航框架,把“目的地、参数、Action、Deep Link、回退栈”集中声明,减少手写 FragmentTransaction/Intent flag 的分散逻辑。
  • 对象关系:
    • NavHost:承载导航内容的容器,Fragment 场景常见 NavHostFragment,Compose 场景是 NavHost Composable。
    • 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 降低风险。
  • 登录、首页、详情都压入同一栈后不清理,返回路径混乱;需要明确 popUpToinclusive
  • 把导航事件放进持久 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:接收 PagingConfigpagingSourceFactory,把分页请求包装成 Flow&lt;PagingData&lt;T&gt;&gt;
    • 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 用于网络 + 数据库单一数据源” → 最后讲 cachedInLoadState、刷新 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 组件层级与作用域

常见层级:SingletonComponentActivityRetainedComponentViewModelComponentActivityComponentFragmentComponent。作用域要和生命周期匹配,不要把短生命周期对象注入成单例。

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 不能乱用?因为对象生命周期过长会造成泄漏,过短会导致重复创建和状态丢失。