You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用Navigation架构组件+底部导航 避免ViewModel重复请求数据

解决Navigation Component底部导航切换时ViewModel重复请求数据的问题

我之前也碰到过一模一样的问题——用Navigation Component做底部导航,来回切换Fragment的时候,每次都会触发数据请求,次数多了不仅浪费资源,还可能导致UI抖动。咱们可以从Fragment生命周期控制、ViewModel缓存和Repository优化三个层面来解决:

1. 先阻止Fragment不必要的重建(最直接的第一步)

Navigation Component默认每次切换底部导航项时,都会销毁并重建目标Fragment。咱们可以给导航图里的每个Fragment加上app:launchSingleTop="true",这样切换时会复用已存在的Fragment实例,不会走销毁重建流程,自然就不会重复调用onActivityCreated里的getData()了。

修改你的main.xml:

<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/mobile_navigation.xml"
    app:startDestination="@id/home">

    <fragment
        android:id="@+id/home"
        android:name="com.nux.ui.home.HomeFragment"
        android:label="@string/title_home"
        app:launchSingleTop="true" <!-- 新增这行 -->
        tools:layout="@layout/fragment_home"/>
    <fragment
        android:id="@+id/search"
        android:name="com.nux.ui.search.SearchFragment"
        android:label="@string/title_search"
        app:launchSingleTop="true" <!-- 新增这行 -->
        tools:layout="@layout/fragment_search" />
    <fragment
        android:id="@+id/my_profile"
        android:name="com.nux.ui.user.MyProfileFragment"
        android:label="@string/title_profile"
        app:launchSingleTop="true" <!-- 新增这行 -->
        tools:layout="@layout/fragment_profile" />
</navigation>

2. ViewModel层缓存请求结果,避免相同输入重复调用Repository

即使Fragment还是会重建(比如配置变更),咱们也要确保ViewModel不会重复发起相同的请求。可以在ViewModel里加一个缓存Map,存储已经请求过的输入对应的结果,当输入相同时直接返回缓存,不用再调用Repository。

修改你的ProductsViewModel:

class ProductsViewModel @Inject constructor(private val repository: ProductsRepository) : ViewModel() {
    private val _input = MutableLiveData<PInput>()
    // 缓存输入参数与请求结果的映射
    private val inputResultCache = mutableMapOf<PInput, ApiResponse<ProductsQuery.Data>>()

    fun setInput(filters: ProductsFilters, limit: Int) {
        val newInput = PInput(filters, limit)
        // 只有当输入变化,或者缓存里没有该输入的结果时,才更新LiveData
        if (_input.value != newInput || !inputResultCache.containsKey(newInput)) {
            _input.value = newInput
        }
    }

    private val getProducts = map(_input) { input ->
        // 从缓存获取,没有则调用Repository请求并缓存
        inputResultCache.getOrPut(input) {
            repository.getProducts(input.filters, input.limit)
        }
    }

    val products = switchMap(getProducts) { it.data }
    val networkState = switchMap(getProducts) { it.networkState }
}

3. 修复Repository的NetworkState问题(避免状态混乱)

你的Repository里networkState是成员变量,这会导致所有请求共用同一个状态LiveData——比如多个请求同时发起时,状态会互相覆盖。咱们要改成每个请求创建独立的networkState:

修改你的ProductsRepository:

@Singleton
class ProductsRepository @Inject constructor(private val api: ApolloClient) {

    fun getProducts(filters: ProductsFilters, limit: Int): ApiResponse<ProductsQuery.Data> {
        val products = MutableLiveData<ProductsQuery.Data>()
        // 每个请求创建独立的NetworkState LiveData
        val networkState = MutableLiveData<NetworkState>()
        networkState.postValue(NetworkState.LOADING)
        
        val request = api.query(ProductsQuery
            .builder()
            .filters(filters)
            .limit(limit)
            .build())
            
        request.enqueue(object : ApolloCall.Callback<ProductsQuery.Data>() {
            override fun onFailure(e: ApolloException) {
                networkState.postValue(NetworkState.error(e.localizedMessage))
            }

            override fun onResponse(response: Response<ProductsQuery.Data>) = when {
                response.hasErrors() -> networkState.postValue(NetworkState.error(response.errors()[0].message()))
                else -> {
                    networkState.postValue(NetworkState.LOADED)
                    products.postValue(response.data())
                }
            }
        })
        return ApiResponse(data = products, networkState = networkState)
    }
}

额外优化:Fragment里的请求触发逻辑

如果还是担心Fragment重建时重复调用setInput,可以在Fragment的getData()里判断savedInstanceState——如果不为null(说明是重建而非首次创建),就不用再调用setInput:

修改HomeFragment的getData():

private fun getData() {
    val productsFilters = ProductsFilters.builder().sortBy(SortProductsBy.NEWEST).build()
    // 只有首次创建Fragment时才触发请求
    if (savedInstanceState == null) {
        productsViewModel.setInput(productsFilters, 2)
    }
    // 观察数据和状态的逻辑保持不变
    productsViewModel.products.observe(viewLifecycleOwner, Observer {
        it.products()?.let { products ->
            productsAdapter.setData(products)
        }
    })
    productsViewModel.networkState.observe(viewLifecycleOwner, Observer {
        //Todo showing progress bar
    })
}

这样一套组合拳下来,不管是底部导航切换还是配置变更,都不会重复发起不必要的网络请求了。

内容的提问来源于stack exchange,提问作者Nux

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.13 08:10:31