当片段重新创建时,ViewModel 会重新获取数据

14
我正在使用底部导航导航架构组件。当用户通过底部导航从一个项目导航到另一个项目,然后再返回时,视图模型将调用存储库函数重新获取数据。因此,如果用户来回切换10次,则会获取相同的数据10次。如何在片段重新创建数据已经存在的情况下避免重新获取数据?。 片段
class HomeFragment : Fragment() {

    @Inject
    lateinit var viewModelFactory: ViewModelProvider.Factory

    private lateinit var productsViewModel: ProductsViewModel
    private lateinit var productsAdapter: ProductsAdapter

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
                              savedInstanceState: Bundle?): View? {
        // Inflate the layout for this fragment
        return inflater.inflate(R.layout.fragment_home, container, false)
    }

    override fun onActivityCreated(savedInstanceState: Bundle?) {
        super.onActivityCreated(savedInstanceState)
        initViewModel()
        initAdapters()
        initLayouts()
        getData()
    }

    private fun initViewModel() {
        (activity!!.application as App).component.inject(this)

        productsViewModel = activity?.run {
            ViewModelProviders.of(this, viewModelFactory).get(ProductsViewModel::class.java)
        }!!
    }

    private fun initAdapters() {
        productsAdapter = ProductsAdapter(this.context!!, From.HOME_FRAGMENT)
    }

    private fun initLayouts() {
        productsRecyclerView.layoutManager = LinearLayoutManager(this.activity)
        productsRecyclerView.adapter = productsAdapter
    }

    private fun getData() {
        val productsFilters = ProductsFilters.builder().sortBy(SortProductsBy.NEWEST).build()

        //Products filters
        productsViewModel.setInput(productsFilters, 2)

        //Observing products data
        productsViewModel.products.observe(viewLifecycleOwner, Observer {
            it.products()?.let { products -> productsAdapter.setData(products) }
        })

        //Observing loading
        productsViewModel.networkState.observe(viewLifecycleOwner, Observer {
            //Todo showing progress bar
        })
    }
}

ViewModel

class ProductsViewModel
@Inject constructor(private val repository: ProductsRepository) : ViewModel() {

    private val _input = MutableLiveData<PInput>()

    fun setInput(filters: ProductsFilters, limit: Int) {
        _input.value = PInput(filters, limit)
    }

    private val getProducts = map(_input) {
        repository.getProducts(it.filters, it.limit)
    }

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

data class PInput(val filters: ProductsFilters, val limit: Int)

仓库

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

    val networkState = MutableLiveData<NetworkState>()

    fun getProducts(filters: ProductsFilters, limit: Int): ApiResponse<ProductsQuery.Data> {
        val products = MutableLiveData<ProductsQuery.Data>()

        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)
    }
}

导航主文件 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"
        tools:layout="@layout/fragment_home"/>
    <fragment
        android:id="@+id/search"
        android:name="com.nux.ui.search.SearchFragment"
        android:label="@string/title_search"
        tools:layout="@layout/fragment_search" />
    <fragment
        android:id="@+id/my_profile"
        android:name="com.nux.ui.user.MyProfileFragment"
        android:label="@string/title_profile"
        tools:layout="@layout/fragment_profile" />
</navigation>

ViewModelFactory

@Singleton
class ViewModelFactory @Inject
constructor(private val viewModels: MutableMap<Class<out ViewModel>, Provider<ViewModel>>) : ViewModelProvider.Factory {

    @Suppress("UNCHECKED_CAST")
    override fun <T : ViewModel> create(modelClass: Class<T>): T {
        val creator = viewModels[modelClass]
                ?: viewModels.asIterable().firstOrNull { modelClass.isAssignableFrom(it.key) }?.value
                ?: throw IllegalArgumentException("unknown model class $modelClass")
        return try {
            creator.get() as T
        } catch (e: Exception) {
            throw RuntimeException(e)
        }
    }
}

enter image description here


你能提供一下你的 ViewModelProvider.Factory 是如何创建实例的吗? - Jeel Vankhede
@JeelVankhede 我已经添加了它。 - Nux
@JeelVankhede 我有很多片段,每个片段在某个时刻都需要一定量的数据。因此,我避免获取用户永远不需要的额外数据。而且我在服务器上使用GraphQL,我认为避免数据过度获取是创建它的原因之一。 - Nux
@CommonsWare 这个 repository.getProducts(it.filters, it.limit) 每次我看到这个 fragment 的时候都会被调用。这是我第一次使用断点,所以我不知道我是否使用得正确,但我可以确定它被调用了,因为当应用程序启动时,线程会被挂起,直到我按下播放按钮(这个 fragment 是主页)。当我导航到另一个 fragment 并返回时,线程再次被挂起。我希望我做得对!那么这意味着什么?提前致谢。 - Nux
@CommonsWare 我已经添加了调试器的截图,请看一下以防我漏掉了什么。我导航了4次并获得了8个命中,我的观察是在productsViewModel中的setInput和在HomeFragment中的getData使其被调用。 - Nux
显示剩余5条评论
4个回答

7

一个简单的解决方案是将该行代码中的 ViewModelProvider 所属者从 this 更改为 requireActivity()

ViewModelProviders.of(this, viewModelFactory).get(ProductsViewModel::class.java)

因此,由于活动是视图模型的所有者,而视图模型的生命周期附着于活动而不是片段,因此在活动内导航片段时不会重新创建视图模型。

6

onActivityCreated() 中,您正在调用 getData()。 在此方法中,以下内容被执行:

productsViewModel.setInput(productsFilters, 2)

这反过来会改变您的ProductsViewModel中的_input的值。每次_input更改时,都将评估getProducts lambda表达式,调用您的存储库。
因此,每个onActivityCreated()调用都会触发对您的存储库的调用。
我不知道您的应用程序足够多的信息,无法告诉您需要更改什么。以下是一些可能性:
  • onActivityCreated()切换到其他生命周期方法。initViewModel()可以在onCreate()中调用,而其余部分应在onViewCreated()中。

  • 重新考虑您的getData()实现。每次导航到此片段时,是否真的需要调用setInput()?或者,这应该作为initViewModel()的一部分,在onCreate()中完成一次?或者,既然productsFilters似乎根本与片段无关,那么productsFilterssetInput()调用是否应该成为ProductsViewModelinit块的一部分,以便它只发生一次?


在这行代码“productsViewModel.products.observe(--WHAT-HERE--, ....”中,我应该使用this 还是 this.activity 或者 viewLifecycleOwner - Nux
@Nux:它将保持原样。但是,getData() 中的前两个语句(即 productsFilters 声明和 setInput() 调用)不依赖于片段,因此它们可以移动到视图模型的 init 块中。 - CommonsWare
@Nux,每次返回到你的Fragment时,onCreate方法不是都会被调用吗?我也遇到了类似的情况。 - Alok Bharti
@AlokBharti,我很久以前就完成了这个有问题的应用程序,我想现在我没有对你的问题给出确切的答案。 - Nux
如果可能的话,你能帮我一下吗?当你回到Fragment时,如何防止调用ViewModel中包含API调用的函数? - Alok Bharti
显示剩余2条评论

0

在mainActivity中使用static定义您的ProductsViewModel,并在onCreate方法中进行初始化。现在在fragment中可以这样使用:

MainActivity.productsViewModel.products.observe(viewLifecycleOwner, Observer {
            it.products()?.let { products -> productsAdapter.setData(products) }
        })

0

当您通过底部导航选择其他页面并返回时,片段会被销毁和重新创建。 因此,onCreate、onViewCreated和onActivityCreated将再次运行。但是viewModel仍然存在。

因此,您可以在viewModel的“init”中调用函数(getProducts)以便只运行一次。

init {
        getProducts()
    }

在所有情况下,这几乎都是可能的,假设您将参数传递给片段,并进行多个网络调用:(。代码变得更加混乱。 - Code_Life

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接