How to use Single Live Event to show toast in Kotlin

10.7k Views Asked by At

I want to use single live event class to show toast (like flag) Here is my code what I tried. I want to not use peding like flag. how do I fix it?

MainViewModel

class MainViewModel(private val movieRepository: MovieRepository) : ViewModel() {
    val keyword = MutableLiveData<String>()
    val movieList = MutableLiveData<List<Movie>>()
    val msg = MutableLiveData<String>()
    val pending: AtomicBoolean = AtomicBoolean(false)

    fun findMovie() {
        val keywordValue = keyword.value ?: return
        pending.set(true)
        if (keywordValue.isNullOrBlank()) {
            msg.value = "emptyKeyword"
            return
        }
        movieRepository.getMovieData(keyword = keywordValue, 30,
            onSuccess = {
                if (it.items!!.isEmpty()) {
                    msg.value = "emptyResult"
                } else {
                    msg.value = "success"
                    movieList.value = it.items
                }
            },
            onFailure = {
                msg.value = "fail"
            }
        )
    }
}

MainActivity

 private fun viewModelCallback() {
        mainViewModel.msg.observe(this, {
            if (mainViewModel.pending.compareAndSet(true, false)) {
                when (it) {
                    "success" -> toast(R.string.network_success)
                    "emptyKeyword" -> toast(R.string.keyword_empty)
                    "fail" -> toast(R.string.network_error)
                    "emptyResult" -> toast(R.string.keyword_result_empty)
                }
            }
        })
}
5

There are 5 best solutions below

0
On BEST ANSWER

Solution

Step 1. Copy the SingleLiveEvent.kt to your app

/*
 *  Copyright 2017 Google Inc.
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */

package com.example.myapp;

import android.util.Log;

import androidx.annotation.MainThread;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.Observer;

import java.util.concurrent.atomic.AtomicBoolean;

/**
 * A lifecycle-aware observable that sends only new updates after subscription, used for events like
 * navigation and Snackbar messages.
 * <p>
 * This avoids a common problem with events: on configuration change (like rotation) an update
 * can be emitted if the observer is active. This LiveData only calls the observable if there's an
 * explicit call to setValue() or call().
 * <p>
 * Note that only one observer is going to be notified of changes.
 */
public class SingleLiveEvent<T> extends MutableLiveData<T> {

    private static final String TAG = "SingleLiveEvent";

    private final AtomicBoolean mPending = new AtomicBoolean(false);

    @MainThread
    public void observe(@NonNull LifecycleOwner owner, @NonNull Observer<? super T> observer) {
        if (hasActiveObservers()) {
            Log.w(TAG, "Multiple observers registered but only one will be notified of changes.");
        }
        // Observe the internal MutableLiveData
        super.observe(owner, t -> {
            if (mPending.compareAndSet(true, false)) {
                observer.onChanged(t);
            }
        });
    }

    @MainThread
    public void setValue(@Nullable T t) {
        mPending.set(true);
        super.setValue(t);
    }

    /**
     * Used for cases where T is Void, to make calls cleaner.
     */
    @MainThread
    public void call() {
        setValue(null);
    }
}

Step 2. Use from your code.

MainViewModel

class MainViewModel(private val movieRepository: MovieRepository) : ViewModel() {
    val keyword = MutableLiveData<String>()
    val movieList = MutableLiveData<List<Movie>>()
    val msg = SingleLiveEvent<String>()

    fun findMovie() {
        val keywordValue = keyword.value ?: return
        if (keywordValue.isNullOrBlank()) {
            msg.value = "emptyKeyword"
            return
        }
        movieRepository.getMovieData(keyword = keywordValue, 30,
            onSuccess = {
                if (it.items!!.isEmpty()) {
                    msg.value = "emptyResult"
                } else {
                    msg.value = "success"
                    movieList.value = it.items
                }
            },
            onFailure = {
                msg.value = "fail"
            }
        )
    }
}

MainActivity

private fun viewModelCallback() {
    mainViewModel.msg.observe(this, {
        when (it) {
            "success" -> toast(R.string.network_success)
            "emptyKeyword" -> toast(R.string.keyword_empty)
            "fail" -> toast(R.string.network_error)
            "emptyResult" -> toast(R.string.keyword_result_empty)
        }
    })
}
0
On

SingleLiveEvent extends MutableLiveData. So, you can use it just like a normal MutableLiveData.

First, you need to include SingleLiveEvent.java class (https://github.com/android/architecture-samples/blob/dev-todo-mvvm-live/todoapp/app/src/main/java/com/example/android/architecture/blueprints/todoapp/SingleLiveEvent.java). Copy this class file and add it to your project.

You can set it like this in your ViewModel when you want to show toast,

SingleLiveEvent<String> toastMsg = new SingleLiveEvent<>(); //this goes in ViewModel constructor
toastMsg.setValue("hello"); //when you want to show toast

Make a function in your ViewModel to observe this SingleLiveEvent toastMsg and observe it just like you observe your regular LiveData in your Activity

In ViewModel:

SingleLiveEvent getToastSLE() {
    return toastMsg
}

In Activity:

viewmodel.getToastSLE().observe(this, toastString -> {
    Toast.makeText(this, toastString, Toast.LENGTH_LONG).show() //this will display toast "hello"
})

Original Article: https://medium.com/androiddevelopers/livedata-with-snackbar-navigation-and-other-events-the-singleliveevent-case-ac2622673150

2
On

Instead of SingleLiveEvent, If u are using Kotlin and for only one-time trigger of data/event use MutableSharedFlow

example:

// init
val data = MutableSharedFlow<String>()

// set value
data.emit("hello world)

lifecycleScope.launchWhenStarted {
      data.collectLatest { 
          // value only collect once unless a new trigger come
      }
}

MutableSharedFlow won't trigger for orientation changes or come back to the previous fragment etc

0
On

As stated here at December 2021 Edit at the end that you should let view tell your viewModel that your event has been processed. it's not the pretty looking solution but it's definitely one of the easiest solutions to understand and implement.

Basically you are adding a StateFlow in your viewModel which will hold your event then after your view Collecting it, you reset that state to null again:
in your viewModel ->

private val _loadingPostVisibilityEvent = MutableStateFlow<Boolean?>(null)
val loadingPostVisibilityEvent: StateFlow<Boolean?> = _loadingPostVisibilityEvent
fun setLoadingPostVisibilityEvent(isVisible: Boolean?) {
    _loadingPostVisibilityEvent.value = isVisible
}

then in your view->

viewLifecycleOwner.lifecycleScope.launch {
    repeatOnLifecycle(Lifecycle.State.CREATED) {
        launch {
           actionsViewModel.loadingPostVisibilityEvent.filterNotNull().collect {
                  // do your magic with the value $it
                  //then don't forget to reset the state.
                  actionsViewModel.setLoadingPostVisibilityEvent(null)
           }
        }
    }
}

Notice if you didn't reset the event stateFlow to null, it might be collected again if your view is recreated again.

if you want to use extension function to collect once then add this ->

suspend fun <T> StateFlow<T?>.collectOnce(reset: (T?) -> Unit, action: (value: T) -> Unit) {
    this.filterNotNull().onEach { reset.invoke(null) }.collect {
        action.invoke(it)
    }
}

and use it like this

viewLifecycleOwner.lifecycleScope.launch {
    repeatOnLifecycle(Lifecycle.State.CREATED) {
        launch {
           actionsViewModel.loadingPostVisibilityEvent.collectOnce(actionsViewModel::setLoadingPostVisibilityEvent) {
                  // do your magic with the value $it
           }
        }
    }
}
0
On

When we subscribe, we do not receive previous values.
When we send new values, all subscribers receive them.

class SingleLiveEvent<T> : MutableLiveData<T> {
    constructor() : super()
    constructor(value: T) : super(value)

    override fun observe(owner: LifecycleOwner, observer: Observer<in T>) {
        var observeNew = true
        super.observe(owner) {
            if (observeNew) observeNew = false
            else observer.onChanged(it)
        }
    }
}