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

Nux
  • 5,890
  • 13
  • 48
  • 74
  • Can you provide how you're `ViewModelProvider.Factory` creates instances? – Jeel Vankhede May 30 '19 at 12:46
  • @JeelVankhede I have added it. – Nux May 30 '19 at 12:49
  • 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. – CommonsWare May 30 '19 at 12:50
  • 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. – Jeel Vankhede May 30 '19 at 12:58
  • @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. – Nux May 30 '19 at 13:06
  • This line looks *miscellaneous* to me: try `ViewModelProviders.of(this@run, viewModelFactory).get(ProductsViewModel::class.java)` – Jeel Vankhede May 30 '19 at 13:14
  • @CommonsWare 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 advance – Nux May 30 '19 at 13:29
  • "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. – CommonsWare May 30 '19 at 13:39
  • @JeelVankhede Thank for response. But It doesn't help. Still data is refetched. – Nux May 30 '19 at 13:39
  • @CommonsWare 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. – Nux May 30 '19 at 13:56

4 Answers4

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.

khesam109
  • 510
  • 9
  • 16
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?

CommonsWare
  • 986,068
  • 189
  • 2,389
  • 2,491
  • What should I use in this line of code ``productsViewModel.products.observe(--WHAT-HERE--,....``** this** or **this.activity or **viewLifecycleOwner**? – Nux May 30 '19 at 14:11
  • @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. – CommonsWare May 30 '19 at 14:15
  • 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? – Nux May 30 '19 at 14:38
  • @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. – CommonsWare May 30 '19 at 14:41
  • @Nux Doesn't onCreate get called every time you navigate back to your fragment?? I'm facing a similar situation. – Alok Bharti May 28 '20 at 07:32
  • @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 now – Nux May 28 '20 at 15:59
  • 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?? – Alok Bharti May 28 '20 at 16:09
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()
    }
roghayeh hosseini
  • 676
  • 1
  • 8
  • 21
  • This is practically possible in all cases, assume you have parameter passed to fragment, multiple network call :(. Code becomes more messy. – Code_Life Jun 22 '20 at 18:28
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) }
        })
M.ghorbani
  • 129
  • 8