SharedFlow in Android project not working as expected - android

I was trying to pass events from UI to viewModel using sharedFlow
this is my viewmodel class
class MainActivityViewModel () : ViewModel() {
val actions = MutableSharedFlow<Action>()
private val _state = MutableStateFlow<State>(State.Idle)
val state: StateFlow<State> = _state
init {
viewModelScope.launch { handleIntents() }
}
suspend fun handleIntents() {
actions.collect {
when (it) {...}
}
}
}
and this is how i am emiting actions
private fun emitActions(action: Action) {
lifecycleScope.launch {
vm.actions.emit(action)
}
}
For the first time emission happening as expected, but then it is not emitting/collecting from the viewmodel.
Am i doing anything wrong here??

When I used collectLatest() instead of collect() it worked as expected

collectLatest() instead of collect() hides the problem
when you do launch{ collect() } the collect will suspend whatever it is in launch code block
so if you do
launch{
events.collect {
otherEvent.collect() //this will suspend the launched block indefinetly
} }
solution is to wrap every collect in it's own launch{} code block, collectLatest will just cancel the suspend if new event is emitted

Related

How to call a function from in ViewModel in viewModelScope?

In the repository class have this listener:
override fun getState(viewModelScope: CoroutineScope) = callbackFlow {
val listener = FirebaseAuth.AuthStateListener { auth ->
trySend(auth.currentUser == null)
}
auth.addAuthStateListener(listener)
awaitClose {
auth.removeAuthStateListener(listener)
}
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), auth.currentUser == null)
In my ViewModel class I call getState function that returns a StateFlow<Boolean> using:
fun getState() = repo.getState(viewModelScope)
And I collect the data:
setContent {
val state = viewModel.getState().collectAsState().value
}
If I change in ViewModel:
fun getState() = viewModelScope.launch {
repo.getState(this)
}
So it can be called from a viewModelScope, I cannot collect the data anymore, as .collectAsState() appears in red. How to solve this? Any help would be greatly appreciated.
I'm not sure why you're trying to do this:
fun getState() = viewModelScope.launch {
repo.getState(this)
}
This code launches an unnecessary coroutine (doesn't call any suspending or blocking code) that get's a StateFlow reference and promptly releases the reference, and the function itself returns a Job (the launched coroutine). When you launch a coroutine, the coroutine doesn't produce any returned value. It just returns a Job instance that you can use to wait for it to finish or to cancel it early.
Your repository function already creates a StateFlow that runs in the passed scope, and you're already passing it viewModelScope, so your StateFlow was already running in the viewModelScope in your original code fun getState() = repo.getState(viewModelScope).
Use live data to send your result of state flow from view model to activity.
In your view model do like this:
var isActive = MutableLiveData<Boolean>();
fun getState() {
viewModelScope.launch {
repo.getState(this).onStart {
}
.collect(){
isActive.value = it;
}
}
}
In your activity observer your liveData like this:
viewModel.isActive.observe(this, Observer {
Toast.makeText(applicationContext,it.toString(),Toast.LENGTH_LONG).show()
})
Hopefully it will help.

How do I cancel a coroutine run inside a withContext?

I have a Repository defined as the following.
class StoryRepository {
private val firestore = Firebase.firestore
suspend fun fetchStories(): QuerySnapshot? {
return try {
firestore
.collection("stories")
.get()
.await()
} catch(e: Exception) {
Log.e("StoryRepository", "Error in fetching Firestore stories: $e")
null
}
}
}
I also have a ViewModel like this.
class HomeViewModel(
application: Application
) : AndroidViewModel(application) {
private var viewModelJob = Job()
private val uiScope = CoroutineScope(Dispatchers.Main + viewModelJob)
private val storyRepository = StoryRepository()
private var _stories = MutableLiveData<List<Story>>()
val stories: LiveData<List<Story>>
get() = _stories
init {
uiScope.launch {
getStories()
}
uiScope.launch {
getMetadata()
}
}
private suspend fun getStories() {
withContext(Dispatchers.IO) {
val snapshots = storyRepository.fetchStories()
// Is this correct?
if (snapshots == null) {
cancel(CancellationException("Task is null; local DB not refreshed"))
return#withContext
}
val networkStories = snapshots.toObjects(NetworkStory::class.java)
val stories = NetworkStoryContainer(networkStories).asDomainModel()
_stories.postValue(stories)
}
}
suspend fun getMetadata() {
// Does some other fetching
}
override fun onCleared() {
super.onCleared()
viewModelJob.cancel()
}
}
As you can see, sometimes, StoryRepository().fetchStories() may fail and return null. If the return value is null, I would like to not continue what follows after the checking for snapshots being null block. Therefore, I would like to cancel that particular coroutine (the one that runs getStories() without cancelling the other coroutine (the one that runs getMetadata()). How do I achieve this and is return-ing from withContext a bad-practice?
Although your approach is right, you can always make some improvements to make it simpler or more idiomatic (especially when you're not pleased with your own code).
These are just some suggestions that you may want to take into account:
You can make use of Kotlin Scope Functions, or more specifically the let function like this:
private suspend fun getStories() = withContext(Dispatchers.IO) {
storyRepository.fetchStories()?.let { snapshots ->
val networkStories = snapshots.toObjects(NetworkStory::class.java)
NetworkStoryContainer(networkStories).asDomainModel()
} ?: throw CancellationException("Task is null; local DB not refreshed")
}
This way you'll be returning your data or throwing a CancellationException if null.
When you're working with coroutines inside a ViewModel you have a CoroutineScope ready to be used if you add this dependendy to your gradle file:
androidx.lifecycle:lifecycle-viewmodel-ktx:{version}
So you can use viewModelScope to build your coroutines, which will run on the main thread:
init {
viewModelScope.launch {
_stories.value = getStories()
}
viewModelScope.launch {
getMetadata()
}
}
You can forget about cancelling its Job during onCleared since viewModelScope is lifecycle-aware.
Now all you have left to do is handling the exception with a try-catch block or with the invokeOnCompletion function applied on the Job returned by the launch builder.

How to wait until a bunch of calls ends to make another call

I'm using RxJava and I know about concat, and I guess it does fit to me, because I want to finish first all of first call and then do the second one but I don't know how to implement it.
I have this from now :
private fun assignAllAnswersToQuestion(questionId: Long) {
answerListCreated.forEach { assignAnswerToQuestion(questionId, it.id) }
}
private fun assignAnswerToQuestion(questionId: Long, answerId: Long) {
disposable = questionService.addAnswerToQuestion(questionId,answerId,MyUtils.getAccessTokenFromLocalStorage(context = this))
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
result -> //Do nothing it should call the next one
},
{ error -> toast(error.message.toString())}
)
}
But then, once this is finished all of this forEach I'd like to do something like this :
private fun assignAllAnswersToQuestion(questionId: Long) {
answerListCreated.forEach { assignAnswerToQuestion(questionId, it.id)
anotherCallHere(questionId) //Do it when the first forEach is finished!!
}
Any idea?
Also, is a way to do it with coroutines this?
I think you have to .map your list (answerListCreated) to a list of Flowables, and then use Flowable.zip on this list.
zip is used to combine the results of the Flowables into a single result. Since you don't need these results we ignore them.
After zip you are sure that all previous Flowables ended, and you can .flatMap to execute your next call (assuming anotherCallHere returns a Flowable.
In the end, it will be something like:
val flowableList = answerListCreated.map { assignAnswerToQuestion(questionId, it.id) }
disposable = Flowable.zip(flowableList) { /* Ignoring results */ }
.flatMap { anotherCallHere(questionId) }
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe {
// ...
}
It should be noted that if any of the calls fails, the whole chain will fail (onError will be called).
I'm new to coroutines but I think I can answer for them:
You can use coroutines runBlocking {} for this.
private fun assignAllAnswersToQuestion(questionId: Long) = launch {
runBlocking {
answerListCreated.forEach { assignAnswerToQuestion(questionId, it.id) }
}
anotherCallHere(questionId)
}
private fun assignAnswerToQuestion(questionId: Long, answerId: Long) = launch (Dispatchers.IO) {
questionService.addAnswerToQuestion(
questionId,
answerId,
MyUtils.getAccessTokenFromLocalStorage(context = this)
)
}
launch {} returns a Job object which becomes a child job of the parent coroutine. runBlocking {} will block until all its child jobs have finished, (an alternative is to use launch {}.join() which will have the same affect).
Note that I have made both functions wrap their code in a launch {} block.
To be able to call launch {} like this, you will likely want to make your class implement CoroutineScope
class MyActivityOrFragment: Activity(), CoroutineScope {
lateinit var job = SupervisorJob()
private val exceptionHandler =
CoroutineExceptionHandler { _, error ->
toast(error.message.toString()
}
override val coroutineContext = Dispatchers.Main + job + exceptionHandler
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
job = Job()
}
override fun onDestroy() {
super.onDestroy()
job.cancel()
}
...
}

How to unit test coroutine when it contains coroutine delay?

When I add a coroutine delay() in my view model, the remaining part of the code will not be executed.
This is my demo code:
class SimpleViewModel : ViewModel(), CoroutineScope {
override val coroutineContext: CoroutineContext
get() = Dispatchers.Unconfined
var data = 0
fun doSomething() {
launch {
delay(1000)
data = 1
}
}
}
class ScopedViewModelTest {
#Test
fun coroutineDelay() {
// Arrange
val viewModel = SimpleViewModel()
// ActTes
viewModel.doSomething()
// Assert
Assert.assertEquals(1, viewModel.data)
}
}
I got the assertion result:
java.lang.AssertionError:
Expected :1
Actual :0
Any idea how to fix this?
You start a coroutine which suspends for 1 second before setting data to 1. Your test just invokes doSomething but does not wait until data is actually being set. If you add another, longer delay, to the test it will, work:
#Test
fun coroutineDelay() = runBlocking {
...
viewModel.doSomething()
delay(1100)
...
}
You can also make the coroutine return a Deferred which you can wait on:
fun doSomething(): Deferred<Unit> {
return async {
delay(1000)
data = 1
}
}
With await there's no need to delay your code anymore:
val model = SimpleViewModel()
model.doSomething().await()
The first issue in your code is that SimpleViewModel.coroutineContext has no Job associated with it. The whole point of making your view model a CoroutineScope is the ability to centralize the cancelling of all coroutines it starts. So add the job as follows (note the absence of a custom getter):
class SimpleViewModel : ViewModel(), CoroutineScope {
override val coroutineContext = Job() + Dispatchers.Unconfined
var data = 0
fun doSomething() {
launch {
delay(1000)
data = 1
}
}
}
Now your test code can ensure it proceeds to the assertions only after all the jobs your view model launched are done:
class ScopedViewModelTest {
#Test
fun coroutineDelay() {
// Arrange
val viewModel = SimpleViewModel()
// ActTes
viewModel.doSomething()
// Assert
runBlocking {
viewModel.coroutineContext[Job]!!.children.forEach { it.join() }
}
Assert.assertEquals(1, viewModel.data)
}
}

Wait For Data Inside a Listener in a Coroutine

I have a coroutine I'd like to fire up at android startup during the splash page. I'd like to wait for the data to come back before I start the next activity. What is the best way to do this? Currently our android is using experimental coroutines 0.26.0...can't change this just yet.
UPDATED: We are now using the latest coroutines and no longer experimental
onResume() {
loadData()
}
fun loadData() = GlobalScope.launch {
val job = GlobalScope.async {
startLibraryCall()
}
// TODO await on success
job.await()
startActivity(startnewIntent)
}
fun startLibraryCall() {
val thirdPartyLib() = ThirdPartyLibrary()
thirdPartyLib.setOnDataListener() {
///psuedocode for success/ fail listeners
onSuccess -> ///TODO return data
onFail -> /// TODO return other data
}
}
The first point is that I would change your loadData function into a suspending function instead of using launch. It's better to have the option to define at call site how you want to proceed with the execution. For example when implementing a test you may want to call your coroutine inside a runBlocking. You should also implement structured concurrency properly instead of relying on GlobalScope.
On the other side of the problem I would implement an extension function on the ThirdPartyLibrary that turns its async calls into a suspending function. This way you will ensure that the calling coroutine actually waits for the Library call to have some value in it.
Since we made loadData a suspending function we can now ensure that it will only start the new activity when the ThirdPartyLibrary call finishes.
import kotlinx.coroutines.*
import kotlin.coroutines.*
class InitialActivity : AppCompatActivity(), CoroutineScope {
private lateinit var masterJob: Job
override val coroutineContext: CoroutineContext
get() = Dispatchers.Main + masterJob
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
masterJob = Job()
}
override fun onDestroy() {
super.onDestroy()
masterJob.cancel()
}
override fun onResume() {
this.launch {
val data = ThirdPartyLibrary().suspendLoadData()
// TODO: act on data!
startActivity(startNewIntent)
}
}
}
suspend fun ThirdPartyLibrary.suspendLoadData(): Data = suspendCoroutine { cont ->
setOnDataListener(
onSuccess = { cont.resume(it) },
onFail = { cont.resumeWithException(it) }
)
startLoadingData()
}
You can use LiveData
liveData.value = job.await()
And then add in onCreate() for example
liveData.observe(currentActivity, observer)
In observer just wait until value not null and then start your new activity
Observer { result ->
result?.let {
startActivity(newActivityIntent)
}
}

Categories

Resources