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互操作:新老代码共存的艺术
Jetpack 架构组件(下) Paging3 分页加载 Hilt 依赖注入 Room + Flow Compose ↔ View PagingSource PagingData PagingConfig @HiltViewModel @Module / @Provides @Singleton Flow DAO collect / stateIn 协程 + 事务 AndroidView ComposeView 生命周期桥接 目标:构建可维护、可测试、高性能的现代Android应用

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的onCreateonResumeonPause等生命周期方法中做对应操作。如果你在Compose里直接用,可能会遇到地图不显示或者内存泄漏的问题。

我的做法是:在AndroidViewfactory里注册生命周期观察者,或者在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

公众号:蓝海资料掘金营,微信 deep3321