LiveData is not re-emitting when fragment is resumed - android

In my fragment, I have this code:
fun onViewCreated(view: View, savedInstanceState: Bundle?) {
//...
viewModel.state.observe(viewLifecycleOwner) {
//do something
}
}
And in my ViewModel:
class MyViewModel: ViewModel() {
val state = liveData {
val state = dataSource.getState()
emit(state)
}
}
When I navigate to another fragment or activity, and press back button, Fragment's onCreateView and onViewCreated methods are called, but viewModel.state has the same value. I mean, dataSource.getState() is not called again. I need state to be re-fetched from data source.
Is this possible using liveData builder? If not, how should I do it?

You need just cal load function every time when it needed. One of possible way to do it
ViewModel :
val stateLiveData = MutableLiveData<>()
fun loadData() {
viewModelScope.launch {
val state = dataSource.getState()
stateLiveData.setValue(state)
}
}
Fragment :
fun onViewCreated(view: View, savedInstanceState: Bundle?) {
//...
viewModel.loadData()
viewModel.stateLiveData.observe(viewLifecycleOwner) {
//do something
}
}

Related

Android Navigation + LiveData stopped observing when fragment goes to background

I have 2 fragments that i navigate throught navigation component from Jetpack. When i go back from fragment 2 to fragment 1, i fire an event throught livedata from fragment 2 and fragment1 should be observing. Althought, fragment 1 didnt observed and when i sste a breakpoint to check how many observers liveData has, it say 0.
Code below
class Fragment1 :
Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
myViewModel.selectedItem.observe(viewLifecycleOwner, {
println("Observed")
})
findNavController().navigate(
Fragment1Directions.actionToFragment2()
)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
myViewModel = ViewModelProvider(requireActivity()).get(MyViewModel::class.java)
}
private lateinit var myViewModel: MyViewModel
}
class Fragment2 :
Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
myButton.setOnClickListener{
MyViewModel().selectItem(true)
findNavController().navigateUp()
}
}
}
class MyViewModel : ViewModel() {
private val mutableSelectedItem = MutableLiveData<Boolean>()
val selectedItem: LiveData<Boolean> get() = mutableSelectedItem
fun selectItem(value: Boolean) {
mutableSelectedItem.value = value
}
}
It's strange to not have observer for
myViewModel.selectedItem.observe(viewLifecycleOwner, {
println("Observed")
})
when it's called onViewCreated and with viewLifecycleOwner, but it looks like you have a mistake for creating common ViewModel for Fragment1 and Fragment2
Both should use
myViewModel = ViewModelProvider(requireActivity()).get(MyViewModel::class.java) to get same ViewModel for both fragments

LiveData binding observer being registered multiple times when changing device orientation even when using viewLifeCycleOwner

I have a view model that is data binded to a fragment. The view model is shared with the main activity.
I've button is binded to the view as follows:
<Button
android:id="#+id/startStopBtn"
android:text="#{dashboardViewModel.startStopText == null ? #string/startBtn : dashboardViewModel.startStopText}"
android:onClick = "#{() -> dashboardViewModel.onStartStopButton(context)}"
android:layout_width="83dp"
android:layout_height="84dp"
android:layout_gravity="center_horizontal|center_vertical"
android:backgroundTint="#{dashboardViewModel.isRecStarted == false ? #color/startYellow : #color/stopRed}"
tools:backgroundTint="#color/startYellow"
android:duplicateParentState="false"
tools:text="START"
android:textColor="#FFFFFF" />
What I expect to happen is that every time I press the button the function onStartStopButton(context) runs. This works fine as long as I don't rotate the device. When I rotate the device the function is run twice, if I rotate again the function is run 3 times and so on. This is not a problem if I go to another fragment and then back to the dashboard fragment. It looks like the live data observer is getting registered every time I rotate my screen, but not every time I detach and reattach the fragment.
This is true for all the elements in that fragment, whether they are data binded or I manually observe them.
Fragment code:
class DashboardFragment : Fragment() {
private var _binding: FragmentDashboardBinding? = null
private val binding get() = _binding!!
private val dashboardViewModel: DashboardViewModel by activityViewModels()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
_binding = FragmentDashboardBinding.inflate(inflater, container, false)
val root: View = binding.root
binding.dashboardViewModel = dashboardViewModel
binding.lifecycleOwner = viewLifecycleOwner
dashboardViewModel.bleSwitchState.observe(viewLifecycleOwner, Observer { switchState -> handleBleSwitch(switchState) })
dashboardViewModel.yLims.observe(viewLifecycleOwner, Observer { yLims ->
updatePlotWithNewData(yLims.first, yLims.second)
})
Timber.i("Dahsboard on create: DashboardViewModel in fragment: $dashboardViewModel")
return root
}
}
The view model:
class DashboardViewModel : ViewModel() {
//region live data
private var _isRecStarted = MutableLiveData<Boolean>()
val isRecStarted: LiveData<Boolean> get() = _isRecStarted
//private var _bleSwitchState = MutableLiveData<Boolean>()
val bleSwitchState = MutableLiveData<Boolean>()
private var _startStopText = MutableLiveData<String>()
val startStopText: LiveData<String> get() = _startStopText
private var _yLims = MutableLiveData<Pair<kotlin.Float,kotlin.Float>>()
val yLims: LiveData<Pair<kotlin.Float,kotlin.Float>> get() = _yLims
//endregion
init {
Timber.d("DashboardViewModel created!")
bleSwitchState.value = true
}
//region start stop button
fun onStartStopButton(context: Context){
Timber.i("Start stop button pressed, recording data size: ${recordingRawData.size}, is started: ${isRecStarted.value}")
isRecStarted.value?.let{ isRecStarted ->
if (!isRecStarted){ // starting recording
_isRecStarted.postValue(true)
_startStopText.postValue(context.getString(R.string.stopBtn))
startDurationTimer()
}else{ // stopping recording
_isRecStarted.postValue(false)
_startStopText.postValue(context.getString(R.string.startBtn))
stopDurationTimer()
}
} ?: run{
Timber.e("Error! Is rec started is not there for some reason")
}
}
}
The view model is created the first time from the MainActivity as follows:
class MainActivity : AppCompatActivity() {
private val dashboardViewModel: DashboardViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
Timber.i("DashboardViewModel in main activity: $dashboardViewModel")
}
}
Edit explaining why the MainActivity is tided to the ViewModel:
The reason why the ViewModel is linked to the main activity is that the main activity handles some Bluetooth stuff for a stream of data, when a new sample arrives then the logic to handle it and update the UI of the dashboard fragment is on the DashboardViewModel. The data still needs to be handled even if the dashboard fragment is not there.
So I need to pass the new sample to the DashboardViewModel from the main activity as that is where I receive it. Any suggestions to make this work?
As you know, when you instantiate the ViewModel of a Fragment with activityViewModels, it means that the ViewModel will follow the lifecycle of the Activity containing that Fragment. Specifically here is MainActivity.
So what does ViewModel tied to Activity lifecycle mean in your case?
When you return to the Fragment, normally LiveData (with ViewModel attached to Fragment lifcycler) will trigger again.
But when that ViewModel is attached to the Activity's lifecycle, the LiveData will not be triggered when returning to the Fragment.
That leads to when you return to the Fragment, your LiveData doesn't trigger again.
And that LiveData only triggers according to the life cycle of the activity. That is, when you rotate the screen, the Activity re-initializes, now your LiveData is triggered.
EDIT:
Here, I will give you one way. Maybe my code below doesn't work completely for your case, but I think it will help you in how to control LiveData and ViewModel when you bind ViewModel to Activity.
First, I recommend that each Fragment should have its own ViewModel and it should not depend on any other Fragment or Activity. Here you should rename the DashboardViewModel initialized by activityViewModels() as ShareViewModel or whatever you feel it is related to this being the ShareViewModel between your Activity and Fragment.
class DashboardFragment : Fragment() {
// Change this `DashboardViewModel` to another class name. Could be `ShareViewModel`.
private val shareViewModel: ShareViewModel by activityViewModels()
// This is the ViewModel attached to the DashboardFragment lifecycle.
private val viewModel: DashboardViewModel by viewModels()
private lateinit var _binding: FragmentDashboardBinding? = null
private val binding get() = _binding!!
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
_binding = FragmentDashboardBinding.inflate(inflater, container, false)
binding.dashboardViewModel = viewModel
binding.lifecycleOwner = viewLifecycleOwner
return binding.root
}
override fun onDestroyView() {
_binding = null
super.onDestroyView()
}
}
Next, when there is data triggered by the ShareViewModel's LiveData, you will set the value for the LiveData in the ViewModel associated with your Fragment. As follows:
DashboardViewModel.kt
class DashboardViewModel: ViewModel() {
private val _blueToothSwitchState = MutableLiveData<YourType>()
val blueToothSwitchState: LiveData<YourType> = _blueToothSwitchState
private val _yLims = MutableLiveData<Pair<YourType, YourType>>()
val yLims: LiveData<Pair<YourType, YourType>> = _blueToothSwitchState
fun setBlueToothSwitchState(data: YourType) {
_blueToothSwitchState.value = data
}
fun setYLims(data: Pair<YourType, YourType>) {
_yLims.value = data
}
}
DashboardFragment.kt
class DashboardFragment : Fragment() {
...
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
shareViewModel.run {
bleSwitchState.observe(viewLifeCycleOwner) {
viewModel.setBlueToothSwitchState(it)
}
yLims.observe(viewLifeCycleOwner) {
viewModel.setYLims(it)
}
}
viewModel.run {
// Here, LiveData fires observe according to the life cycle of `DashboardFragment`.
// So when you go back to `DashboardFragment`, the LiveData is re-triggered and you still get the observation of that LiveData.
blueToothSwitchState.observe(viewLifeCycleOwner, ::handleBleSwitch)
yLims.observe(viewLifeCycleOwner) {
updatePlotWithNewData(it.first, it.second)
}
}
}
...
}
Edit 2:
In case you rotate the device, the Activity and Fragment will be re-initialized. At that time, LiveData will fire observe. To prevent that, use Event. It will keep your LiveData from observing the value until you set the value again for LiveData.
First, let's create a class Event.
open class Event<out T>(private val content: T) {
var hasBeenHandled = false
private set
fun getContentIfNotHandled(): T? = if (hasBeenHandled) {
null
} else {
hasBeenHandled = true
content
}
fun peekContent(): T = content
}
Next, modify the return type of the LiveData that you want to trigger once.
ShareViewModel.kt
class ShareViewModel: ViewModel() {
private val _test = MutableLiveData<Event<YourType>>()
val test: LiveData<Event<YourType>> = _test
fun setTest(value: YourType) {
_test.value = Event(value)
}
}
Add this extension to easily get LiveData's observations.
LiveDataExt.kt
fun <T> LiveData<Event<T>>.eventObserve(owner: LifecycleOwner, observer: (t: T) -> Unit) {
this.observe(owner) { it?.getContentIfNotHandled()?.let(observer) }
}
Finally in the view, you get the data observed by LiveDatat.
class DashboardFragment : Fragment() {
...
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
shareViewModel.test.eventObserve(viewLifeCycleOwner) {
Timber.d("This is test")
}
}
...
}
Note: When using LiveData with Event, make sure that LiveData is not reset when rotating the device. If LiveData is set to value again, LiveData will still trigger even if you use Event.

Getting FragmentResultListener triggered without popbackstack

Currently I'm facing a problem getting data from a child fragment to it's parent fragment, I saw that the best option is using setFragmentResult but because setFragmentResultListener needs to be in STARTED state at parentFragment(doen't happen because it is stoped when replaced by another fragment) I see that the only option is to use popBackStack() and then the listener gets triggered. The thing is that I don't wanna use popBackStack()
Can anyone help me?
PS: No, I don't want to use viewModel in this case to keep data.
Listener:
class ResultListenerFragment : Fragment() {
val viewModel : SomeViewModel by viewModels()
var result : String? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Use the Kotlin extension in the fragment-ktx artifact
setFragmentResultListener("requestKey") { requestKey, bundle ->
//verify values
viewModel.repeat()
}
//in case of an error
viewModel.getError().observe(viewLifecycleOwner,{
requireActivity()
.supportFragmentManager
.beginTransaction()
.replace(R.id.framelayout, ErrorFragment.newInstance(it).commit()
}
}
}
Triggerer:
class ErrorFragment: Fragment(R.layout.fragment_error) {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
view.findViewById(R.id.some_tv).text = arguments.getString(keyError)
view.findViewById(R.id.result_button).setOnClickListener {
val result = "retry"
// Use the Kotlin extension in the fragment-ktx artifact
setFragmentResult("requestKey", bundleOf("bundleKey" to result))
}
}
companion object {
private const val keyError = "errorKey"
fun newInstance(error:String): ErrorFragment{
val args = Bundle().apply {
putString(keyError, error)
}
}
return ErrorFragment().apply{arguments = args}
}
}

When to call livedata observer?

On a button click i have to get some value from API call and then launch one screen. I have two options:
Call the observer each time when user will click on button.
Call the observer on fragment onActivityCreated() and store the value in variable and act accordingly on button click.
So which approach I should follow?
Actually it's up to you. But i always prefer to call it in Activity's onCreate() function, so activity only has 1 observer. If you call it in button click, it will give you multiple observers as much as button clicking
Here is some example :
class HomeProfileActivity: BaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
initObserver()
initView()
}
private fun initObserver() {
viewModel.profileWorkProccess.observe(this, {
swipeRefreshLayout.isRefreshing = it
})
viewModel.isLoadingJobs.observe(this, {
layoutProgressBarJobs.visibility = View.VISIBLE
recyclerViewJobs.visibility = View.GONE
dotsJobs.visibility = View.GONE
})
//other viewmodel observing ......
}
private fun initView() {
imageProfile.loadUrl(user.image, R.drawable.ic_user)
textName.text = identity.user?.fullName
textAddress.text = identity.user?.city
buttonGetData.setOnClickListener { viewModel.getData(this) }
}
}
If the button is placed on the Activity, and data is displayed in the Fragment, you need to store variable in Activity ViewModel and observe it in Fragment
You only need to call observe one time when fragment is created.
For example:
class MyActivity : AppCompatActivity() {
val viewModel: MyActViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
myButton.setOnClickListener { view ->
viewModel.getData()
}
}
}
class MyActViewModel: ViewModel {
val data: LiveData<String> = MutableLiveData()
fun getData() {}
}
class MyFragment: Fragment {
val actViewModel: MyActViewModel by activityViewModels()
override fun onActivityCreated(...) {
....
actViewModel.data.observe(viewLifecycleOwner, Observer { data ->
...
}
}
}

Fragment. getViewLifeCycleOwner doesn't prevent multiple calls of LiveData Observer

I use Clean Architecture, LiveData, Navigation component & Bottom Navigation view.
I am creating a simple application with three tabs. By default, the First tab Fragment loads user data using some API. When i go to another tabs and then return to the First tab Fragment, i see, that observe return a new data!
I need observe not to return data again when I switch back to the first tab! what am I doing wrong? Could you help me please?
P.s. For navigation i use sample from navigation-advanced-sample and after switching tabs onDestroy is not called.
First solution in the article Observe LiveData from ViewModel in Fragment said:
One proper solution is to use getViewLifeCycleOwner() as LifeCycleOwer while observing LiveData inside onActivityCreated as follows.
I use following code, but it's not work for me:
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
Timber.d("onActivityCreated")
viewModel.getProfileLive().observe(viewLifecycleOwner, observer)
}
Second solution in the article Architecture Components pitfalls — Part 1 recommends using Resetting an existing observer and Manually unsubscribing the observer in onDestroyView(). But it doesn't work for me either...
ProfileFragment.kt
class ProfileFragment : DaggerFragment() {
#Inject
lateinit var viewModel: ProfileFragmentViewModel
private val observer = Observer<Resource<Profile>> {
when (it.status) {
Resource.Status.LOADING -> {
Timber.i("Loading...")
}
Resource.Status.SUCCESS -> {
Timber.i("Success: %s", it.data)
}
Resource.Status.ERROR -> {
Timber.i("Error: %s", it.message)
}
}
};
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Timber.d("onCreate")
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
Timber.d("onCreateView")
return inflater.inflate(R.layout.fragment_profile, container, false)
}
fun <T> LiveData<T>.reObserve(owner: LifecycleOwner, observer: Observer<T>) {
removeObserver(observer)
observe(owner, observer)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
Timber.d("onViewCreated")
viewModel.getProfileLive().observe(viewLifecycleOwner, observer)
// viewModel.getProfileLive().reObserve(viewLifecycleOwner, observer)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
Timber.d("onActivityCreated")
}
override fun onDestroyView() {
super.onDestroyView()
Timber.d("onDestroyView")
// viewModel.getProfileLive().removeObserver(observer)
}
override fun onDestroy() {
super.onDestroy()
Timber.d("onDestroy")
}
override fun onDetach() {
super.onDetach()
Timber.d("onDetach")
}
}
ProfileFragmentViewModel.kt
class ProfileFragmentViewModel #Inject constructor(
private val profileUseCase: ProfileUseCase
) : ViewModel() {
init {
Timber.d("Init profile VM")
}
fun getProfileLive() = profileUseCase.getProfile()
}
ProfileUseCase
class ProfileUseCase #Inject constructor(
private val profileRepository: ProfileRepository
) {
fun getProfile(): LiveData<Resource<Profile>> {
return profileRepository.getProfile()
}
}
ProfileRepository.kt.
class ProfileRepository #Inject constructor(
private val loginUserDao: LoginUserDao,
private val profileDao: ProfileDao,
) {
fun getProfile(): LiveData<Resource<Profile>> =
liveData(Dispatchers.IO)
{
emit(Resource.loading(data = null))
val profile = profileDao.getProfile()
// Emit Success result...
}
}
It's because of how Fragment Lifecycle works. When you move to and fro from a fragment onViewCreated() is called again. In onViewCreated you're calling viewModel.getProfileLive() which returns the livedata upto from the repository and observe to it.
Since onViewCreated() gets called everytime when you move back to the Fragment so is your call to viewModel.getProfileLive() and in turn the repository gets called again which again triggers the observe method in your Fragment.
In order to solve this problem,
create a LiveData variable in your ViewModel, set it to the returned Live Data from Repository.
In the Fragment observe to the LiveData variable of your ViewModel not the one returned from Repository.
That way, your observe method will get triggered on very first time and only when value of your data from repository changes.

Categories

Resources