Search code examples
androidandroid-fragmentsandroid-lifecycleandroid-livedata

Remove item using LiveData and ViewModel causes re-emitting


I have a fragment showing a list of items, observing from view model (from a http service, they are not persisted in database). Now, I need to delete one of those items. I have a delete result live data so the view can observe when an item has been deleted.

Fragment

fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    //...
    viewModel.deleteItemLiveData.observe(viewLifecycleOwner) {
        when (it.status) {
            Result.Status.ERROR -> showDeletingError()
            Result.Status.SUCCESS -> {
                itemsAdapter.remove(it.value)
                commentsAdapter.notifyItemRemoved(it.value)
            }
        }
    }

}

fun deleteItem(itemId: String, itemIndex: Int) = lifecycleScope.launch {
    viewModel.deleteItem(itemId, itemIndex) 
}

ViewModel

val deleteItemLiveData = MutableLiveData<Result<Int>>()

suspend fun deleteItem(itemId: String, itemIndex: Int) = withContext(Dispatchers.IO) {
    val result = service.deleteItem(itemId)
    withContext(Dispatchers.Main) {
        if (result.success) {
            deleteItemLiveData.value = Result.success(itemIndex)
        } else {
            deleteItemLiveData.value = Result.error()
        }
    }
}

It is working fine, but the problem comes when I navigate to another fragment and go back again. deleteItemLIveData is emitted again with the last Result, so fragment tries to remove again the item from the adapter, and it crashes.

How con I solve this?


Solution

  • Rather than deleting an individual item from the adapter, it would make sense to update the original source of LiveData<List> since the view observes that list.

    The item repository should handle deletions, removing that item from the LiveData<List> which in turns propagates the update to the view and then the adapter.

    Repo might look something like this...

    fun deleteItem(item: Item): Result {
        val updated = items.value
        updated.remove(item)
        items.postValue(updated)
        . . .
        // propagate result of success/failure back to the view
    }
    
    fun observeItems() = items
    

    In your fragment you would get immediate updates from a single LiveData source

    fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        viewModel.observeItems().observe(viewLifecycleOwner) {
                 itemsAdapter.update(it) //use DiffUtil to update list or notifyDataSetChanged
            }
        }
    }
    

    Showing errors should be contextual, a toast message or some visual notification.

    Update: Handle error in deletion might look like this, off the top of my head...

    suspend fun deleteItem(itemId: String, itemIndex: Int): Result = withContext(Dispatchers.IO) {
        val result = service.deleteItem(itemId)
        withContext(Dispatchers.Main) {
            if (result.success) {
                // push updated list to items
                val updated = items.value
                updated.remove(item)
                items.postValue(updated)
                Result.Success()
            } else {
                Result.error()
            }
        }
    }