collector in Activity does not always trigger - android

So I have this in an onCreate in an activity
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
setSupportActionBar(binding.toolbar)
lifecycleScope.launch(Dispatchers.IO) {
repeatOnLifecycle(Lifecycle.State.CREATED) {
viewModel.state.collect() { state ->
println(state)
when (state) {
MyViewModel.State.First -> launch(Dispatchers.Main) {
supportFragmentManager.commit {
replace(R.id.nav_host_fragment_content_main, FirstFragment())
}
}
MyViewModel.State.Second -> launch(Dispatchers.Main) {
supportFragmentManager.commit {
replace(R.id.nav_host_fragment_content_main, SecondFragment())
}
}
MyViewModel.State.Init -> {}
}
}
}
}
}
and in the viewmodel I have a stateflow
class MyViewModel : ViewModel() {
enum class State {
First,
Second,
Init; }
val state = MutableStateFlow(State.Init)
fun goToFirst() {
viewModelScope.launch {
println("go to first")
state.emit(State.First)
}
}
fun goToSecond() {
viewModelScope.launch {
println("go to second")
state.emit(State.Second)
}
}
}
the app displays the list fragment and I can add and remove users its great... until the list is empty. The activity stops collecting from the stateflow and never switches to the empty. Its gets weird though. In the viewModel, as an experiment, I added
init {
while (isActive) {
delay(1000)
state.emit(State.First)
delay(1000)
state.emit(State.Second)
}
}
and it switches back forth between fragments. It just doesn't switch to the empty state fragment when I use the buttons on the screen to clear the list. I've tried using SharedFlow and I've tried using a stateflow of an enum class that had two vales list and empty. Samething. The collector in the activity doesn't fire everytime. I know about conflation. the values are different. I've tried almost every combination of Dispatchers and add in a coroutine exception handler to every launch that never catches anything. I've also tried using globalscope. Why doesn't the collector in the activity fire everytime a different value is emitted?

I was using a viewModel factory in the activity and in the fragments. So the fragment would call a function it's viewmodel to change the state, but the viewmodel the activity was listening to never sent any state change.

Related

LiveData not giving callback on lifecycleOwner state change to active state

LiveData used to give callback when lifecycleOwner changed from inactive to active state, therefore we have SingleLiveEvent or Event wrapper as described in this article.
But I am not getting callback on state change if callback was given once, I have created a sample project for same
MainActivity
class MainActivity : AppCompatActivity() {
private val viewModel: MainViewModel by lazy {
ViewModelProvider(this)[MainViewModel::class.java]
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
findViewById<View>(R.id.tv).setOnClickListener {
startActivity(Intent(this, MainActivity2::class.java))
}
viewModel.liveData.observe(this) {
Toast.makeText(this, it, Toast.LENGTH_LONG).show()
}
}
}
ViewModel
class MainViewModel: ViewModel() {
val liveData = MutableLiveData<String>("Random value")
}
In this code, toast is shown when the app launches and it is not shown again if MainActivity2 is started or the app goes to background and then comes back to foreground.

Android Jetpack Navigation Fragment show again and again

I am developing an android app using Jetpack library:
Hilt
Navigation
ViewModel
DataBinding
Actually, I am familiar with MVP pattern.
I am trying to study MVVP pattern (Databinding and Jetpack ViewModel)
I have 2 fragments (A and B).
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
#AndroidEntryPoint
class AFragment {
private val viewModel: AViewModel by viewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.viewModel = viewModel
with(binding) {
button.setOnClickListener {
this#AFragment.viewModel.doAction()
}
}
viewModel.result.observe(viewLifecycleOwner) { result ->
findNavController().navigate(AFragmentDirections.actionAFragmentToBFragment(result))
}
}
}
And here is AViewModel:
#HiltViewModel
class AViewModel #Inject constructor(): ViewModel() {
private val _result: MutableLiveData<Int> = MutableLiveData()
val result: LiveData<Int>
get() = _result
fun doAction() {
_result.postValue(SOME_ACTION_RESULT)
}
}
It shows BFragment correctly.
But If I touch Back Button on BFragment, it still shows BFragment.
Actually, It went to back AFragment, but it comes again to BFragment.
When I touch Back Button on BFragment,
AFragment is started again (I checked onViewCreated() is called again)
Below observe code is called again:
viewModel.result.observe(viewLifecycleOwner) { result ->
findNavController().navigate(AFragmentDirections.actionAFragmentToBFragment(result))
}
Why this code is called again?
And do I write code correctly?
What is the best practice?
Now, I found a solution.
In AFragment:
viewModel.result.observe(viewLifecycleOwner) { result ->
if (result != null) {
findNavController().navigate(AFragmentDirections.actionAFragmentToBFragment(result))
viewModel.resetResult()
}
}
and In AViewModel:
fun resetResult() {
_result.postValue(null)
}
With this code, it works fine.
Yes... But I don't like this code...
It's... so weird...
I don't know what is the best practice...
the problem is related with livedata and fragment lifecycle.
AFragment and AViewModel lives when you move to FragmentB, but view in AFragment detach and attach when you move and come back. It means onViewCreated() called every time when you touch Back button on BFragment. As a result, AFragment start to observe AViewModel which has already valid data with its _result livedata.
You should separate uidata and events in livedatas. Easiest solution is SingleEventLiveData implementation and use it.
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
}
}
in viewmodel:
private val _result: MutableLiveData<Event<Int>> = MutableLiveData()
val result: LiveData<Event<Int>>
get() = _result
fun doAction() {
_result.postValue(Event(5))
}
how to observe:
viewModel.result.observe(viewLifecycleOwner) { result ->
result.getContentIfNotHandled()?.let {
findNavController().navigate(R.id.action_fragment_a_to_fragment_b)
}
}
sources:
https://medium.com/androiddevelopers/livedata-with-snackbar-navigation-and-other-events-the-singleliveevent-case-ac2622673150
How to create LiveData which emits a single event and notifies only last subscribed observer?

Observer in Fragment works but not in Activity

I created an Observer in a Fragment which works perfectly (it fires a toast when an Int increases), but when I try to move this code into the Activity, the observer doesn't seem to connect and it does not update when the LiveData changes.
Fragment (this works!):
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
loginViewModel = ViewModelProviders.of(this).get(LoginViewModel::class.java)
loginViewModel.getLoginAttemptCount().observe(this, Observer { count ->
if (count > 0) makeToast("Authentication failed")
})
}
Activity (when I put the observer in the Activity it doesn't!):
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.login_activity)
loginViewModel = ViewModelProviders.of(this).get(LoginViewModel::class.java)
loginViewModel.getLoginAttemptCount().observe(this, Observer { count ->
if (count > 0) makeToast("Authentication failed")
})
}
ViewModel (both call same function in VM):
fun getLoginAttemptCount(): MutableLiveData<Int> {
Log.d(TAG, "getLoginAttemptCount()")
return firestoreRepository.getLoginAttemptCount() }
Repo (called from VM):
fun getLoginAttemptCount(): MutableLiveData<Int>{
Log.d(TAG, "getLoginAttemptCount()")
return loginAttempt
}
loginAttempt.value is increased everytime there is a login attempt and I have verified this works in Logcat..
For info, makeToast is simply a function to create a justified Toast (text and position):
private fun makeToast(message: String) {
val centeredText: Spannable = SpannableString(message)
centeredText.setSpan(
AlignmentSpan.Standard(Layout.Alignment.ALIGN_CENTER),
0, message.length - 1,
Spannable.SPAN_INCLUSIVE_INCLUSIVE
)
val toast = Toast.makeText(this, centeredText, Toast.LENGTH_LONG)
toast.setGravity(Gravity.CENTER,0,0)
toast.show()
Log.d(TAG, "Toast message: $message")
}
I'm assuming it is to do with the lifeCycleOwner but I am at a loss!
loginViewModel = ViewModelProviders.of(this).get(LoginViewModel::class.java)
In Fragment
you are using above line to create loginviewmodel passing the context of fragment to viewmodel
so,the first thing android does is that it check ,if it contain's any other viewmodel associated with this fragment, if it contains it will not create new Viewmodel it will return the old one
if it does not contain it create a new one.Viewmodel are created using key value pair.
So in your case
you are creating total two viewmodel each of fragment and activity you are changing the live data of fragment but you are trying to observe it in activity using activity viewmodel.
If you want to acheive that you need to create shared viewmodel among activity and fragment.How to create shared viewmodel

Observe LiveData called when onCreate fragment

In short: when Observe is active it works correctly when I do notify, but when I go back to the previous fragment (I use the navigation component) and again navigate to the current fragment, there is a creation of the fragment, and for some reason the Observe is called.
Why is the Observe not deleted when going back? It should behave according to the fragment's lifecycle.
I tried removing on onStop and still the observe called.
More detail:
Each of my project fragments is divided into 3 parts: model, viewModel, view
In the view section, I first set the viewModel.
class EmergencyFragment : Fragment() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
emergencyFragmentViewModel = ViewModelProviders.of(this).get(EmergencyFragmentViewModel::class.java)
}
And in onViewCreated I set the Observer object so that any changes made in LiveData I get a change notification here:
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
emergencyFragmentViewModel.isEmergencyEventMediaLDSuccess.observe(viewLifecycleOwner, Observer {
Log.d("sendEmergencyEvent", "isEmergencyEventMediaLDSuccess observer called")
}
})
}
In the ViewModel class, I set the LiveData parameter as follows:
EmergencyFragmentViewModel: ViewModel() {
var isEmergencyEventMediaLDSuccess: LiveData<Boolean> = MutableLiveData()
private set
private val observerEventMedia = Observer<Boolean> { (isEmergencyEventMediaLDSuccess as MutableLiveData).value = it}
And in the init I set an observer:
init {
EmergencyFragmentModel.isEmergencyEventMediaLDSuccessModel.observeForever(observerEventMedia)
}
And of course removes when needed
override fun onCleared() {
super.onCleared()
EmergencyFragmentModel.isEmergencyEventMediaLDSuccessModel.removeObserver(observerEventMedia)
}
The part of the model is defined as follows:
class EmergencyFragmentModel {
companion object{
val isEmergencyEventMediaLDSuccessModel: LiveData<Boolean> = MutableLiveData()
And I do request network and when a reply comes back I perform a notify
override fun onResponse(call: Call<Int>, response: Response<Int>) {
if(response.isSuccessful) {
(isEmergencyEventLDModelSuccess as MutableLiveData).postValue(true)
Log.d("succeed", "sendEmergencyEvent success: ${response.body().toString()}")
}
Can anyone say what I'm missing? Why when there is an active Observe and I go back to the previous fragment (I use the navigation component) and navigate to the current fragment again, the Observe is called? I can understand that when a ViewModel instance is created and it executes setValue for the LiveData parameter, then it is notified. But Why is the observe not removed when I go back? I tried removing the Observe on the onStop and it keeps happening.
override fun onStop() {
super.onStop()
emergencyFragmentViewModel.isEmergencyEventMediaLDSuccess.removeObservers(viewLifecycleOwner)
emergencyFragmentViewModel.isEmergencyEventMediaLDSuccess.removeObserver(observeEmergencyEventLDSuccess)
}
#Pawel is right. LiveData stores the value and everytime you observe it (in your onViewCreated, in this case), it'll emit the last value stored.
Maybe you want something like SingleLiveEvent, which clean its value after someone reads it.
So when you go back and forth, it won't emit that last value (once it was cleaned).
As I understand your question, you only want to run the observer, when the new value differs from the old one. That can be done by retaining the value in another variable in the viewModel.
if (newValue == viewModel.retainedValue) return#observe
viewModel.retainedValue = newValue
I fixed this by creating an extension in kotlin by checkin the lifecycle state.
fun <T> LiveData<T>.observeOnResumedState(viewLifecycleOwner: LifecycleOwner, observer: Observer<T>) {
this.observe(viewLifecycleOwner) {
if (viewLifecycleOwner.lifecycle.currentState == Lifecycle.State.RESUMED) {
observer.onChanged(it)
}
}
}
And here is how i observe
viewModel.result.observeOnResumedState(viewLifecycleOwner) {
// TODO
}

LiveData observer fired twice, even with viewLifecycleOwner

I'm struggling with a LiveData observer which is firing twice. In my fragment I'm observing a LiveData as below, using viewLifeCycleOwner as LifeCycleOwner
private lateinit var retailViewModel: RetailsViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
retailViewModel = ViewModelProviders.of(this).get(RetailsViewModel::class.java)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
retailViewModel.retailLiveData.observe(viewLifecycleOwner, Observer {
// updating UI here, but firing twice!
}
retailViewModel.getRetailById(retail.id)
}
And this is my view model:
class RetailsViewModel(override val service: MyFoodyApiService = MyFoodyApiService.service) :
BaseViewModel(service) {
var retailLiveData: MutableLiveData<Retail> = MutableLiveData()
fun getRetailById(id: Int) {
scope.launch {
try {
val response =
service.getRetailById(authString, id).await()
when (response.isSuccessful) {
true -> {
response.body()?.let { payload ->
retailLiveData.postValue(payload.data)
} ?: run {
errorLiveData.postValue("An error occurred: ${response.message()}")
}
}
false -> errorLiveData.postValue("An error occurred: ${response.message()}")
}
} catch (e: Exception) {
noConnectionLiveData.postValue(true)
}
}
}
}
When I run the fragment for the first time, everything works fine, however when I go to its DetailFragment and come back, retailLiveData Observer callback is fired twice. According to this article this was a known problem solved with the introduction of viewLifeCycleOwner who should be helpful to remove active observers once fragment's view is destroyed, however it seems not helping in my case.
This happens because view model retains value when you open another fragment, but the fragment's view is destroyed. When you get back to the fragment, view is recreated and you subscribe to retailLiveData, which still holds the previous value and notifies your observer as soon as fragment moves to started state. But you are calling retailViewModel.getRetailById(retail.id) in onViewCreated, so after awhile the value is updated and observer is notified again.
One possible solution is to call getRetailById() from view model's init method, the result will be cached for view model lifetime then.

Categories

Resources