Using liveData in viewModel, I observe if any web api response return, but how to remove specific observe with removeObserve method?
class MyViewModel: ViewModel() {
fun buttomSubmit() {
val responseLiveData = webFetch()
responseLiveData.observeForever(
Observe { // define a Observe?
doSomething()
}
)
}
override fun onCleared() {
responseLiveData.removeObserver(observer) // how to correctly remove the observe
super.onCleared()
}
}
First, define your observer and store it
val mObserver: Observer<MyClass> = Observer { obj ->
doSomething(obj)
}
then you can start observing forever with
responseLiveData.observeForever(mObserver)
and then stop
responseLiveData.removeObserver(mObserver)
Related
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'm starting to use coroutines with Kotlin, I want to pass as a parameter an object from the fragment, but I still haven't understood well how this object could happen to this coroutine, I'll really be grateful for your help
val addObject: LiveData<Object> = liveData {
emit(Result.loading())
try {
emit(Result.success(fetchUser()))
} catch(ioException: Exception) {
emit(Result.error(ioException))
}
}
From my fragment I would have to send my object and then be able to observe it
productViewModel.addObject(Object).observe
you cant pass an object in a parameter that does not even make sense.
what you probably want to be doing is something like this
ViewModel
private val _liveData: MutableLiveData<Object> = MutableLiveData()
val liveData:LiveData<Object>
get() = _liveData
fun addObject(myObject:Object){
// do work here
emit(someData)
}
Or even just
fun addObject(myObject:Object):LiveData<Object>{
val liveData: MutableLiveData<Object> = MutableLiveData()
viewModelScope.launch {
//do work then emit back
liveData.postValue(it)
}
return liveData
}
Activity
viewModel.liveData.observe(this, Observer{
})
viewModel.addObject(myObject)
Or for the second example
viewModel.addObject(myObject).observe(this, Observer{
})
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.
I'm working with MVVM, and I have made different implementations of it, but one thing that is still making me doubt is how do I get data from a Repository (Firebase) from my ViewModel without attaching any lifecycle to the ViewModel.
I have implemented observeForever() from the ViewModel, but I don't think that is a good idea because I think I should communicate from my repository to my ViewModel either with a callback or a Transformation.
I leave here an example where I fetch a device from Firebase and update my UI, if we can see here, I'm observing the data coming from the repo from the UI, but from the ViewModel I'm also observing data from the repo, and here is where I really doubt if I'm using the right approach, since I don't know if observeForever() will be cleared on onCleared() if my view is destroyed, so it won't keep the observer alive if the view dies.
UI
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
button.setOnClickListener {
val deviceId = editText.text.toString().trim()
observeData(deviceId)
}
}
fun observeData(deviceId:String){
viewModel.fetchDeviceData(deviceId).observe(this, Observer {
textView.text = "Tipo: ${it.devType}"
})
ViewModel
class MainViewmodel: ViewModel() {
private val repo = Repo()
fun fetchDeviceData(deviceId:String):LiveData<Device>{
val mutableData = MutableLiveData<Device>()
repo.getDeviceData(deviceId).observeForever {
mutableData.value = it
}
return mutableData
}
}
Repository
class Repo {
private val db = FirebaseDatabase.getInstance().reference
fun getDeviceData(deviceId:String):LiveData<Device>{
val mutableData = MutableLiveData<Device>()
db.child(deviceId).child("config/device").addListenerForSingleValueEvent(object: ValueEventListener{
override fun onDataChange(dataSnapshot: DataSnapshot) {
val device = dataSnapshot.getValue(Device::class.java)
mutableData.value = device
}
override fun onCancelled(dataError: DatabaseError) {
Log.e("Error","handle error callback")
}
})
return mutableData
}
}
This example just shows how to fetch the device from Firebase, it works, but from my ViewModel, it keeps making me think that observeForever() is not what I'm looking for to communicate data between the repository to the ViewModel.
I have seen Transformations, but I, in this case, I just need to deliver the entire Device object to my UI, so I don't need to transform the Object I'm retrieving to another Object
What should be here the right approach to communicate the repository and the ViewModel properly?
is observeForever lifecycle aware?
No, that's why it's called observeForever.
I have implemented observeForever() from the ViewModel, but I don't think that is a good idea
No, it's not, you should be using Transformations.switchMap {.
since I don't know if observeForever() will be cleared on onCleared() if my view is destroyed, so it won't keep the observer alive if the view dies.
Well if you're not clearing it in onCleared() using removeObserver(observer), then it won't clear itself, because it observes forever.
here is where I really doubt if I'm using the right approach,
No, you can do much better than this following a reactive approach.
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
button.setOnClickListener {
val deviceId = editText.text.toString().trim()
viewModel.onSelectedDeviceChanged(deviceId)
}
viewModel.selectedDevice.observe(this, Observer { device ->
textView.text = "Tipo: ${device.devType}"
})
}
And
class MainViewModel(
private val savedStateHandle: SavedStateHandle,
): ViewModel() {
private val repo = Repo() // TODO: move to Constructor Argument with ViewModelProvider.Factory
private val selectedDeviceId: MutableLiveData<String> = savedStateHandle.getLiveData<String>("selectedDeviceId")
fun onSelectedDeviceChanged(deviceId: String) {
selectedDeviceId.value = deviceId
}
val selectedDevice = Transformations.switchMap(selectedDeviceId) { deviceId ->
repo.getDeviceData(deviceId)
}
}
And
class Repo {
private val db = FirebaseDatabase.getInstance().reference // TODO: move to constructor arg? Probably
fun getDeviceData(deviceId:String) : LiveData<Device> {
return object: MutableLiveData<Device>() {
private val mutableLiveData = this
private var query: Query? = null
private val listener: ValueEventListener = object: ValueEventListener {
override fun onDataChange(dataSnapshot: DataSnapshot) {
val device = dataSnapshot.getValue(Device::class.java)
mutableLiveData.value = device
}
override fun onCancelled(dataError: DatabaseError) {
Log.e("Error","handle error callback")
}
}
override fun onActive() {
query?.removeEventListener(listener)
val query = db.child(deviceId).child("config/device")
this.query = query
query.addValueEventListener(listener)
}
override fun onInactive() {
query?.removeEventListener(listener)
query = null
}
}
}
}
This way, you can observe for changes made in Firebase (and therefore be notified of future changes made to your values) using LiveData, rather than only execute a single fetch and then not be aware of changes made elsewhere to the same data.
To use ObserveForever, you need to remove the observer inside onClear in the ViewModel.
In this case, I would suggest to use Transformation even though you just need a direct mapping without any processing of the data, which is actually the same as what you are doing with the observer for observerForever.
observeForever() is not Lifecycle aware and will continue to run until removeObserver() is called.
In your ViewModel do this instead,
class MainViewmodel: ViewModel() {
private val repo = Repo()
private var deviceData : LiveData<Device>? = null
fun fetchDeviceData(deviceId:String):LiveData<Device>{
deviceData = repo.getDeviceData(deviceId)
return deviceData!!
}
}
I have a repository setup like this
class ServerTimeRepo #Inject constructor(private val retrofit: Retrofit){
var liveDataTime = MutableLiveData<TimeResponse>()
fun getServerTime(): LiveData<TimeResponse> {
val serverTimeService:ServerTimeService = retrofit.create(ServerTimeService::class.java)
val obs = serverTimeService.getServerTime()
obs.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()).unsubscribeOn(Schedulers.io())
.subscribe(object : Observer<Response<TimeResponse>> {
override fun onComplete() {
}
override fun onSubscribe(d: Disposable) {
}
override fun onNext(t: Response<TimeResponse>) {
val gson = Gson()
val json: String?
val code = t.code()
val cs = code.toString()
if (!cs.equals("200")) {
json = t.errorBody()!!.string()
val userError = gson.fromJson(json, Error::class.java)
} else {
liveDataTime.value = t.body()
}
}
override fun onError(e: Throwable) {
}
})
return liveDataTime
}
}
Then I have a viewmodel calling this repo like this
class ServerTimeViewModel #Inject constructor(private val serverTimeRepo: ServerTimeRepo):ViewModel() {
fun getServerTime(): LiveData<TimeResponse> {
return serverTimeRepo.getServerTime()
}
}
Then I have an activity where I have an onClickListener where I am observing the livedata, like this
tvPWStart.setOnClickListener {
val stlv= serverTimeViewModel.getServerTime()
stlv.observe(this#HomeScreenActivity, Observer {
//this is getting called multiple times??
})
}
I don't know what's wrong in this. Can anyone point me in the right direction? Thanks.
Issue is that every time your ClickListener gets fired, you observe LiveData again and again. So, you can solve that problem by following solution :
Take a MutableLiveData object inside your ViewModel privately & Observe it as LiveData.
class ServerTimeViewModel #Inject constructor(private val serverTimeRepo: ServerTimeRepo):ViewModel() {
private val serverTimeData = MutableLiveData<TimeResponse>() // We make private variable so that UI/View can't modify directly
fun getServerTime() {
serverTimeData.value = serverTimeRepo.getServerTime().value // Rather than returning LiveData, we set value to our local MutableLiveData
}
fun observeServerTime(): LiveData<TimeResponse> {
return serverTimeData //Here we expose our MutableLiveData as LiveData to avoid modification from UI/View
}
}
Now, we observe this LiveData directly outside of ClickListener and we just call API method from button click like below :
//Assuming that this code is inside onCreate() of your Activity/Fragment
//first we observe our LiveData
serverTimeViewModel.observeServerTime().observe(this#HomeScreenActivity, Observer {
//In such case, we won't observe multiple LiveData but one
})
//Then during our ClickListener, we just do API method call without any callback.
tvPWStart.setOnClickListener {
serverTimeViewModel.getServerTime()
}