When flow collect stop itself? - android

There is ParentFragment that shows DialogFragment. I collect a dialog result through SharedFlow. When result received, dialog dismissed. Should I stop collect by additional code? What happens when dialog closed, but fragment still resumed?
// ParentFragment
private fun save() {
val dialog = ContinueDialogFragment(R.string.dialog_is_save_task)
dialog.show(parentFragmentManager, "is_save_dialog")
lifecycleScope.launch {
dialog.resultSharedFlow.collect {
when (it) {
ContinueDialogFragment.RESULT_YES -> {
viewModel.saveTask()
closeFragment()
}
ContinueDialogFragment.RESULT_NO -> {
closeFragment()
}
ContinueDialogFragment.RESULT_CONTINUE -> {
// dont close fragment
}
}
}
}
}
class ContinueDialogFragment(
#StringRes private val titleStringId: Int,
#StringRes private val messageStringId: Int? = null
) : DialogFragment() {
private val _resultSharedFlow = MutableSharedFlow<Int>(1)
val resultSharedFlow = _resultSharedFlow.asSharedFlow()
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
return activity?.let { context ->
AlertDialog.Builder(context)
.setTitle(getString(titleStringId))
.setMessage(messageStringId?.let { getString(it) })
.setPositiveButton(getString(R.string.dialog_yes)) { _, _ ->
_resultSharedFlow.tryEmit(RESULT_YES)
}
.setNegativeButton(getString(R.string.dialog_no)) { _, _ ->
_resultSharedFlow.tryEmit(RESULT_NO)
}
.setNeutralButton(getString(R.string.dialog_continue)) { _, _ ->
_resultSharedFlow.tryEmit(RESULT_CONTINUE)
}
.create()
} ?: throw IllegalStateException("Activity cannot be null")
}
companion object {
const val RESULT_YES = 1
const val RESULT_NO = 0
const val RESULT_CONTINUE = 2
}
}

When a Flow completes depends on its original source. A Flow built with flowOf or asFlow() ends once it reaches the last item in its list. A Flow built with the flow builder could be finite or infinite, depending on whether it has an infinite loop in it.
A flow created with MutableSharedFlow is always infinite. It stays open until the coroutine collecting it is cancelled. Therefore, you are leaking the dialog fragment with your current code because you are hanging onto its MutableSharedFlow reference, which is capturing the dialog fragment reference. You need to manually cancel your coroutine or collection.
Or more simply, you could use first() instead of collect { }.
Side note, this is a highly unusual uses of a Flow, which is why you're running into this fragile condition in the first place. A Flow is for a series of emitted objects, not for a single object.
It is also very fragile that you're collecting this flow is a function called save(), but you don't appear to be doing anything in save() to store the instance state such that if the activity/fragment is recreated you'll start collecting from the flow again. So, if the screen rotates, the dialog will reappear, the user could click the positive button, and nothing will be saved. It will silently fail.
DialogFragments are pretty clumsy to work with in my opinion. Anyway, I would take the easiest route and directly put your behaviors in the DialogFragment code instead of trying to react to the result back in your parent fragment. But if you don't want to do that, you need to go through the pain of calling back through to the parent fragment. Alternatively, you could use a shared ViewModel between these two fragments that will handle the dialog results.

I believe you will have a memory leak of DialogFragment: ParentFragment will be referencing the field dialog.resultSharedFlow until the corresponding coroutine finishes execution. The latter may never happen while ParentFragment is open because dialog.resultSharedFlow is an infinite Flow. You can call cancel() to finish the coroutine execution and make dialog eligible for garbage collection:
lifecycleScope.launch {
dialog.resultSharedFlow.collect {
when (it) {
ContinueDialogFragment.RESULT_YES -> {
viewModel.saveTask()
closeFragment()
cancel()
}
ContinueDialogFragment.RESULT_NO -> {
closeFragment()
cancel()
}
ContinueDialogFragment.RESULT_CONTINUE -> {
// dont close fragment
}
}
}
}

Related

Why state flow calls callectLatest multiple times?

So, I would like to use StateFlow instead of LiveData, but I can not figure out what's the problem with my logic.
I have a flow, which has a default null value. When I open a dialog which contains a some datas, after that I select one data, I emit the new value to the flow.
In the first time, after the dialog closed, collectLatest called, and I get the null value (init), after the emit, I get the new value, it is good. But If I open the dialog again, and select value, and close the dialog, the collectLatest fun called 3-times, and I again open the dialog... and collectLatest called 4 times and so on.
So this is very bad behavior, and I'm sure , I did something wrong, but I don't see the bug.
In the liveData the expected behavior is after the dialog close, that the observer fun is called just once. I would like to achive this.
I also checked, that I emit the new value only once, so there is no reason why collectLatest fire multiple times.
ViewModel:
private val _previousManufacture = MutableStateFlow<PreviousManufactureView?>(null)
val previousManufacture = _previousManufacture.asStateFlow()
private suspend fun setPreviousManufactureByMachineId(machineId: String) {
val result = stateReportRepository.getPreviousManufactureByMachineId(machineId)
if (result is Result.Success) {
_previousManufacture.emit(result.data)
} else {
_previousManufacture.emit(null)
}
}
Fragment:
lifecycleScope.launchWhenCreated {
viewModel.previousManufacture.collectLatest {
var d = it
}
}
[Update]
Fragment:
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.vm = viewModel
initFlows()
}
private fun initFlows() {
lifecycleScope.launchWhenCreated {
viewModel.openStateOfWorkflowBrowser.collectLatest {
openStateOfWorkflowSelectionDialog()
}
}
...
}
Sorry, I missed this before in my comment, but I think the problem is that you are calling launchWhenCreated in the lifecycleScope of the Fragment, not in its viewLifecycle.lifecycleScope. So if the Fragment is reused (like after a dialog fragment has a appeared), the old collector is not cancelled and a new one is added, because the lifecycle of the Fragment has not ended, only the lifecycle of its previous view. You should almost always use viewLifecycle.lifecycleScope when you are using coroutines in a Fragment.

conditional navigation in compose, without click

I am working on a compose screen, where on application open, i redirect user to profile page. And if profile is complete, then redirect to user list page.
my code is like below
#Composable
fun UserProfile(navigateToProviderList: () -> Unit) {
val viewModel: MainActivityViewModel = viewModel()
if(viewModel.userProfileComplete == true) {
navigateToProviderList()
return
}
else {
//compose elements here
}
}
but the app is blinking and when logged, i can see its calling the above redirect condition again and again. when going through doc, its mentioned that we should navigate only through callbacks. How do i handle this condition here? i don't have onCLick condition here.
Content of composable function can be called many times.
If you need to do some action inside composable, you need to use side effects
In this case LaunchedEffect should work:
LaunchedEffect(viewModel.userProfileComplete == true) {
if(viewModel.userProfileComplete == true) {
navigateToProviderList()
}
}
In the key(first argument of LaunchedEffect) you need to specify some key. Each time this key changes since the last recomposition, the inner code will be called. You may put Unit there, in this case it'll only be called once, when the view appears at the first place
The LaunchedEffect did not work for me since I wanted to use it in UI thread but it wasn't for some reason :/
However, I made this for my self:
#Composable
fun <T> SelfDestructEvent(liveData: LiveData<T>, onEvent: (argument: T) -> Unit) {
val previousState = remember { mutableStateOf(false) }
val state by liveData.observeAsState(null)
if (state != null && !previousState.value) {
previousState.value = true
onEvent.invoke(state!!)
}
}
and you use it like this in any other composables:
SingleEvent(viewModel.someLiveData) {
//your action with that data, whenever it was triggered, but only once
}

How to cancel collect coroutine StateFlow?

I have collect flow from shared viewmodel in fragment :
private val viewModel: MyViewModel by sharedViewModel()
private fun observeViewModelStateFlowData() {
job = lifecycleScope.launch {
viewModel.stateFlowData.collect {
when (it) {
is ViewStates.Success -> handleSuccess(it.data)
}
}
}
}
in ViewModel :
private val _stateFlowData = MutableStateFlow<ViewStates<Model>>(ViewStates.Idle)
val stateFlowData: StateFlow<ViewStates<Model>> get() = _stateFlowData
but when I go to next fragment and back to this fragment again, flow collect again.
I cancel the job in onStop() lifecycle method of fragment :
override fun onStop() {
job?.cancel()
super.onStop()
}
but not cancel and collect again!!!
This happens even when I leave the activity (when the viewmodel is cleared) and come back to activity again!!!
How can I do this so that I can prevent the collecting of flow ?
Well you have to know something about coroutine. If we just call cancel, it doesn’t mean that the coroutine work will just stop. If you’re performing some relatively heavy computation, like reading from multiple files, there’s nothing that automatically stops your code from running.
You need to make sure that all the coroutine work you’re implementing is cooperative with cancellation, therefore you need to check for cancellation periodically or before beginning any long running work. Try to add check before handling a result.
job = lifecycleScope.launch {
viewModel.stateFlowData.collect {
ensureActive()
when (it) {
is ViewStates.Success -> handleSuccess(it.data)
}
}
}
}
For more info take a look on this article https://medium.com/androiddevelopers/cancellation-in-coroutines-aa6b90163629

Android ViewModel MutableLiveData update multiple times

Scenario
Hi,
I have an Activity with a ViewPager. In the ViewPagerAdapter, I create instances of a same fragment with different data.
And in each instance I initialize a ViewModel
val dataViewModelFactory = this.activity?.let { DataViewModelFactory(it) }
mainViewModel = ViewModelProviders.of(this, dataViewModelFactory).get(MainViewModel::class.java)
In my fragment, I observe two MutableLiveData when I call APIs
mainViewModel.isResponseSuccessful.observe(this, Observer { it ->
if(it) {
//do Something
}else{
Toast.makeText(activity, "Error in Sending Request", Toast.LENGTH_SHORT).show()
}
})
mainViewModel.isLoading.observe(this, Observer {
if (it) {
println("show progress")
} else {
println("dismiss progress")
}
})
In each fragment, on a button click I load another fragment. And if required call and API to fetch data.
PROBLEM
The code comes to the observe block multiple times in my fragment. When I comeback from one fragment to previous fragment, even though no API is called, the code on observe block is executed.
What I tried
I tried using an activity instance in the ViewModel initialization
mainViewModel = ViewModelProviders.of(activity,dataViewModelFactory).get(MainViewModel::class.java)
But it did not work.
Please help,
If you want to prevent multiple calls of your observer u can just change MutableLiveData to SingleLiveEvent. Read this
It might help you:
import java.util.concurrent.atomic.AtomicBoolean
class OneTimeEvent<T>(
private val value: T
) {
private val isConsumed = AtomicBoolean(false)
private fun getValue(): T? =
if (isConsumed.compareAndSet(false, true)) value
else null
fun consume(block: (T) -> Unit): T? =
getValue()?.also(block)
}
fun <T> T.toOneTimeEvent() =
OneTimeEvent(this)
First, when you want to post a value on LiveData, use toOneTimeEvent() extension function to wrap it in a OneTimeEvent:
liveData.postValue(yourObject.toOneTimeEvent())
Second, when you are observing on the LiveData, use consume { } function on the delivered value to gain the feature of OneTimeEvent. You'll be sure that the block of consume { } will be executed only once.
viewModel.liveData.observe(this, Observer {
it.consume { yourObject ->
// TODO: do whatever with 'yourObject'
}
})
In this case, when the fragment resumes, your block of code does not execute again.

ViewModel refetching data again with distinctUntilChanged()

I have a Fragment that I want to do a fetch once on its data, I have used distinctUntilChanged() to fetch just once because my location is not changing during this fragment.
Fragment
private val viewModel by viewModels<LandingViewModel> {
VMLandingFactory(
LandingRepoImpl(
LandingDataSource()
)
)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val sharedPref = requireContext().getSharedPreferences("LOCATION", Context.MODE_PRIVATE)
val nombre = sharedPref.getString("name", null)
location = name!!
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupRecyclerView()
fetchShops(location)
}
private fun fetchShops(localidad: String) {
viewModel.setLocation(location.toLowerCase(Locale.ROOT).trim())
viewModel.fetchShopList
.observe(viewLifecycleOwner, Observer {
when (it) {
is Resource.Loading -> {
showProgress()
}
is Resource.Success -> {
hideProgress()
myAdapter.setItems(it.data)
}
is Resource.Failure -> {
hideProgress()
Toast.makeText(
requireContext(),
"There was an error loading the shops.",
Toast.LENGTH_SHORT
).show()
}
}
})
}
Viewmodel
private val locationQuery = MutableLiveData<String>()
fun setLocation(location: String) {
locationQuery.value = location
}
val fetchShopList = locationQuery.distinctUntilChanged().switchMap { location ->
liveData(viewModelScope.coroutineContext + Dispatchers.IO) {
emit(Resource.Loading())
try{
emit(repo.getShopList(location))
}catch (e:Exception){
emit(Resource.Failure(e))
}
}
}
Now, if I go to the next fragment and press back, this fires again, I know that maybe this is because the fragment is recreating and then passing a new instance of viewmodel and thats why the location is not retained, but if I put activityViewModels as the instance of the viewmodel, it also happends the same, the data is loaded again on backpress, this is not acceptable since going back will get the data each time and this is not server efficient for me, I need to just fetch this data when the user is in this fragment and if they press back to not fetch it again.
Any clues ?
I'm using navigation components, so I cant use .add or do fragment transactions, I want to just fetch once on this fragment when creating it first time and not refetching on backpress of the next fragment
TL;DR
You need to use a LiveData that emits its event only once, even if the ui re-subscribe to it. for more info and explanation and ways to fix, continue reading.
When you go from Fragment 1 -> Fragment 2, Fragment 1 is not actually destroyed right away, it just un-subscribe from your ViewModel LiveData.
Now when you go back from F2 to F1, the fragment will re-subscribe back to ViewModel LiveData, and since the LiveData is - by nature - state holder, then it will re-emit its latest value right away, causing the ui to rebind.
What you need is some sort of LiveData that won't emit an event that has been emitted before.
This is common use case with LiveData, there's a pretty nice article talking about this need for a similar LiveData for different types of use cases, you can read it here.
Although the article proposed a couple of solutions but those can be a bit of an overkill sometimes, so a simpler solution would be using the following ActionLiveView
// First extend the MutableLiveData class
class ActionLiveData<T> : MutableLiveData<T>() {
#MainThread
override fun observe(owner: LifecycleOwner, observer: Observer<T?>) {
// Being strict about the observer numbers is up to you
// I thought it made sense to only allow one to handle the event
if (hasObservers()) {
throw Throwable("Only one observer at a time may subscribe to a ActionLiveData")
}
super.observe(owner, Observer { data ->
// We ignore any null values and early return
if (data == null) return
observer.onChanged(data)
// We set the value to null straight after emitting the change to the observer
value = null
// This means that the state of the data will always be null / non existent
// It will only be available to the observer in its callback and since we do not emit null values
// the observer never receives a null value and any observers resuming do not receive the last event.
// Therefore it only emits to the observer the single action so you are free to show messages over and over again
// Or launch an activity/dialog or anything that should only happen once per action / click :).
})
}
// Just a nicely named method that wraps setting the value
#MainThread
fun sendAction(data: T) {
value = data
}
}
You can find more explainiation for ActionLiveData in this link if you want.
I would advise using the ActionLiveData class, I've been using it for small to medium project size and it's working alright so far, but again, you know your use cases better than me. :)

Categories

Resources