Reputation: 79
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)
}
}
})
}
Upvotes: 3
Views: 12077
Reputation: 55
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)
}
}
}
Upvotes: 0
Reputation: 3599
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
Upvotes: 7
Reputation: 1332
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
}
}
}
}
Upvotes: 0
Reputation: 14193
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)
}
})
}
Upvotes: 9
Reputation: 252
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
Upvotes: 0