Jetpack Compose - rememberCoroutineScope but with keys - android

How can I obtain a coroutine scope bound to a composable but also to some key values? Basically I want to obtain something like this:
#Composable
fun Sth(val sth: Int) {
val coroutineScope = rememberCoroutineScope(sth)
}
I need the scope to be canceled when the call leaves the composition (just like with rememberCoroutineScope), but also when the key sth changes.
Update:
One place in which I need this functionality:
class SomeIndication(
val a: Int,
val b: Int
) : Indication {
#Composable
override fun rememberUpdatedInstance(interactionSource: InteractionSource): IndicationInstance {
val coroutineScope = rememberCoroutineScope(interactionSource)
return remember(interactionSource) {
val sth: State<Int> = sth(a, b, coroutineScope)
object: IndicationInstance {
override fun ContentDrawScope.drawIndication() {
drawContent()
drawSomething(x.value)
}
}
}
}
}

Try to use LaunchedEffect:
#Composable
fun Sth(val sth: Int) {
// `LaunchedEffect` will cancel and re-launch if `sth` changes
LaunchedEffect(sth) {
// call suspend functions
}
}
When LaunchedEffect enters the Composition, it launches a coroutine
with the block of code passed as a parameter. The coroutine will be
cancelled if LaunchedEffect leaves the composition. If
LaunchedEffect is recomposed with different keys, the existing
coroutine will be cancelled and the new suspend function will be
launched in a new coroutine.
Or try to wrap launching a coroutine with a LaunchedEffect:
val coroutineScope = rememberCoroutineScope()
LaunchedEffect(key1 = sth) {
// will be canceled and re-launched if sth is changed
coroutineScope.launch() {
// call suspend functions
}
}

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 to manage coroutines in MainActivity with waiting until done?

I use GlobalScope with runBlocking in MainActivity, but I do not use there a flow just suspend function. I would like to change GlobalScope to other Scope from Coroutines.
UseCase
class UpdateNotificationListItemUseCase #Inject constructor(private val notificationDao: NotificationDao): BaseUpdateBooleanUseCase<Int, Boolean, Boolean, Boolean, Unit>() {
override suspend fun create(itemId: Int, isRead: Boolean, isArchived: Boolean, isAccepted: Boolean){
notificationDao.updateBooleans(itemId, isRead, isArchived, isAccepted)
}
}
MainActivity
val job = GlobalScope.launch { vm.getIdWithUpdate() }
runBlocking {
job.join()
}
MainViewmodel
suspend fun getIdWithUpdate() {
var id = ""
id = notificationAppSessionStorage.getString(
notificationAppSessionStorage.getIncomingKeyValueStorage(),
""
)
if (id != "") {
updateNotificationListItemUseCase.build(id.toInt(), true, false, false)
}
}
}
My proposition:
I have read documentation https://developer.android.com/kotlin/coroutines/coroutines-best-practices
val IODispatcher: CoroutineDispatcher = Dispatchers.IO
val externalScope: CoroutineScope = CoroutineScope(IODispatcher)
suspend {
externalScope.launch(IODispatcher) {
vm.getIdWithUpdate()
}.join()
}
Second option, but here I do not wait until job is done
suspend {
withContext(Dispatchers.IO) {
vm.getIdWithUpdate()
}
}
What do you think about it?
Doesn't it provide to ANR, I also block thread.
You can use lifecycleScope in MainActivity, instead of GlobalScope, to launch a coroutine :
lifecycleScope.launch {
vm.getIdWithUpdate() // calling suspend function
// here suspend function `vm.getIdWithUpdate()` finished execution
// ... do something after suspend function is done
}
To use lifecycleScope add dependency:
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:$2.4.0'
GlobalScope is highly discouraged to use. And there is no need to call job.join(), you can do something in the coroutine builder block after calling vm.getIdWithUpdate(), for example update UI. This coroutine is running using Dispatchers.Main context.

SharedFlow in Android project not working as expected

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

Coroutines best way to use

I am new to coroutines. SO I just wanted to know what is the best way to use them.
My scenraio/use case is I want to make a API call on IO thread and observe the results on Main thread and update the UI. Also when fragment's onDestoryView() is called, then I want to cancel my job.
My fragment asks the presenter for some updates. So my presenter has a coroutine running like this -
class MyPresenter(view: MyView,
private val coroutineCtx: CoroutineContext = Dispatchers.Main) : CoroutineScope {
private val job: Job = Job()
private var view: MyView? = null
init {
this.view= view
}
override val coroutineContext: CoroutineContext
get() = job + coroutineCtx
fun updateData() = launch{
//repo is singleton
val scanResult = repo.updateData()
when(scanResult) {
sucess -> { this.view.showSuccess()}
}
}
fun stopUpdate() {
job.cancel()
}
}
In my repository,
suspend fun updateData(): Result<Void> {
val response = API.update().await()
return response
}
Am I using coroutines correctly? If yes, my job.cancel() never seems to work although I call it from fragment's onDestroyView().
From my point of view you are using coroutine correctly. A few notes:
You don't have to pass view: MyView to the constructor, and assign its value to the property in init block. Instead you can mark view parameter in the constructor as val and it will became a property:
class MyPresenter(private val view: MyView,
private val coroutineCtx: CoroutineContext = Dispatchers.Main) : CoroutineScope {
// you can get rid of the next lines:
private var view: MyView? = null
init {
this.view= view
}
}
launch function returns a Job. You can add an extension function, e.g. launchSilent, to return Unit :
fun CoroutineScope.launchSilent(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
) {
launch(context, start, block)
}
From my observation job.cancel() works correctly: when you invoke it a coroutine must stop. For example if we put some logs:
fun updateData() = launch{
Log.d("Tag", "launch start")
val scanResult = repo.updateData()
when(scanResult) {
success -> { this.view.showSuccess()}
}
Log.d("Tag", "launch end")
}
And add some delay to the repo's updateData() function:
suspend fun updateData(): Result<Void> {
delay(5000)
val response = API.update().await()
return response
}
And, for example, in the fragment after invoking presenter.updateData() we call something like Handler().postDelayed({ presenter.stopUpdate() }, 3000) we won't see "launch end" log in the Logcat.

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