Search code examples
androidkotlindata-bindingandroid-databindingandroid-architecture-navigation

Prevent my fragment and list from being created every time they are displayed


I'm using databinding and fragments with navigation component for the first time in my app. Everything works fine with regard to navigation and loading my list. The problem I have is this:

When I'm in my list and I click on one of the items, I navigate to the item's detail fragment. But when I return to the list (onBack), the fragment and the list are loaded again, they are re-created.

What I want to achieve is that the fragment and therefore the list are not recreated. That they remain in the same state, so that when I return from the detail I can continue to navigate my list from the point where I left off.

My code:

Fragment

 class ListFragment : Fragment() {

    private lateinit var viewDataBinding: FragmentListBinding
    private lateinit var adapter: ListAdapter

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
        viewDataBinding = FragmentListBinding.inflate(inflater, container, false).apply {
            viewmodel = ViewModelProviders.of(this@ListFragment).get(ListViewModel::class.java)
            lifecycleOwner = viewLifecycleOwner
        }
        return viewDataBinding.root
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        setupAdapter()
        setupObservers()
    }

    private fun setupObservers() {
        viewDataBinding.viewmodel?.listLive?.observe(viewLifecycleOwner, Observer {
            adapter.updateList(it)
        })

        viewDataBinding.viewmodel?.toastMessage?.observe(viewLifecycleOwner, Observer {
            activity?.longToast(it)
        })
    }

    private fun setupAdapter() {
        val viewModel = viewDataBinding.viewmodel

        if (viewModel != null) {
            adapter = ListAdapter(viewDataBinding.viewmodel!!)
            val layoutManager = LinearLayoutManager(activity)
            list_rv.layoutManager = layoutManager
            list_rv.adapter = adapter
        }
    }

    override fun onResume() {
        super.onResume()
        viewDataBinding.viewmodel?.fetchList()
    }
}

Fragment layout

<layout 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">

<data>

    <import type="android.view.View" />

    <variable
        name="viewmodel"
        type="app.example.list.ListViewModel" />
</data>

<RelativeLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent">

        <androidx.recyclerview.widget.RecyclerView
         android:id="@+id/list_rv"
         android:layout_width="match_parent"
         android:layout_height="match_parent"
         android:visibility="@{safeUnbox(viewmodel.dataLoading) ? View.GONE : View.VISIBLE}" />
          
</RelativeLayout>

View model

class ListViewModel : BaseViewModel() {
val listLive = MutableLiveData<List<Item>>()

fun fetchList() {
    dataLoading.value = true
    ItemRepository.getInstance().getList() { isSuccess, response ->
        dataLoading.value = false

        if (isSuccess) {
            listLive.value = response?.items
            empty.value = false
        } else {
            empty.value = true
        }
    }
}
}

I'm navigating between the list and the item with navigation

itemView.findNavController().navigate(R.id.action_listFragment_to_detailFragment)

Solution

  • You should never be reloading your data in a Lifecycle method such as onResume() - that's why your data goes back to loading every time you go back to the fragment.

    Instead, you should call fetchList() just once in your ViewModel's init block - this ensures that it runs only once:

    class ListViewModel : BaseViewModel() {
         val listLive = MutableLiveData<List<Item>>()
    
          init {
              fetchList()
          }
    
          fun fetchList() {
               dataLoading.value = true
               ItemRepository.getInstance().getList() { isSuccess, response ->
                    dataLoading.value = false
    
                    if (isSuccess) {
                         listLive.value = response?.items
                         empty.value = false
                    } else {
                         empty.value = true
                    }
               }
          }
     }
    

    It isn't clear how your getList() method works. How it is written now is generally correct for a single callback. If your getList() is actually returning a stream of values that continue to deliver callbacks as the underlying data changes, then you must make sure you unregister that callback in onCleared() (the callback when the ViewModel is destroyed).