I have a class that launches coroutines and allows them to be cancelled when the Activity/Fragment they are called from is destroyed. However it is not working like I expect. When I back out of the fragment while the operation is running, the coroutine cancel does not take, and I get an NPE when trying to access a View that does not exist anymore.
open class CoroutineLauncher : CoroutineScope {
private val dispatcher: CoroutineDispatcher = Dispatchers.Main
private val supervisorJob = SupervisorJob()
override val coroutineContext: CoroutineContext
get() = dispatcher + supervisorJob
fun launch(action: suspend CoroutineScope.() -> Unit) = launch(block = action)
fun cancelCoroutines() {
supervisorJob.cancelChildren() //coroutineContext.cancelChildren() has same results
}
}
here is the usage
class MyFragment : Fragment {
val launcher = CoroutineLauncher()
fun onSomeEvent() {
launcher.launch {
val result = someSuspendFunction()
if (!isActive) return
// CAUSES CRASH
myTextView.text = result.userText
}
}
override fun onDestroyView() {
super.onDestroyView()
launcher.cancelCoroutines()
}
}
I added log lines to ensure onDestroyView and cancelCoroutines are both being called before the crash. I feel like I'm missing something obvious but what I'm doing seems to be inline with the recipes suggested here: https://proandroiddev.com/android-coroutine-recipes-33467a4302e9
Any ideas?
Ok I figured it out. onSomeEvent was being invoked after cancelCoroutines was called. Since we call cancelChildren on the SupervisorJob instead of cancel, the launcher does not refuse new Jobs, and since the cancel already happened, the new coroutine runs like normal and crashes. I fixed this by checking if the fragment is visible before calling launcher.launch and bailing out of the method if the fragment is not visible.
This could also be fixed by calling supervisorJob.cancel() instead of supervisorJob.cancelChildren(), though that has some other side effects that I didn't want
Related
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
I checked the other questions but none of them seem to address my issue.
I have two suspend funs in my HomeViewModel and I'm calling them in my HomeFragment (with a spinner text parameter).
The two suspend functions in HomeViewModel:
suspend fun tagger(spinner: Spinner){
withContext(Dispatchers.IO){
val vocab: String = inputVocab.value!!
var tagger = Tagger(
spinner.getSelectedItem().toString() + ".tagger"
)
val sentence = tagger.tagString(java.lang.String.valueOf(vocab))
tagAll(sentence)
}
}
suspend fun tagAll(vocab: String){
withContext(Dispatchers.IO){
if (inputVocab.value == null) {
statusMessage.value = Event("Please enter sentence")
}
else {
insert(Vocab(0, vocab))
inputVocab.value = null
}
}
}
and this is how I call them in the HomeFragment:
GlobalScope.launch (Dispatchers.IO) {
button.setOnClickListener {
homeViewModel.tagger(binding.spinner)
}
}
At tagger I get the error "Suspension functions can be called only within coroutine body". But it's already inside a global scope. How can I avoid this problem?
But it's already inside a global scope.
The call to button.onSetClickListener() is in a launched coroutine from a CoroutineScope. However, the lambda expression that you are passing to onSetClickListener() is a separate object, mapped to a separate onClick() function, and that function call is not part of that coroutine.
You would need to change this to:
button.setOnClickListener {
GlobalScope.launch (Dispatchers.IO) {
homeViewModel.tagger(binding.spinner)
}
}
BTW, you may wish to review Google's best practices for coroutines in Android, particularly "The ViewModel should create coroutines".
The problem with this as I see it is that you have to guarantee that registerForActivityResult() is called before your own activity's OnCreate() completes. OnCreate() is obviously not a suspending function, so I can't wrap registerForActivityResult() and ActivityResultLauncher.launch() in a suspendCoroutine{} to wait for the callback, as I can't launch the suspendCoroutine from OnCreate and wait for it to finish before letting OnCreate complete...
...which I did think I might be able to do using runBlocking{}, but I have found that invoking runBlocking inside OnCreate causes the app to hang forever without ever running the code inside the runBlocking{} block.
So my question is whether runBlocking{} is the correct answer but I am using it wrong, or whether there is some other way to use registerForActivityResult() in a coroutine, or whether it is simply not possible at all.
You can do something like this.
Please refer to the implementation below.
class RequestPermission(activity: ComponentActivity) {
private var requestPermissionContinuation: CancellableContinuation<Boolean>? = null
#SuppressLint("MissingPermission")
private val requestFineLocationPermissionLauncher =
activity.registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted ->
requestPermissionContinuation?.resumeWith(Result.success(isGranted))
}
suspend operator fun invoke(permission: String) = suspendCancellableCoroutine<Boolean> { continuation ->
requestPermissionContinuation = continuation
requestFineLocationPermissionLauncher.launch(permission)
continuation.invokeOnCancellation {
requestPermissionContinuation = null
}
}
}
Make sure you initialize this class before onStart of the activity. registerForActivityResult API should be called before onStart of the activity. Refer to the sample below
class SampleActivity : AppCompatActivity() {
val requestPermission: RequestPermission = RequestPermission(this)
override fun onResume() {
super.onResume()
lifecycleScope.launch {
val isGranted = requestPermission(Manifest.permission.ACCESS_FINE_LOCATION)
//Do your actions here
}
}
}
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
}
}
New to kotlin, i tried many examples and tutorials to no avail,
My requirement is:
Ui creates a coroutine that initiates a network connection
on press of a button, that coroutine sends a msg like "i need info about foo" (taken from a edittext?) to the connected server.
coroutine should also be listening for incoming messages and pass those messages to ui (or update ui directly)
coroutine should keep connected to the server unless it is told to close the connection.
I feel that i need global scope, dispatcher.io.
All the examples i found do nothing more than printing values and terminating coroutines and doesn't mention how to implement a long running coroutine which can act as a continues background socket connection.
I do understand that listening from a socket in loop can achieve that but what kind of coroutine do i need here and how do i send messages to and from ui?
Update:Code
// Added comments for new devs who love copy-pasting as it is a nice little startup code
// you can add android:screenOrientation="portrait" in manifest if you want to use this code
class MainActivity2 : AppCompatActivity(), CoroutineScope {
override val coroutineContext: CoroutineContext
get() = Dispatchers.Main + job
private lateinit var job: Job // lateinit = will be initialized later
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main2)
job = Job() // initialized
launch(Dispatchers.IO) { //dispatcher defined, otherwise launch{
// activity also extends coroutine scope so it will be launched in that scope
connector()
}
}
override fun onDestroy() {
// to avoid launching multiple coroutines
// on configuration changes and cancelling it on exit
job.cancel()
super.onDestroy()
}
suspend fun connector() = withContext(Dispatchers.IO){ //defined dispatcher twice here
// useless, once is enough, either here or on launch
//if you defined dispatcher on launch, fun should look like
// suspend fun connector(){
while(true){
// talk to a server
// need to update ui?
withContext(Dispatchers.Main){ // back to UI
// you can update/modify ui here
Toast.makeText(this#MainActivity2, "hi there", Toast.LENGTH_LONG).show()
}
}
}
}
New Question: How do i handle configuration changes :(
Answer: I used Fragments with ViewModels and coroutine launched via viewmodelScope, working flawlessly so far.
From what I understand you want to create a coroutine that listens for responses over a connection. In that case the only thing that you need make sure is that the coroutine should be cancellable, once activity is closed.
suspend fun connector() = withContext(Dispatchers.IO){
try {
// open the connection here
while(isActive) {
var doing : String = "nothing" // fetched from a network call
withContext(Dispatchers.Main){
Toast.makeText(this#MainActivity2, doing, Toast.LENGTH_LONG).show()
}
}
} finally {
withContext(NonCancellable) {
//close the connection here
}
}
isActive is an extension property available inside the coroutine via the CoroutineScope object.
When the screen is rotated, the connection is closed and a new one is being opened once the coroutine is called again in onCreate.