Search code examples
androidkotlinandroid-fragmentskotlin-coroutinesandroid-viewmodel

ViewModel Fragment Recreates On Screen Rotation


I'm building an application with latest android architecture components. I'm using firebase firestore as a database with jetpack navigation(Bottom nav). I'm successfully able to display data from DB. But Whenever I rotate mt screen the store fragment recreates & makes request to DB.

Repo

    override fun getAllStores() = callbackFlow<State<List<Store>>> {
    // Emit loading state
    send(State.loading())

    val listener = remoteDB.collection(Constants.COLLECTION_STORES)
        .addSnapshotListener { querySnapshot, exception ->
            querySnapshot?.toObjects(Store::class.java)?.let { store ->
                // Emit success state with data
                offer(State.success(store))
            }
            exception?.let {
                // emit exception with message
                offer(State.failed(it.message!!))
                cancel()
            }
        }
    awaitClose {
        listener.remove()
        cancel()
    }

}.catch {
    // Thrown exception on State Failed
    emit(State.failed(it.message.toString()))
}.flowOn(Dispatchers.IO)

ViewModel

@ExperimentalCoroutinesApi
@InternalCoroutinesApi
class StoreViewModel(private val repository: DBInterface = Repo()) : ViewModel() {

fun getAllStores() = repository.getAllStores()

}

Store Fragment

@ExperimentalCoroutinesApi
@InternalCoroutinesApi
class StoreFragment : Fragment(R.layout.fragment_store) {
private lateinit var storeAdapter: StoreAdapter
private val viewModel: StoreViewModel by viewModels()

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)
    (activity as MainActivity).supportActionBar?.title = getString(R.string.store_title)

    setUpRV()

    // get all stores
    lifecycleScope.launch {
        getAllStores()
    }

}

private suspend fun getAllStores() {
    viewModel.getAllStores().collect { state ->
        when (state) {
            is State.Loading -> {
                store_progress.show()
            }

            is State.Success -> {
                storeAdapter.differ.submitList(state.data)
                store_progress.animate().alpha(0f)
                        .withEndAction {
                            store_rv.animate().alpha(1f)
                            store_progress.hide()
                        }

            }
            is State.Failed -> {
                store_progress.hide()
                activity?.toast("Failed! ${state.message}")
            }
        }
    }
}

private fun setUpRV() {
    storeAdapter = StoreAdapter()
    store_rv.apply {
        adapter = storeAdapter
        addItemDecoration(SpacesItemDecorator(16))
    }
}

}

Main activity(Nav graph)

@InternalCoroutinesApi
@ExperimentalCoroutinesApi
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
    setSupportActionBar(toolbar)

    // init bottom navigation
    bottom_navigation.setupWithNavController(nav_host_fragment.findNavController())
   
}
}

Every time it recreates my fragment. I don't want to save or retain any views using methods. Because ViewModel used to protect view on screen rotation. Kindly let me know any tips & tricks. Thanks in advance ;)


Solution

  • Flow in itself is not stateful - that is a key difference between it and LiveData. That means that after your collect completes, the next collect starts the callbackFlow from scratch.

    This is precisely why the lifecycle-livedata-ktx artifact contains the asLiveData() extension that allows you to continue to use a Flow at the repository layer while maintaining the stateful (and Lifecycle) properties of LiveData for your UI:

    @ExperimentalCoroutinesApi
    @InternalCoroutinesApi
    class StoreViewModel(private val repository: DBInterface = Repo()) : ViewModel() {
        fun getAllStores() = repository.getAllStores().asLiveData()
    }
    

    You'd change your UI code to continue to use LiveData and observe().

    Kotlin is working on a shareIn operation that would allow your ViewModel to save the state of a Flow. That would allow you to use Flow at all layers of your app without requerying information from scratch when the Fragment/Activity that is calling collect gets destroyed and recreated.