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)
}
}
}
ViewModelProvider.Factory
creates instances? – Indigoidrepository.getProducts(it.filters, it.limit)
and see when it is getting called. My guess is that the Navigation component is creating 10 instances ofHomeFragment
in your scenario, and each would get its own viewmodel. – HildegardViewModelProviders
) and then observe it usingLiveData
inside fragment. – IndigoidViewModelProviders.of(this@run, viewModelFactory).get(ProductsViewModel::class.java)
– Indigoidrepository.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 – LissomeProductsViewModel
is being created each time, then I think my earlier guess is correct: the Navigation component is creating 10 instances ofHomeFragment
in your scenario, and each would get its own viewmodel. – Hildegard