I have two fragments: GeneralInformationFrament and HomeFragment, and I have one ViewModel associated for each of them. Inside both of them I have a method like:
private fun getInstallationSiteInformation() {
launch(Dispatchers.IO) {
currentFragment.getInstallationSiteOfUser(employeeId).collect {
when(it.status){
Resource.Status.LOADING -> {
withContext(Dispatchers.Main){
//Code
}
}
Resource.Status.SUCCESS -> {
withContext(Dispatchers.Main){
//code
}
}
Resource.Status.ERROR -> {
withContext(Dispatchers.Main) {
//more code
}
}
}
}
}
}
Inside each viewModel I have:
fun getInstallationSiteOfUser(employeeId: Int): Flow<Resource<InstallationSiteEntity?>> =
installationSiteRepository.getInstallationSiteOfUser(employeeId).map{
installationSiteResponse ->
when(installationSiteResponse.status){
Resource.Status.LOADING -> {
Resource.loading(null)
}
Resource.Status.SUCCESS -> {
val installationSite = installationSiteResponse.data
Resource.success(installationSite)
}
Resource.Status.ERROR -> {
Resource.error(installationSiteResponse.message!!, null)
}
}
}
and in the InstallationSiteRepository I have:
fun getInstallationSiteOfUser(employeeId: Int): Flow<Resource<InstallationSiteEntity>> = flow{
emit(Resource.loading(null))
val installationSiteOfEmployee = employeesDao.getEmployeeDetailed(employeeId)
remoteApiService.getInstallationSiteOfEmployee(employeeId).collect {apiResponse ->
when(apiResponse){
is ApiSuccessResponse -> {
apiResponse.body?.let {
installationSitesDao.insertInstallationSite(it.installationSite)}
emitAll(installationSitesDao.getInstallationSiteFromEmployeeId(employeeId).map { installationData ->
Resource.success(installationData)
})
}
is ApiErrorResponse -> {
emitAll(installationSitesDao.getInstallationSiteFromEmployeeId(employeeId).map { installationData ->
Resource.error(apiResponse.errorMessage, installationData)
})
}
}
}
}
Soon after the transition between GeneralInformationFragment to HomeFragment the method getInstallationSiteInformation() is called in onViewCreated() so the behavior that I am encountering is that the flows are being collected in both fragments one after another and because one of the fragments is not available anymore I am getting a NullPointerException. My question is: When a flow source emits, every target collecting it gets the values? Is it possible what I am describing? Shouldn't the flow inside GeneralInformationFragment have been canceled and stopped receiving it ?
[EDIT 1]
In the top of my Fragments there is:
#AndroidEntryPoint
class GeneralInformationFragment : Fragment(), CoroutineScope {
private var job = Job()
override val coroutineContext: CoroutineContext
get() = Dispatchers.IO + job
And in OnDestroy() of the Fragments:
override fun onDestroy() {
super.onDestroy()
job.cancel()
}
When a flow source emits, every target collecting it gets the values?
Yes. Flows are cold streams. They do nothing until a receiver calls collect. There is no restriction on multiple calls from receivers. Flow will emit its values for each caller.
Shouldn't the flow inside GeneralInformationFragment have been canceled and stopped receiving it ?
You need to cancel the Flow collection. In getInstallationSiteInformation(), launch(Dispatchers.IO) {/**/} will return a Job. You can keep a reference to that Job and cancel when you want.
Alternatively, you can cancel the CoroutineScope.
Your CoroutineScope in your code is unclear, does your Fragment implement CoroutineScope maybe?
There are "out the box" CoroutineScopes for a Fragment you could use instead: lifecycleScope and viewLifecycleOwner.lifecycleScope.
They will handle the lifecycle (cancellation) for you. lifecycleScope is tied to the Fragment lifecyle, whereas viewLifecycleOwner.lifecycleScope is tied to the Fragment View lifecycle, onViewCreated to onDestroyView
I hope this helps at least a bit.
Related
I am doing multiple network requests in parallel and monitoring the result using a Stateflow.
Each network request is done in a separate flow, and I use combine to push the latest status on my Stateflow. Here's my code:
Repo class:
fun networkRequest1(id: Int): Flow<Resource<List<Area>>> =
flow {
emit(Resource.Loading())
try {
val areas = retrofitInterface.getAreas(id)
emit(Resource.Success(areas))
} catch (throwable: Throwable) {
emit(
Resource.Error()
)
)
}
}
fun networkRequest2(id: Int): Flow<Resource<List<Area>>> = //same code as above for simplicity
fun networkRequest3(id: Int): Flow<Resource<List<Area>>> = //same code as above for simplicity
fun networkRequest4(id: Int): Flow<Resource<List<Area>>> = //same code as above for simplicity
ViewModel class:
val getDataCombinedStateFlow: StateFlow<Resource<HashMap<String, Resource<out List<Any>>>>?> =
getDataTrigger.flatMapLatest {
withContext(it) {
combine(
repo.networkRequest1(id: Int),
repo.networkRequest2(id: Int),
repo.networkRequest3(id: Int),
repo.networkRequest4(id: Int)
) { a,
b,
c,
d
->
hashMapOf(
Pair("1", a),
Pair("2",b),
Pair("3", c),
Pair("4", d),
)
}.flatMapLatest {
val progress = it
var isLoading = false
flow<Resource<HashMap<String, Resource<out List<Any>>>>?> {
emit(Resource.Loading())
progress.forEach { (t, u) ->
if (u is Resource.Error) {
emit(Resource.Error(error = u.error!!))
// I want to cancel here, as I no longer care if 1 request fails
return#flow
}
if (u is Resource.Loading) {
isLoading = true
}
}
if (isLoading) {
emit(Resource.Loading())
return#flow
}
if (!isLoading) {
emit(Resource.Success(progress))
}
}
}
}
}.stateIn(viewModelScope, SharingStarted.Lazily, null)
View class:
viewLifecycleOwner.lifecycleScope.launchWhenCreated() {
viewModel.getDataCombinedStateFlow.collect {
val result = it ?: return#collect
binding.loadingErrorState.apply {
if (result is Resource.Loading) {
//show smth
}
if (result is Resource.Error) {
//show error msg
}
if (result is Resource.Success) {
//done
}
}
}
}
I want to be able to cancel all work after a Resource.Error is emitted, as I no longer want to wait or do any related work for the response of other API calls in case one of them fails.
How can I achieve that?
I tried to cancel the collect, but the flows that build the Stateflow keep working and emmit results. I know that they won't be collected but still, I find this a waste of resources.
I think this whole situation is complicated by the fact that you have source flows just to precede what would otherwise be suspend functions with a Loading state. So then you're having to merge them and filter out various loading states, and your end result flow keeps repeatedly emitting a loading state until all the sources are ready.
If you instead have basic suspend functions for your network operations, for example:
suspend fun networkRequest1(id: Int): List<Area> =
retrofitInterface.getAreas(id)
Then your view model flow becomes simpler. It doesn't make sense to use a specific context just to call a flow builder function, so I left that part out. (I'm also confused as to why you have a flow of CoroutineContexts.)
I also think it's much cleaner if you break out the request call into a separate function.
private fun makeParallelRequests(id: Int): Map<String, Resource<out List<Any>> = coroutineScope {
val results = listOf(
async { networkRequest1(id) },
async { networkRequest2(id) },
async { networkRequest2(id) },
async { networkRequest4(id) }
).awaitAll()
.map { Resource.Success(it) }
listOf("1", "2", "3", "4").zip(results).toMap()
}
val dataCombinedStateFlow: StateFlow<Resource<Map<String, Resource<out List<Any>>>>?> =
getDataTrigger.flatMapLatest {
flow {
emit(Resource.Loading())
try {
val result = makeParallelRequests(id)
emit(Resource.Success(result))
catch (e: Throwable) {
emit(Resource.Error(e))
}
}
}
I agree with #Tenfour04 that those nested flows are overly complicated and there are several ways to simplify this (#Tenfour04's solution is a good one).
If you don't want to rewrite everything then you can fix that one line that breaks the structured concurrency:
.stateIn(viewModelScope, SharingStarted.Lazily, null)
With this the whole ViewModel flow is started in the ViewModel's scope while the view starts the collect from a separate scope (viewLifecycleOwner.lifecycleScope which would be the Fragment / Activity scope).
If you want to cancel the flow from the view, you need to use either the same scope or expose a cancel function that would cancel the ViewModel's scope.
If you want to cancel the flow from the ViewModel itself (at the return#flow statement) then you can simply add:
viewModelScope.cancel()
Fragment
private fun makeApiRequest() {
vm.getRandomPicture()
var pictureElement = vm.setRandomPicture()
GlobalScope.launch(Dispatchers.Main) {
// what about internet
if (pictureElement != null && pictureElement!!.fileSizeBytes!! < 400000) {
Glide.with(requireContext()).load(pictureElement!!.url)
.into(layout.ivRandomPicture)
layout.ivRandomPicture.visibility = View.VISIBLE
} else {
getRandomPicture()
}
}
}
viewmodel
fun getRandomPicture() {
viewModelScope.launch {
getRandomPictureItemUseCase.build(Unit).collect {
pictureElement.value = it
Log.d("inspirationquotes", "VIEWMODEL $pictureElement")
Log.d("inspirationquotes", "VIEWMODEL VALUE ${pictureElement.value}")
}
}
}
fun setRandomPicture(): InspirationQuotesDetailsResponse? {
return pictureElement.value
}
Flow UseCase
class GetRandomPictureItemUseCase #Inject constructor(private val api: InspirationQuotesApi): BaseFlowUseCase<Unit, InspirationQuotesDetailsResponse>() {
override fun create(params: Unit): Flow<InspirationQuotesDetailsResponse> {
return flow{
emit(api.getRandomPicture())
}
}
}
My flow task from viewmodel doesn't goes on time. I do not know how to achieve smooth downloading data from Api and provide it further.
I was reading I could use runBlocking, but it is not recommended in production as well.
What do you use in your professional applications to achieve nice app?
Now the effect is that that image doesn't load or I have null error beacause of my Log.d before GlobalScope in Fragment (it is not in code right now).
One more thing is definding null object I do not like it, what do you think?
var pictureElement = MutableStateFlow<InspirationQuotesDetailsResponse?>(null)
EDIT:
Viewmodel
val randomPicture: Flow<InspirationQuotesDetailsResponse> = getRandomPictureItemUseCase.build(Unit)
fragment
private fun makeApiRequest() = lifecycleScope.launch {
vm.randomPicture
.flowWithLifecycle(lifecycle, Lifecycle.State.STARTED)
.collect { response ->
if (response.fileSizeBytes < 600000) {
Log.d("fragment", "itGetsValue")
Glide.with(requireContext()).load(response.url)
.into(layout.ivRandomPicture)
layout.ivRandomPicture.visibility = View.VISIBLE
} else {
onFloatingActionClick()
}
}
}
Edit2 problem on production, another topic:
Link -> What is the substitute for runBlocking Coroutines in fragments and activities?
First of all, don't use GlobalScope to launch a coroutine, it is highly discouraged and prone to bugs. Use provided lifecycleScope in Fragment:
lifecycleScope.launch {...}
Use MutableSharedFlow instead of MutableStateFlow, MutableSharedFlow doesn't require initial value, and you can get rid of nullable generic type:
val pictureElement = MutableSharedFlow<InspirationQuotesDetailsResponse>()
But I guess we can get rid of it.
Method create() in GetRandomPictureItemUseCase returns a Flow that emits only one value, does it really need to be Flow, or it can be just a simple suspend function?
Assuming we stick to Flow in GetRandomPictureItemUseCase class, ViewModel can look something like the following:
val randomPicture: Flow<InspirationQuotesDetailsResponse> = getRandomPictureItemUseCase.build(Unit)
And in the Fragment:
private fun makeApiRequest() = lifecycleScope.launch {
vm.randomPicture
.flowWithLifecycle(lifecycle, State.STARTED)
.collect { response ->
// .. use response
}
}
Dependency to use lifecycleScope:
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.4.0'
I have following clickListener on one of fragments
binding.btnCamera.setOnClickListener {
val photo = takePhoto()
val prediction = MachineLearning().predict(photo)
val action = FirstFragmentDirections.actionFirstToSecond(prediction)
findNavController().navigate(action)
}
MachinLearing.kt predict() function looks like below which contain a task which is executed asynchronously.
fun predict(photo: Bitmap): Boolean {
val conditions = CustomModelDownloadConditions.Builder()
.requireWifi()
.build()
FirebaseModelDownloader.getInstance()
.getModel(modelName, DownloadType.LOCAL_MODEL_UPDATE_IN_BACKGROUND, conditions)
.addOnSuccessListener { customModel ->
...
}
.addOnFailureListener{
...
}
...
}
I want to halt the navigation to the next fragment until the Task inside the predict() is complete.
Due to asynchronous nature of Task. Task completion less likely to happen before fragment navigation. If fragment navigation happened then the onDestroy() will be called in current fragment causing MachineLearning object to garbage collected, so Task completion will not happen at all.
So how could I wait for Task completion to happen before the fragment navigation?
The methods addOnCompleteListener and addOnFailureListener are callbacks. That means is asynchronous, so whatever the method predict is returning, it can happen before or after those methods are triggered. There are 2 options: add another callback or transform it into suspend.
Callback:
fun predict(photo: Bitmap, predictionDelegate: (Boolean) -> Unit) {
//...
addOnCompleteListener { model ->
//do your thing and then call the callback
predictionDelegate.invoke(yourCalculatedBoolean)
}
.addOnFailureListener...//do the same
}
And then for calling it:
MachineLearning().predict(photo) { prediction ->
val action = FirstFragmentDirections.actionFirstToSecond(prediction)
findNavController().navigate(action)
}
The other alternative is to make it suspend:
suspend fun predict(): Boolean() {
val deferred = CompletableDeferred<Boolean()
//your thing
enter code here
.addOnSuccessListener { model ->
//do your thing and then update the defferred
deferred.complete(yourCalculatedBoolean)
}
.addOnFailureListener...//do the same
return deferred.await()
}
And then for calling it
lifecycleScope.launch {
val prediction = MachineLearning().predict(photo)
val action = FirstFragmentDirections.actionFirstToSecond(prediction)
findNavController().navigate(action)
}
I'm assuming you are in a Fragment or in an Activity due to the binding, so you should have lifecycleScope.
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.
I have a StateFlow coroutine that is shared amongst various parts of my application. When I cancel the CoroutineScope of a downstream collector, a JobCancellationException is propagated up to the StateFlow, and it stops emitting values for all current and future collectors.
The StateFlow:
val songsRelay: Flow<List<Song>> by lazy {
MutableStateFlow<List<Song>?>(null).apply {
CoroutineScope(Dispatchers.IO)
.launch { songDataDao.getAll().distinctUntilChanged().collect { value = it } }
}.filterNotNull()
}
A typical 'presenter' in my code implements the following base class:
abstract class BasePresenter<T : Any> : BaseContract.Presenter<T> {
var view: T? = null
private val job by lazy {
Job()
}
private val coroutineScope by lazy { CoroutineScope( job + Dispatchers.Main) }
override fun bindView(view: T) {
this.view = view
}
override fun unbindView() {
job.cancel()
view = null
}
fun launch(block: suspend CoroutineScope.() -> Unit): Job {
return coroutineScope.launch(block = block)
}
}
A BasePresenter implementation might call launch{ songsRelay.collect {...} }
When the presenter is unbound, in order to prevent leaks, I cancel the parent job. Any time a presenter that was collecting the songsRelay StateFlow is unbound, the StateFlow is essentially terminated with a JobCancellationException, and no other collectors/presenters can collect values from it.
I've noticed that I can call job.cancelChildren() instead, and this seems to work (StateFlow doesn't complete with a JobCancellationException). But then I wonder what the point is of declaring a parent job, if I can't cancel the job itself. I could just remove job altogether, and call coroutineScope.coroutineContext.cancelChildren() to the same effect.
If I do just call job.cancelChildren(), is that sufficient? I feel like by not calling coroutineScope.cancel(), or job.cancel(), I may not be correctly or completely cleaning up the tasks that I have kicked off.
I also don't understand why the JobCancellationException is propagated up the hierarchy when job.cancel() is called. Isn't job the 'parent' here? Why does cancelling it affect my StateFlow?
UPDATE:
Are you sure your songRelay is actually getting cancelled for all presenters? I ran this test and "Song relay completed" is printed, because onCompletion also catches downstream exceptions. However Presenter 2 emits the value 2 just fine, AFTER song relay prints "completed". If I cancel Presenter 2, "Song relay completed" is printed again with a JobCancellationException for Presenter 2's job.
I do find it interesting how the one flow instance will emit once each for each collector subscribed. I didn't realize that about flows.
val songsRelay: Flow<Int> by lazy {
MutableStateFlow<Int?>(null).apply {
CoroutineScope(Dispatchers.IO)
.launch {
flow {
emit(1)
delay(1000)
emit(2)
delay(1000)
emit(3)
}.onCompletion {
println("Dao completed")
}.collect { value = it }
}
}.filterNotNull()
.onCompletion { cause ->
println("Song relay completed: $cause")
}
}
#Test
fun test() = runBlocking {
val job = Job()
val presenterScope1 = CoroutineScope(job + Dispatchers.Unconfined)
val presenterScope2 = CoroutineScope(Job() + Dispatchers.Unconfined)
presenterScope1.launch {
songsRelay.onCompletion { cause ->
println("Presenter 1 Completed: $cause")
}.collect {
println("Presenter 1 emits: $it")
}
}
presenterScope2.launch {
songsRelay.collect {
println("Presenter 2 emits: $it")
}
}
presenterScope1.cancel()
delay(2000)
println("Done test")
}
I think you need to use SupervisorJob in your BasePresenter instead of Job. In general using Job would be a mistake for the whole presenter, because one failed coroutine will cancel all coroutines in the Presenter. Generally not what you want.
OK, so the problem was some false assumptions I made when testing this. The StateFlow is behaving correctly, and cancellation is working as expected.
I was thinking that between Presenters, StateFlow would stop emitting values, but I was actually testing the same instance of a Presenter - so its Job had been cancelled and thus it's not expected to continue collecting Flow emissions.
I also mistakenly took CancellationException messages emitted in onCompletion of the StateFlow to mean the StateFlow itself had been cancelled - when actually it was just saying the downstream Collector/Job had been cancelled.
I've come up with a better implementation of BasePresenter that looks like so:
abstract class BasePresenter<T : Any> : BaseContract.Presenter<T>, CoroutineScope {
var view: T? = null
private var job = Job()
override val coroutineContext: CoroutineContext
get() = job + Dispatchers.Main
override fun bindView(view: T) {
if (job.isCancelled) {
job = Job()
}
this.view = view
}
override fun unbindView() {
job.cancel()
view = null
}
}