ViewModel refetches data when fragment is recreated
L

4

14

I am using Bottom Navigation with Navigation Architecture Component. When the user navigates from one item to another(via Bottom navigation) and back again view model call repository function to fetch data again. So if the user goes back and forth 10 times the same data will be fetched 10 times. How to avoid re-fetching when the fragment is recreated data is already there?.

Fragment

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)

Repository

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

Navigation 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

Lissome answered 30/5, 2019 at 12:43 Comment(10)
Can you provide how you're ViewModelProvider.Factory creates instances?Indigoid
@JeelVankhede I have added it.Lissome
Put a breakpoint on repository.getProducts(it.filters, it.limit) and see when it is getting called. My guess is that the Navigation component is creating 10 instances of HomeFragment in your scenario, and each would get its own viewmodel.Hildegard
Okay, one solution would be that avoid fetching data from fragment and fetch it on activity once instead (As you're already using activity context for ViewModelProviders) and then observe it using LiveData inside fragment.Indigoid
@JeelVankhede I have a lot of fragments which every each will need a certain amount of data at some point. So I am avoiding fetching extra data user will never need. And I am using GraphQL on server, I think to avoid data over fetching is one of the reason it was created.Lissome
This line looks miscellaneous to me: try ViewModelProviders.of(this@run, viewModelFactory).get(ProductsViewModel::class.java)Indigoid
@Hildegard this repository.getProducts(it.filters, it.limit) get called every time I fragment is seen. Its my first time to use breakpoint so I don't know If I use it collectly but I can just tell It is called because when app launches thread is suspended till I press play (This fragment is Home). When I navigate to another fragment and comes back thread is suspended again. I hope I did it correctly! so what does that mean? Thanks in advanceLissome
"When I navigate to another fragment and comes back thread is suspended again" -- in the debugger, you will see a call stack when your breakpoint is hit (by default, it is on the left side of the Debug tool). See what lines of your code are causing the breakpoint to be called. If it is because a new instance of ProductsViewModel is being created each time, then I think my earlier guess is correct: the Navigation component is creating 10 instances of HomeFragment in your scenario, and each would get its own viewmodel.Hildegard
@JeelVankhede Thank for response. But It doesn't help. Still data is refetched.Lissome
@Hildegard I have added screenshot of debugger please take a look incase I am missing something. I navigate 4 times and got 8 hits, My observation is setInput in productsViewModel, getData in HomeFragment make it called.Lissome
H
6

In onActivityCreated(), you are calling getData(). In there, you have:

productsViewModel.setInput(productsFilters, 2)

This, in turn, changes the value of _input in your ProductsViewModel. And, every time that _input changes, the getProducts lambda expression will be evaluated, calling your repository.

So, every onActivityCreated() call triggers a call to your repository.

I do not know enough about your app to tell you what you need to change. Here are some possibilities:

  • Switch from onActivityCreated() to other lifecycle methods. initViewModel() could be called in onCreate(), while the rest should be in onViewCreated().

  • Reconsider your getData() implementation. Do you really need to call setInput() every time we navigate to this fragment? Or, should that be part of initViewModel() and done once in onCreate()? Or, since productsFilters does not seem to be tied to the fragment at all, should productsFilters and the setInput() call be part of the init block of ProductsViewModel, so it only happens once?

Hildegard answered 30/5, 2019 at 14:4 Comment(7)
What should I use in this line of code productsViewModel.products.observe(--WHAT-HERE--,....** this** or this.activity or **viewLifecycleOwner?Lissome
@Nux: That would stay where it is. The first two statements in getData() (the productsFilters declaration and setInput() call, though, do not depend on the fragment, and so they could move to be part of the init block of the viewmodel.Hildegard
I have done as you mentioned Thanks now there is no re-fetch, The only problem remained is that viewLifecycleOwner when is referred in onCreate causes app to crash. I have changed it to this and When I move from one item to another in Buttom nav menu everything is fine. But when I click product title to go to product details fragment(via directions, Implemented in nav Editor) and come back data is lost. What could cause this?Lissome
@Nux: I have no idea, sorry. You might consider asking a separate Stack Overflow question, showing your now-current code and explain your symptoms there.Hildegard
@Lissome Doesn't onCreate get called every time you navigate back to your fragment?? I'm facing a similar situation.Nystrom
@AlokBharti I already finished the app that had this problem a long time ago, I think I don't have certain answer to your question right nowLissome
If possible, can you help me here? How do you manage to prevent calling viewmodel's function which contains API call whenever you navigate back to the fragment??Nystrom
G
7

One simple solution would be to change the ViewModelProvider owner from this to requireActivity() in this line of code:

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

Therefore, as the activity is the owner of the viewmodel and the lifcycle of viewmodel attached to the activity not to the fragment, navigating between fragments within the activity won't recreated the viewmodel.

Gt answered 29/6, 2020 at 4:32 Comment(0)
H
6

In onActivityCreated(), you are calling getData(). In there, you have:

productsViewModel.setInput(productsFilters, 2)

This, in turn, changes the value of _input in your ProductsViewModel. And, every time that _input changes, the getProducts lambda expression will be evaluated, calling your repository.

So, every onActivityCreated() call triggers a call to your repository.

I do not know enough about your app to tell you what you need to change. Here are some possibilities:

  • Switch from onActivityCreated() to other lifecycle methods. initViewModel() could be called in onCreate(), while the rest should be in onViewCreated().

  • Reconsider your getData() implementation. Do you really need to call setInput() every time we navigate to this fragment? Or, should that be part of initViewModel() and done once in onCreate()? Or, since productsFilters does not seem to be tied to the fragment at all, should productsFilters and the setInput() call be part of the init block of ProductsViewModel, so it only happens once?

Hildegard answered 30/5, 2019 at 14:4 Comment(7)
What should I use in this line of code productsViewModel.products.observe(--WHAT-HERE--,....** this** or this.activity or **viewLifecycleOwner?Lissome
@Nux: That would stay where it is. The first two statements in getData() (the productsFilters declaration and setInput() call, though, do not depend on the fragment, and so they could move to be part of the init block of the viewmodel.Hildegard
I have done as you mentioned Thanks now there is no re-fetch, The only problem remained is that viewLifecycleOwner when is referred in onCreate causes app to crash. I have changed it to this and When I move from one item to another in Buttom nav menu everything is fine. But when I click product title to go to product details fragment(via directions, Implemented in nav Editor) and come back data is lost. What could cause this?Lissome
@Nux: I have no idea, sorry. You might consider asking a separate Stack Overflow question, showing your now-current code and explain your symptoms there.Hildegard
@Lissome Doesn't onCreate get called every time you navigate back to your fragment?? I'm facing a similar situation.Nystrom
@AlokBharti I already finished the app that had this problem a long time ago, I think I don't have certain answer to your question right nowLissome
If possible, can you help me here? How do you manage to prevent calling viewmodel's function which contains API call whenever you navigate back to the fragment??Nystrom
C
0

When you select other pages via bottom navigation and come back, fragment destroy and recreate. So the onCreate, onViewCreated and onActivityCreate will run again. But viewModel is still alive.

So you can call your function (getProducts) inside the "init" in viewModel to run it once.

init {
        getProducts()
    }
Cashew answered 22/6, 2020 at 4:24 Comment(1)
This is practically possible in all cases, assume you have parameter passed to fragment, multiple network call :(. Code becomes more messy.Contortion
H
0

define your ProductsViewModel by static in mainActivity and initialize in onCreate method. Now just use it this way in fragment:

MainActivity.productsViewModel.products.observe(viewLifecycleOwner, Observer {
            it.products()?.let { products -> productsAdapter.setData(products) }
        })
Hintze answered 5/11, 2020 at 14:54 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.