I'm facing an issue which drives me crazy.
I have 4 fragments inside an activity.
The logic is: FragA -> FragB -> FragC -> FragD -> FragA -> ...
I'm connected to websockets which post livedata values.
To navigate from FragB to FragC, I'm waiting an event.
The first time, everything works fine, the websockets is recieved, the event is triggered and I'm going to FragC.
But, the second time (after Frag D -> Frag A), if I go back to fragB, the same event is triggered once again. The user doesn't see FragB, and arrives on FragC.
This is the actual behavior but this is not the one I'm expected.
I have do some research and I think it's because the livedata is trigger twice in is normal behavior. And, it can be only dispatch on main thread, so if my fragment goes in the back stack, it will wait for it to be active again.
I have try to removeObserver in the onDestroyView(), it works and the observer is removed, but once the fragment goes again inside onActivityCreated() and I observe the livedata, the observer is instantanetely triggered... I always use "viewLifecycleOwner" as owner.
Is there any way to cancel a liveData execution if I ever go back on an instanciated fragment?
All my frags extends ScopeFragment :
abstract class ScopedFragment : Fragment(), CoroutineScope {
private lateinit var job: Job
override val coroutineContext: CoroutineContext
get() = job + Dispatchers.Main
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
job = Job()
}
override fun onDestroy() {
super.onDestroy()
job.cancel()
}
}
My liveData:
class MyLiveDatas {
private val _myLiveData = MutableLiveData<CustomType>()
val myLiveData: LiveData<CustomType>
get() = _myLiveData
fun customTrigger(webSocketMessage: WebSocketMessage) {
val createdCustomType = CreatedCustomType(webSocketMessage)
_myLiveData.post(createdCustomType)
}
}
My Fragment:
class MyFragment: ScopedFragment(), KodeinAware {
override val kodein by closestKodein()
private val myLiveData: MyLiveDatas by instance()
private val myLiveDataObserver = Observer<CustomType> { customType ->
... my actions
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
myLiveDatas.myLiveData.observe(viewLifecycleOwner, myLiveDataObserver)
}
override fun onDestroyView() {
super.onDestroyView()
myLiveDatas.myLiveData.removeObserver(myLiveDataObserver)
// I've also try removeObservers with viewLifecycleOwner
}
}
Thanks a lot!
You need to use custom live data , in case you want single event
this is my custom mutable live data in one of my project and it is working
class SingleLiveEvent<T> : MediatorLiveData<T>() {
private val observers = ArraySet<ObserverWrapper<in T>>()
#MainThread
override fun observe(owner: LifecycleOwner, observer: Observer<in T>) {
val wrapper = ObserverWrapper(observer)
observers.add(wrapper)
super.observe(owner, wrapper)
}
#MainThread
override fun removeObserver(observer: Observer<in T>) {
if (observers.remove(observer)) {
super.removeObserver(observer)
return
}
val iterator = observers.iterator()
while (iterator.hasNext()) {
val wrapper = iterator.next()
if (wrapper.observer == observer) {
iterator.remove()
super.removeObserver(wrapper)
break
}
}
}
#MainThread
override fun setValue(t: T?) {
observers.forEach { it.newValue() }
super.setValue(t)
}
private class ObserverWrapper<T>(val observer: Observer<T>) : Observer<T> {
private var pending = false
override fun onChanged(t: T?) {
if (pending) {
pending = false
observer.onChanged(t)
}
}
fun newValue() {
pending = true
}
}
}
LiveData is analogous to a BehaviorRelay, and replays the last value it was told to hold.
LiveData is not LiveEvent, it's not designed for event dispatching.
A regular event bus, a PublishRelay, or something like EventEmitter are better suited for this problem.
Google has devised LiveData<Event<T>> and EventObserver, but if you ever use observe(lifecycleOwner, Observer { instead of observe(lifecycleOwner, EventObserver { it will misbehave, which shows that it's a code smell (LiveData<Event<T>> does not work with Observer, only EventObserver, but its observe method still accepts Observers.)
So personally I'd rather pull in that library EventEmitter I mentioned above, with the LiveEvent helper class.
// ViewModel
private val eventEmitter = EventEmitter<Events>()
val controllerEvents: EventSource<Events> = eventEmitter
// Fragment
viewModel.controllerEvents.observe(viewLifecycleOwner) { event: ControllerEvents ->
when (event) {
is ControllerEvents.NewWordAdded -> showToast("Added ${event.word}")
}.safe()
}
Try to observe the LiveData at onCreate() of the Fragment lifecycle with lifecycle owner as Activity and remove the observer at onDestroy() of the Fragment lifecycle.
Or if that doesn't workout use Event class.
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
}
This article describes two ways to achieve what you want.
Alternative 1: Wrap your live data in a class that makes sure the value is only observed once.
/**
* Used as a wrapper for data that is exposed via a LiveData that represents an event.
*/
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
}
Alternative 2: Use a custom live data class (SingleLiveEvent) that only emits the value once.
Related
The issue that I have is not actually bug or big problem. And all works as it should, but nevertheless it annoys me.
In Fragment pbserver:
viewModel.pageNumbersPosition.observe(viewLifecycleOwner) {
if (it!=null) {
SharedPreferenceHelper.pagesNumber = viewModel.pageNumbersArray.value?.get(it)
DLog.d("Set: ${viewModel.pageNumbersArray.value?.get(it)}}")
//Log shows twice as start
}
}
ViewModel:
class MenuViewModel : ViewModel() {
var pageNumbersArray = MutableLiveData(getPageNumbering())
var pageNumbersPosition = MutableLiveData(pageNumbersArray.value?.indexOf(SharedPreferenceHelper.pagesNumber))
private fun getPageNumbering():Array<String> {
val list = mutableListOf<String>()
for (i in 1..25) {
list.add(i.toString())
}
return list.toTypedArray()
}
}
Spinner:
<Spinner
android:id="#+id/spinner"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:entries="#{viewModel.pageNumbersArray}"
android:selectedItemPosition="#={viewModel.pageNumbersPosition}"/>
What happes is viewModel.pageNumbersPosition.observe triggered twice on start. Once from the initiation of the fragment and second time when the spinner sets. This is actually suppose to happen, but I don't like it when Shared Preference sets twice.
I came across a handy class SingleLiveEvent that we can use instead of LiveData in ViewModel class to send only new updates after subscription.
class SingleLiveEvent<T> : MutableLiveData<T>() {
private val pending = AtomicBoolean(false)
override fun observe(owner: LifecycleOwner, observer: Observer<in T>) {
super.observe(owner, Observer<T> { t ->
if (pending.compareAndSet(true, false)) {
observer.onChanged(t)
}
})
}
override fun setValue(t: T?) {
pending.set(true)
super.setValue(t)
}
fun call() {
postValue(null)
}
}
This LiveData extension only calls the observable if there's an explicit call to setValue() or call().
Update, primary constructor with parameter:
class SingleLiveEvent<T>(value: T) : MutableLiveData<T>(value) {...}
You can check if there is equal value in your shared to avoid the double set
if (it!=null) {
viewModel.pageNumbersArray.value?.get(it).let{ value ->
if (SharedPreferenceHelper.pagesNumber != value)
SharedPreferenceHelper.pagesNumber = value
}
}
In my ViewModel I have two MutableLiveData for the response of my webservice :
val getFindByCategorySuccess: MutableLiveData<List<DigitalService>> by lazy {
MutableLiveData<List<DigitalService>>()
}
val getFindByCategoryError: MutableLiveData<String> by lazy {
MutableLiveData<String>()
}
and this method for the request :
fun requestFindByCategory(categoryId: String){
viewModelScope.launch {
when (val retrofitPost = digitalServicesRemoteRepository.getFindByCategoryRequest(categoryId)) {
is ApiResult.Success -> getFindByCategorySuccess.postValue(retrofitPost.data)
is ApiResult.Error -> getFindByCategoryError.postValue(retrofitPost.exception)
}
}
}
It's working fine using it in my Fragment class :
viewModel.getFindByCategorySuccess.observe(viewLifecycleOwner, { digitalServices ->
logD("I have a good response from the webservice; luanch an other fragment now!")
})
The problem is if I go to an other fragment in my observable (using findNavController().navigate(action)). If I go back to the previous fragment, I go automatically to the nextFragment because the observable is called again.
So I'm looking for solutions...
Maybe clearing all my viewmodel when I go back to my fragment ?
Maybe clearing only getFindByCategorySuccess and getFindByCategoryError ?
Maybe an other solution? I think my architecture is not good. What do you think about it ?
By default, a livedata will emit to its current state (the value that exist on it) for any new observer that subscribes to it.
Answering your question, you might try the operator distincUntilChanged transformation, which, according to the documentation:
Creates a new LiveData object that does not emit a value until the source LiveData value has been changed. The value is considered changed if equals() yields false.
But, this showcases a problem with your snippet, and a bad practice that is common when using livedata, you shouldn't expose mutable live data to your observers. Instead, you should expose a non-mutable version of them.
In your case, in my opinion, your view model should look like the following:
private val getFindByCategorySuccess by lazy {
MutableLiveData<List<DigitalService>>()
}
private val getFindByCategoryError by lazy {
MutableLiveData<String>()
}
val onFindByCategorySuccess: LiveData<List<DigitalService>
get() = getFindByCategorySuccess.distincUntilChanged()
val onFindCategoryError: LiveData<List<String>
get() = getFindByCategoryrRror.distincUntilChanged()
And your observers would subscribe as follows:
ExampleFragment
fun setupObservers() {
viewModel.onFindByCategorySuccess.observe(viewLifecycleOwner) { // Do stuff }
}
I hope it helps
I found a solution to my problem using this class :
class SingleLiveEvent<T> : MutableLiveData<T>() {
private val mPending = AtomicBoolean(false)
override fun observe(owner: LifecycleOwner, observer: Observer<in T>) {
super.observe(owner, { t ->
if (mPending.compareAndSet(true, false))
observer.onChanged(t)
})
}
override fun setValue(t: T?) {
mPending.set(true)
super.setValue(t)
}
}
Like this :
var getFindByCategorySuccess: SingleLiveEvent<List<DigitalService>> = SingleLiveEvent()
var getFindByCategoryError: SingleLiveEvent<String> = SingleLiveEvent()
I have fragment1, from which I go to fragment2.
The problem is that I can't go back to fragment1 from fragment2
This is how I handle a button click
val onClickLiveData = MutableLiveData<OnClick>()
fun onClick() {
onClickLiveData.value = OnClick.CLICK
}
This is how I handle transition to fragment2
private fun subscribeToClickCallbacks() {
viewModel.onClickLiveData.observe(viewLifecycleOwner, Observer {
findNavController().navigate(R.id.action_home_fragment_to_repositories_fragment)
})
}
I process the transition back like this
navController.popBackStack()
With the help of debug, I found out that with each transition to fragment1, livedata is called and instantly opens fragment2.
How can the problem be solved? I would be grateful for every answer.
If live data is observer multiple times use SingleEvent for handle this case.
Create one global class for SingleLiveData like this.
class SingleLiveEvent<T> : MutableLiveData<T>() {
private val mPending = AtomicBoolean(false)
#MainThread
override fun observe(owner: LifecycleOwner, observer: Observer<T>) {
if (hasActiveObservers()) {
Log.w(TAG, "Multiple observers registered but only one will be notified of changes.")
}
// Observe the internal MutableLiveData
super.observe(owner, object : Observer<T> {
override fun onChanged(t: T?) {
if (mPending.compareAndSet(true, false)) {
observer.onChanged(t)
}
}
})
}
#MainThread
override fun setValue(t: T?) {
mPending.set(true)
super.setValue(t)
}
/**
* Used for cases where T is Void, to make calls cleaner.
*/
#MainThread
fun call() {
setValue(null)
}
companion object {
private val TAG = "SingleLiveEvent"
}
}
Now use this class like this,
val onClickLiveData = SingleLiveEvent<OnClick>()
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 ;)
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.
you can add android:configChanges="orientation|screenSize|screenLayout" to your manifest for the activity. this should prevents restarts when orientation changes.
check this site and also here are som infos.
I am using MutableLiveData within my application for event based communication. I have single activity two fragments architecture.
With the help of ViewModel, I'm consuming the LiveData events in Fragment-1. But, when I replace this Fragment-1 with Fragment-2 using Menu bar and finally come back to Fragment-1, old values of LiveData are captured again.
How to avoid this problem? Any help/suggestions are highly appreciated!
Thank you.
You can use Event to wrap LiveData values to handle consuming its values as in the following article:
https://medium.com/androiddevelopers/livedata-with-snackbar-navigation-and-other-events-the-singleliveevent-case-ac2622673150
Event class would be like:
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
}
Let us say that your LiveData value is a String then the LiveData of single event would be like:
val navigateToDetails = MutableLiveData<Event<String>>()
Problem with accepted answer is that you can only have one observer.
This article describes solution with multiple observers.
Wherever you're observing the liveData, in onChanged method remove the observers by calling myLiveDataObject.removeObservers(this);
This will remove the observer after first-time data is observed.
I faced the same problem and came up with this library to solve it
https://github.com/ueen/LiveEvent
Hope this helps, enjoy!
Simple, clean, reusable:
class Event<T>(val payload: T, var broadcasted: Boolean = false)
class MutableEventLiveData<T>: MutableLiveData<Event<T>>() {
fun postEvent(value: T) {
super.postValue(Event(value))
}
}
typealias EventLiveData<T> = LiveData<Event<T>>
class EventObserver<T>(private val broadcastCallback: (t: T)->Unit): Observer<Event<T>> {
override fun onChanged(e: Event<T>) {
if (!e.broadcasted) {
broadcastCallback(e.payload)
e.broadcasted = true
}
}
}
Sample usage:
class YourViewModel : ViewModel() {
private val _errorEvent = MutableEventLiveData<String>()
val errorEvent: EventLiveData<String>
get() = _errorEvent
fun fireErrorEvent(errorMessage: String) {
_errorEvent.postEvent(errorMessage)
}
...
}
class YourActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
...
//Note!!! EventObserver handles events not Observer
viewModel.errorEvent.observe(this, EventObserver {
errorMessage -> showErrorMessage(errorMessage)
})
}
...
}