Fragment livedata observer called only once with default value - android

I am just not able to figure out what is wrong in this code and why the observer is not called when the value is updated. I am using Fragement with livedata and here is the complete code. When app starts fragment gets it value from default data which in this case is 100. But after the value is updated using queueChannelId(channelId) method the observer is not called. I put a print statement and I can see method is executed in main thread. Please help
Fragment:
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
viewModel =
ViewModelProviders.of(this).get(SomeViewModel::class.java)
viewModel.getChannelId().observe(this, Observer {
// Only called with default value of mutablelivedata
})
}
I can assure that onDestroyView and onDestroy have not been called anytime.
ViewModel:
fun getChannelId() : MutableLiveData<Int> {
return repository.getChannelId()
}
Repository:
var channelIdObservable = MutableLiveData(100)
fun queueChannelId(channelId: Int) {
channelIdObservable.value = channelId
}
fun getChannelId() : MutableLiveData<Int> = channelIdObservable

if you are calling queueChannelId from some other Thread try
channelIdObservable.postValue (channelId)
P.S: I cant see any other issue here.Share your code of how are u calling queueChannelId.

Related

why flow collect call more than twice in kotlin?

Hey I am working in kotlin flow in android. I noticed that my kotlin flow collectLatest is calling twice and sometimes even more. I tried this answer but it didn't work for me. I printed the log inside my collectLatest function it print the log. I am adding the code
MainActivity.kt
class MainActivity : AppCompatActivity(), CustomManager {
private val viewModel by viewModels<ActivityViewModel>()
private lateinit var binding: ActivityMainBinding
private var time = 0
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
setupView()
}
private fun setupView() {
viewModel.fetchData()
lifecycleScope.launchWhenStarted {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.conversationMutableStateFlow.collectLatest { data ->
Log.e("time", "${time++}")
....
}
}
}
}
}
ActivityViewModel.kt
class ActivityViewModel(app: Application) : AndroidViewModel(app) {
var conversationMutableStateFlow = MutableStateFlow<List<ConversationDate>>(emptyList())
fun fetchData() {
viewModelScope.launch {
val response = ApiInterface.create().getResponse()
conversationMutableStateFlow.value = response.items
}
}
.....
}
I don't understand why this is calling two times. I am attaching logs
2022-01-17 22:02:15.369 8248-8248/com.example.fragmentexample E/time: 0
2022-01-17 22:02:15.629 8248-8248/com.example.fragmentexample E/time: 1
As you can see it call two times. But I load more data than it call more than twice. I don't understand why it is calling more than once. Can someone please guide me what I am doing wrong. If you need whole code, I am adding my project link.
You are using a MutableStateFlow which derives from StateFlow, StateFlow has initial value, you are specifying it as an emptyList:
var conversationMutableStateFlow = MutableStateFlow<List<String>>(emptyList())
So the first time you get data in collectLatest block, it is an empty list. The second time it is a list from the response.
When you call collectLatest the conversationMutableStateFlow has only initial value, which is an empty list, that's why you are receiving it first.
You can change your StateFlow to SharedFlow, it doesn't have an initial value, so you will get only one call in collectLatest block. In ActivityViewModel class:
var conversationMutableStateFlow = MutableSharedFlow<List<String>>()
fun fetchData() {
viewModelScope.launch {
val response = ApiInterface.create().getResponse()
conversationMutableStateFlow.emit(response.items)
}
}
Or if you want to stick to StateFlow you can filter your data:
viewModel.conversationMutableStateFlow.filter { data ->
data.isNotEmpty()
}.collectLatest { data ->
// ...
}
The reason is collectLatest like backpressure. If you pass multiple items at once, flow will collect latest only, but if there are some time between emits, flow will collect each like latest
EDITED:
You really need read about MVVM architecture.
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
setupView()
}
private fun setupView() {
if (supportFragmentManager.findFragmentById(R.id.fragmentView) != null)
return
supportFragmentManager
.beginTransaction()
.add(R.id.fragmentView, ConversationFragment())
.commit()
}
}
Delele ActivityViewModel and add that logic to FragmentViewModel.
Also notice you don't need use AndroidViewModel, if you can use plain ViewModel. Use AndroidViewModel only when you need access to Application or its Context

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
}

How to pass data or add observer at onBackPressed event in FragmentActivity

I'm developing android app and I'm facing problem with passing data when user pressed back button (which means onBackPress event is fired).
I wanted to fire event with observer with viewmodel but it doesn't work.
like this.
// First Fragment
private val viewModel: MyViewModel by bindViewModel()
viewModel.currencyVal.observe { state ->
Timber.i("Event fired")
}
...
// Second fragment which was displayed with fragment transaction. This code is when user pressed back button. like override fun onBackPressed
private val viewModel: MyViewModel by bindViewModel()
viewModel.currencyVal(5)
// MyViewModel
...
val currencyVal = MutableLiveData<Int>()
...
fun setCurrencyVal(currencyVal: Int) {
currencyVal.value = currencyVal
}
Here's bindViewModel function
protected inline fun <reified T : ViewModel> bindViewModel(
crossinline initializer: T.() -> Unit = {}
): Lazy<T> = nonConcurrentLazy {
ViewModelProviders.of(requireActivity())
.get(T::class.java)
.also { it.initializer() }
}
And also passing data via fragment transaction doesn't work.
Could anyone please suggest how to pass data when user presses back button in FragmentActivity?
Thanks.
Please make sure your fragment instance is same.
If it's not same, you need to create view model by activity.
Hope this helps.
I am missing something. Is viewModel and firstViewModel the same object? Also if so are you sure that you are creating the ViewModel of the Activity, but not the Fragment?
mViewModel = ViewModelProviders.of(requireActivity()).get(YourViewModel.class);

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