22、Jetpack架构组件(下):Paging3分页加载、Hilt依赖注入、Room与Flow结合、Compose与View互操作
好,咱们接着聊Jetpack。上一章我们把LiveData、ViewModel、Room这些基础组件过了一遍。这一章,我打算把剩下的几个硬骨头啃下来——Paging3、Hilt、Room+Flow,还有Compose和View的互操作。
说实话,这几个东西在实际项目里碰到的频率极高。尤其是Paging3和Hilt,几乎成了现代Android开发的标配。我个人习惯是把它们当成一套组合拳来用——Room存数据,Flow做响应式,Paging3管分页,Hilt管依赖。你想想看,这一套下来,代码干净得跟刚擦过的白板似的。
本章核心脉络:
- Paging3:告别手动分页,拥抱声明式加载
- Hilt:依赖注入的终极形态,告别Dagger的繁琐
- Room + Flow:响应式数据库操作的最佳拍档
- Compose与View互操作:新老代码共存的艺术
22.1 Paging3:分页加载,这次真的省心了
先说说Paging3。我记得以前做分页加载,那叫一个痛苦——自己写RecyclerView的滚动监听,手动拼接列表数据,还要处理加载状态、重试逻辑、空状态……代码量巨大,而且容易出bug。尤其是快速滑动时,数据错乱的问题,我调试过整整一个下午。
Paging3的出现,说白了就是Google替我们把这些脏活累活全干了。你只需要定义好数据源,剩下的交给它。
核心概念:PagingSource
PagingSource是Paging3的基石。它定义了如何从数据源加载数据。我习惯把它理解成一个「数据水龙头」——你告诉它从哪一页开始,它给你一页数据,然后告诉你下一页的key是什么。
class UserPagingSource(
private val api: UserApi,
private val query: String
) : PagingSource<Int, User>() {
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, User> {
return try {
val page = params.key ?: 1
val response = api.searchUsers(query, page, params.loadSize)
LoadResult.Page(
data = response.users,
prevKey = if (page == 1) null else page - 1,
nextKey = if (response.users.size < params.loadSize) null else page + 1
)
} catch (e: Exception) {
LoadResult.Error(e)
}
}
override fun getRefreshKey(state: PagingState<Int, User>): Int? {
return state.anchorPosition?.let { anchorPosition ->
state.closestPageToPosition(anchorPosition)?.prevKey?.plus(1)
?: state.closestPageToPosition(anchorPosition)?.nextKey?.minus(1)
}
}
}
我的经验:getRefreshKey这个方法很容易被忽略。如果不实现,下拉刷新时可能会从第一页重新加载,用户体验很差。我曾经踩过这个坑,后来养成了习惯——每次写PagingSource都顺手把getRefreshKey实现掉。
在ViewModel中使用
Paging3和Flow天生一对。在ViewModel里,我们这样用:
@HiltViewModel
class UserListViewModel @Inject constructor(
private val userRepository: UserRepository
) : ViewModel() {
val users: Flow<PagingData<User>> = Pager(
config = PagingConfig(
pageSize = 20,
enablePlaceholders = false,
prefetchDistance = 5
),
pagingSourceFactory = { userRepository.getPagingSource() }
).flow.cachedIn(viewModelScope)
}
注意这个cachedIn(viewModelScope),它会把分页数据缓存在ViewModel的作用域里。如果不加,每次重新collect都会重新请求网络,那体验就太糟糕了。
22.2 Hilt:依赖注入,从入门到真香
说到Hilt,我得坦白——我一开始是抵触的。Dagger的学习曲线太陡了,我花了两周才搞明白@Component和@Module的关系。但Hilt不一样,它把Dagger的复杂度封装得死死的,你只需要记住几个注解就行。
三步走:安装、模块、注入
第一步,在Application类上加上@HiltAndroidApp:
@HiltAndroidApp
class MyApplication : Application()
第二步,定义Module,告诉Hilt怎么创建对象:
@Module
@InstallIn(SingletonComponent::class)
object AppModule {
@Provides
@Singleton
fun provideOkHttpClient(): OkHttpClient {
return OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS)
.build()
}
@Provides
@Singleton
fun provideRetrofit(client: OkHttpClient): Retrofit {
return Retrofit.Builder()
.baseUrl("https://api.example.com/")
.client(client)
.addConverterFactory(GsonConverterFactory.create())
.build()
}
}
第三步,在Activity或Fragment里注入:
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
@Inject
lateinit var viewModelFactory: ViewModelFactory
private val viewModel: MainViewModel by viewModels { viewModelFactory }
}
注意:Hilt的注入是编译时处理的。如果你看到lateinit var报错说未初始化,先检查一下有没有加@AndroidEntryPoint。我刚开始用的时候,这个错误至少犯了5次。
HiltViewModel:ViewModel的注入更优雅
我个人最喜欢的是@HiltViewModel注解。它让ViewModel的构造注入变得极其简单:
@HiltViewModel
class MainViewModel @Inject constructor(
private val userRepository: UserRepository,
private val savedStateHandle: SavedStateHandle
) : ViewModel() {
// ...
}
在Activity里直接拿:
private val viewModel: MainViewModel by viewModels()
不需要ViewModelFactory了,Hilt自动帮你搞定。嗯,这才是现代Android开发该有的样子。
22.3 Room + Flow:响应式数据库操作
Room和Flow的结合,是我在项目中用得最爽的组合之一。以前用LiveData,虽然也能观察数据变化,但LiveData不支持背压,也不方便做复杂的操作符变换。Flow就不一样了——它天然支持协程,操作符丰富,还能和Paging3无缝衔接。
DAO中返回Flow
@Dao
interface UserDao {
@Query("SELECT * FROM users ORDER BY name ASC")
fun getAllUsers(): Flow<List<User>>
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertUsers(users: List<User>)
@Query("DELETE FROM users")
suspend fun deleteAll()
}
注意看,getAllUsers()返回的是Flow<List<User>>。这意味着每次数据库里的users表发生变化,Flow都会自动发射新的数据。你想想看,这比手动刷新不知道高到哪里去了。
在ViewModel中收集Flow
@HiltViewModel
class UserViewModel @Inject constructor(
private val userDao: UserDao
) : ViewModel() {
val users: StateFlow<List<User>> = userDao.getAllUsers()
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = emptyList()
)
}
stateIn把Flow转成了StateFlow,这样UI层就可以通过collectAsState()来观察了。WhileSubscribed(5000)的意思是,如果UI层5秒内没有订阅者,就停止收集。这个设计很巧妙——比如屏幕旋转时,不会重复创建数据库查询。
避坑指南:我曾经在DAO里写了一个复杂的JOIN查询,返回Flow<List<UserWithOrders>>。结果每次更新任意一个关联表,整个Flow都会重新发射。后来我用了distinctUntilChanged()操作符,才避免了不必要的UI刷新。记住,Flow的响应式是「有变化就通知」,但有时候变化不是你关心的。
22.4 Compose与View互操作:新老代码共存的艺术
最后聊聊Compose和View的互操作。说实话,现在很多项目都是混合状态——新功能用Compose写,老功能还是View。怎么让它们和平共处?Google给了两个方案。
在View中使用Compose:ComposeView
如果你有一个旧的XML布局,想在里面嵌入一个Compose组件,用ComposeView:
// 在Activity或Fragment中
binding.composeContainer.setContent {
MaterialTheme {
Greeting("来自Compose的问候")
}
}
或者在XML里直接声明:
<androidx.compose.ui.platform.ComposeView
android:id="@+id/composeContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
在Compose中使用View:AndroidView
反过来,如果你在Compose里想用老牌的View组件(比如MapView、WebView),用AndroidView:
@Composable
fun MapViewComposable() {
AndroidView(
factory = { context ->
MapView(context).apply {
// 初始化MapView
}
},
update = { mapView ->
// 更新MapView的状态
}
)
}
生命周期问题:这是最容易出坑的地方。Compose的生命周期和View的生命周期是两套体系。比如MapView,它需要在Activity的onCreate、onResume、onPause等生命周期方法中做对应操作。如果你在Compose里直接用,可能会遇到地图不显示或者内存泄漏的问题。
我的做法是:在AndroidView的factory里注册生命周期观察者,或者在Activity层面手动管理。嗯,这个确实有点麻烦,但为了兼容老代码,值得。
互操作的最佳实践
| 场景 | 推荐方案 | 注意事项 |
|---|---|---|
| 旧XML布局嵌入Compose | ComposeView | 注意setContent的调用时机,避免在onCreate之前调用 |
| Compose中使用老View | AndroidView | 处理好View的生命周期,尤其是MapView、WebView |
| 在Fragment中使用Compose | ComposeView + Fragment | Fragment的onCreateView中返回ComposeView即可 |
| 在Compose中使用Fragment | 不推荐,建议重构 | 如果必须,可以用AndroidView包裹FragmentContainerView |
好了,这一章的内容就到这里。Paging3、Hilt、Room+Flow、Compose与View互操作,这四个东西单独拿出来都不算难,但组合在一起,就能构建出非常优雅的现代Android应用。我个人觉得,掌握好它们,你的Android开发水平会上一个大台阶。
本章小结:
- Paging3用PagingSource定义数据源,用Pager创建分页流,别忘了cachedIn
- Hilt用@HiltAndroidApp、@Module、@Inject三步搞定依赖注入
- Room + Flow实现响应式数据库,stateIn把Flow转成StateFlow供UI使用
- Compose与View互操作:ComposeView嵌入Compose,AndroidView嵌入View