Toast Message keeps appearing when orientation changes

78 views Asked by At

So, I am trying to use the Event class for handling one-time events where if I make an API call in onCreate() of my activity and observe the view model's Livedata variable, I am showing a Toast message in the observer.

open class Event<out T>(private val content: T) {

    var hasBeenHandled = false
        private set // Allow external read but not write

    /**
     * Returns the content and prevents its use again.
     */
    fun getContentIfNotHandled(): T? {
        return if (hasBeenHandled) {
            null
        } else {
            hasBeenHandled = true
            content
        }
    }

    /**
     * Returns the content, even if it's already been handled.
     */
    fun peekContent(): T = content
}
class CustomActivity : AppCompatActivity() {

viewModel.getTownsList(InputModel(1))

bindObservers()

}

 private fun bindObservers() {

        viewModel.townsList.observe(this) { event ->

            event.getContentIfNotHandled()?.let {

                when (it) {
                    is NetworkResult.Error -> Toast.makeText(this, "Error", Toast.LENGTH_SHORT)
                        .show()

                    is NetworkResult.Loading -> Toast.makeText(this, "Loading", Toast.LENGTH_SHORT)
                        .show()

                    is NetworkResult.Success -> {

                        Toast.makeText(this, "Success", Toast.LENGTH_SHORT).show()
                        townsAdapter.townsList = it.data!!
                    }
                }
            }
        }
    }

@HiltViewModel
class TownViewModel @Inject constructor(private val townRepository: TownRepository): ViewModel() {

    private var _townsList = MutableLiveData<Event<NetworkResult<List<Towns>>>>()
    val townsList get() = _townsList
    fun getTownsList(inputModel: InputModel) = viewModelScope.launch (Dispatchers.IO) {

        _townsList.postValue(Event(NetworkResult.Loading()))
        val response = townRepository.getTowns(inputModel)
        if (response.isSuccessful && response.body() != null){

            _townsList.postValue(Event(NetworkResult.Success(response.body()!!)))

        }
        else if (response.errorBody() != null){
            _townsList.postValue(Event(NetworkResult.Error(response.message())))
        }

    }

}

But the toast message keeps appearing when orientation changes, I am wondering if wrapping Event class with my livedata variable is a good solution? if it is what am I missing?

2

There are 2 answers

2
Gabe Sechan On

Whenever you rotate the device, the old Activity is destroyed and a new one is created. That new one, when drawing, will call bind again. That means it will display the toast again. The solution to this is in two steps:

1)Put the result of the network request in the ViewModel, so that you don't remake the request if the Activity is restarted.

2)Put the toast in the handler for the network request, not in the drawing code. Th toast is something you do in response to the incoming event, not in response to a state update causing a redraw.

2
Parniyan On

The reason of showing text by each orientation change as mentioned is Activity lifecycle change. If you want to use live data for this purpose, bear in mind that LiveData instance is not cleared when the activity is recreated. You can try this:

open class Event<out T>(private val content: T) {

    private var hasBeenHandled = false

    /**
     * Returns the content and prevents its use again.
     */
    fun getContentIfNotHandled(): T? {
        return if (hasBeenHandled) {
            null
        } else {
            hasBeenHandled = true
            content
        }
    }

    /**
     * Returns the content, even if it's already been handled.
     */
    fun peekContent(): T = content

    /**
     * Clears the event, allowing it to be handled again.
     */
    fun clear() {
        hasBeenHandled = false
    }
}

The clear() method ensures you that the event is cleared and won't be triggered again after an orientation change.

private fun bindObservers() {
    viewModel.townsList.observe(this) { event ->
        event.getContentIfNotHandled()?.let { networkResult ->
            when (networkResult) {
                is NetworkResult.Error -> Toast.makeText(this, "Error", Toast.LENGTH_SHORT).show()
                is NetworkResult.Loading -> Toast.makeText(this, "Loading", Toast.LENGTH_SHORT).show()
                is NetworkResult.Success -> {
                    Toast.makeText(this, "Success", Toast.LENGTH_SHORT).show()
                    townsAdapter.townsList = networkResult.data!!
                }
            }
            event.clear()
        }
    }
}

In your TownViewModel, you can clear the townsList LiveData when the ViewModel is cleared:

class TownViewModel @Inject constructor(private val townRepository: TownRepository): ViewModel() {
    private val _townsList = MutableLiveData<Event<NetworkResult<List<Towns>>>>()
    val townsList: LiveData<Event<NetworkResult<List<Towns>>>>
        get() = _townsList

    // ...

    override fun onCleared() {
        super.onCleared()
        _townsList.value = null // Clear the LiveData to prevent observers from receiving events after ViewModel is cleared
    }
}