I have a UseCase class with method that returns Flow
fun startTimeTicker(startTime: Long) = flow {
val countdownStart = System.currentTimeMillis()
for (currentTime in countdownStart..startTime step ONE_SECOND_MILLIS) {
.... irrelevant ...
emit("Some String")
delay(ONE_SECOND_MILLIS)
}
}
I'm collecting emmitted date in ViewModel like this
private fun startCollecting(startTime: Long) = launch {
matchStartTimerUseCase.startTimeTicker(startTime).collect {
_startTimeCountdown.postValue(it)
}
}
and my fragment is observing LiveData and displaying values. It works as expected until the time I leave the screen. As launch is called with coroutineScope of ViewModel shouldn't it get canceled and not emit values anymore?
Scope of ViewModel is implemented in BaseViewModel from which my ViewModel extends like this:
abstract class BaseViewModel(
private val dispatcherProvider: DispatcherProvider
) : ViewModel(), CoroutineScope {
override val coroutineContext: CoroutineContext
get() = job + dispatcherProvider.provideUIContext()
private val job = SupervisorJob()
override fun onCleared() {
super.onCleared()
job.cancel()
}
}
Am I forgetting to add some custom cancellation logic or missing something other?
If scope your ViewModel is Activity's life cycle ViewModelProvider(requireActivity()).get(YOUR_VIEWMODEL::class.java)then onCleared won't be called unless your Activity gets destroyed, but not with rotation changes.
First, make sure that onCleared() is called, if it's not called you can call it any life cycle method of Fragment or Activity.
Related
I have read the article A safer way to collect flows from Android UIs.
I know the following content.
A cold flow backed by a channel or using operators with buffers such as buffer, conflate, flowOn, or shareIn is not safe to collect with some of the existing APIs such as CoroutineScope.launch, Flow.launchIn, or LifecycleCoroutineScope.launchWhenX, unless you manually cancel the Job that started the coroutine when the activity goes to the background. These APIs will keep the underlying flow producer active while emitting items into the buffer in the background, and thus wasting resources.
The Code A is from the official sample project.
The viewModel.suggestedDestinations is a MutableStateFlow, it's a hot Flow.
I don't know if the operation collectAsState() of hot Flow is safe in #Composable UI.
1: Do I need to use the Code just like Code B or Code C replace Code A for a hot Flow?
2: Is the operation collectAsState() of cold Flow safe in #Composable UI.
Code A
#OptIn(ExperimentalMaterialApi::class)
#Composable
fun CraneHomeContent(
onExploreItemClicked: OnExploreItemClicked,
openDrawer: () -> Unit,
modifier: Modifier = Modifier,
viewModel: MainViewModel = viewModel(),
) {
val suggestedDestinations by viewModel.suggestedDestinations.collectAsState()
...
}
#HiltViewModel
class MainViewModel #Inject constructor(
...
) : ViewModel() {
...
private val _suggestedDestinations = MutableStateFlow<List<ExploreModel>>(emptyList())
val suggestedDestinations: StateFlow<List<ExploreModel>>
}
Code B
class LocationActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
...
}
}
}
}
Code C
#Composable
fun LocationScreen(locationFlow: Flow<Flow>) {
val lifecycleOwner = LocalLifecycleOwner.current
val locationFlowLifecycleAware = remember(locationFlow, lifecycleOwner) {
locationFlow.flowWithLifecycle(lifecycleOwner.lifecycle, Lifecycle.State.STARTED)
}
val location by locationFlowLifecycleAware.collectAsState()
...
}
collectAsState (Code A) is safe for any kind of Flow (cold/hot it doesn't matter). If you look at how collectAsState is implemented then you will see that it uses a LaunchedEffect deep down (collectAsState -> produceState -> LaunchedEffect)
internal class LaunchedEffectImpl(
parentCoroutineContext: CoroutineContext,
private val task: suspend CoroutineScope.() -> Unit
) : RememberObserver {
private val scope = CoroutineScope(parentCoroutineContext)
private var job: Job? = null
override fun onRemembered() {
job?.cancel("Old job was still running!")
job = scope.launch(block = task)
}
override fun onForgotten() {
job?.cancel()
job = null
}
override fun onAbandoned() {
job?.cancel()
job = null
}
}
which creates a coroutine scope and launches the task lambda once it enters the composition and cancels it automatically once it leaves the composition.
In Code A, viewModel.suggestedDestinations.collectAsState() (together with it's LaunchedEffect and it's coroutine scope) will be active as long as CraneHomeContent is being called by some other code. As soon as CraneHomeContent is stopped being called the LaunchedEffect inside of collectAsState() is canceled (and coroutine scope as well).
If it's called from multiple places then there will be multiple LaunchedEffects and thus multiple coroutine scopes.
I'm trying to unit test my viewmodel:
private val loginRepository: LoginRepository = LoginRepository()
private val _loginSuccess = MutableLiveData<Resource<String>>()
val loginSuccess : LiveData<Resource<String>>
get() = _loginSuccess
fun login(credentials : RequestLogin){
_loginSuccess.value = Resource.loading()
viewModelScope.launch {
_loginSuccess.postValue(loginRepository.login(credentials))
}
With this:
#Test
fun login_success(){
val loginRequest = RequestLogin("username", "test")
val app:Application = ApplicationProvider.getApplicationContext()
PreferencesHelper.init(app)
val viewModel = LoginViewModel(app)
viewModel.loginSuccess.observeForever(dataObserver)
runBlocking {
viewModel.login(loginRequest)
assertEquals(viewModel.loginSuccess.getOrAwaitValue(), Resource.success("OK"))
}
viewModel.loginSuccess.removeObserver(dataObserver)
}
But everytime i'm getting just the first value of the liveData object Resource.loading() instead of the one obtained with the postValue method.
How can i ignore the result of the first liveData update and just get the final one?
runBlocking executes and waits for completion for the block you pass to it, in this case it is
viewModel.login(loginRequest)
assertEquals(viewModel.loginSuccess.getOrAwaitValue(), Resource.success("OK"))
But this code does not have any suspend calls, so runBlocking does not have any effect here. In particular it does not affect the viewModelScope.launch call.
There are a couple of ways to test this code. I would suggest using kotlinx-coroutines-test library. It provides TestCoroutineDispatcher which is very convenient in this case.
viewModelScope uses Dispatchers.Main dispatcher by default, so you need to replace it with TestCoroutineDispatcher. E.g. you can create a simple test rule:
class CoroutineTestRule(val dispatcher: TestCoroutineDispatcher = TestCoroutineDispatcher()) : TestWatcher() {
override fun starting(description: Description?) {
super.starting(description)
Dispatchers.setMain(dispatcher)
}
override fun finished(description: Description?) {
super.finished(description)
Dispatchers.resetMain()
}
}
Then apply it to your test:
#get:Rule
var coroutineTestRule: CoroutineTestRule = CoroutineTestRule()
And use it like this
#Test
fun login_success(){
...
viewModel.login(loginRequest)
coroutineTestRule.dispatcher.advanceUntilIdle()
assertEquals(viewModel.loginSuccess.getOrAwaitValue(), Resource.success("OK"))
...
}
And here is a bit of how it works:
CoroutineTestRule replaces Dispatcher.Main with the CoroutineTestRule.dispatcher
Your viewmodel launches a login job, using viewModelScope, which uses the same CoroutineTestRule.dispatcher
coroutineTestRule.dispatcher.advanceUntilIdle() makes the dispatcher to execute all outstanding tasks, so it will execute all coroutines, which are using this dispatcher and are ready to be executed.
There is also very convenient advanceTimeBy method on TestCoroutineDispatcher which allows you to fast-forward and skip e.g. delay calls.
How to provide scope or how to call suspend function from Service Android?
Usually, activity or viewmodel provides us the scope, from where we can launch suspend but there is no similar thing in Service
You can create your own CoroutineScope with a SupervisorJob that you can cancel in the onDestroy() method. The coroutines created with this scope will live as long as your Service is being used. Once onDestroy() of your service is called, all coroutines started with this scope will be cancelled.
class YourService : Service() {
private val job = SupervisorJob()
private val scope = CoroutineScope(Dispatchers.IO + job)
...
fun foo() {
scope.launch {
// Call your suspend function
}
}
override fun onDestroy() {
super.onDestroy()
job.cancel()
}
}
Edit: Changed Dispatchers.Main to Dispatchers.IO
SupervisorJob() (Kotlin GitHub) is a job that provides uniderectional cancellation; it allows for cancellations to propogate downwards only.
The SupervisorJob ... is similar to a regular Job with the only exception that cancellation is propagated only downwards. [KotlinLang.org]
Use case: You have a service that makes a log entry, checks settings, and depending on those settings goes ahead and performs some actions. Do you want all children jobs of the parent job (scope) to cancel if, for instance, a job run based on settings' values throws an exception? If not (i.e. you still want your logging and check for settings jobs to complete at the least) then you want to use the SupervisorJob(), or even supervisorScope (Kotlin GitHub) for 'scoped concurrency' [KotlinLang.org], as both provide unidirectional job cancellation - and in that case the provided answer works.
Coroutine Exception Handling - Supervision (KotlinLang.org)
However, there is a more direct solution that answers the question.
To provide to your service a scope with which to run coroutines (or suspending functions) that execute blocking code, you can simply create a new CoroutineScope() with an EmptyCoroutineContext:
(Snippet from CoroutineScope Documentation)
If the given context does not contain a Job element, then a default Job() is created. This way, cancellation or failure of any child coroutine in this scope cancels all the other children, just like inside coroutineScope block [Kotlin GitHub]
class YourClass : Extended() {
...
private val serviceScope: CoroutineScope( EmptyCoroutineContext )
...
private inner class ServiceHandler( looper: Looper ): Handler( looper ) {
override fun handleMessage( msg: Message ) {
super.handleMessage( msg )
serviceScope.launch {
try{
+ ...
} catch( e: Exception ) {
+ ...
} finally {
stopSelf( msg.arg1 )
}
}
}
}
override fun onCreate(){
+ ...
}
override fun onDestroy(){
/* In a service, unlike in an activity, we do not
need to make a call to the super implementation */
//super.onDestory()
serviceScope.cancel()
}
}
for me worked like that
import androidx.lifecycle.lifecycleScope
class ServiceLife : LifecycleService() {
private var supervisorJob = SupervisorJob(parent = null)
override fun onCreate() {
super.onCreate()
val serviceJob = lifecycleScope.launch {
//some suspend fun
}
supervisorJob[serviceJob.key]
supervisorJob.cancel()
}
}
I wanted to know how can I send/emit items to a Kotlin.Flow, so my use case is:
In the consumer/ViewModel/Presenter I can subscribe with the collect function:
fun observe() {
coroutineScope.launch {
// 1. Send event
reopsitory.observe().collect {
println(it)
}
}
}
But the issue is in the Repository side, with RxJava we could use a Behaviorsubject expose it as an Observable/Flowable and emit new items like this:
behaviourSubject.onNext(true)
But whenever I build a new flow:
flow {
}
I can only collect. How can I send values to a flow?
If you want to get the latest value on subscription/collection you should use a ConflatedBroadcastChannel:
private val channel = ConflatedBroadcastChannel<Boolean>()
This will replicate BehaviourSubject, to expose the channel as a Flow:
// Repository
fun observe() {
return channel.asFlow()
}
Now to send an event/value to that exposed Flow simple send to this channel.
// Repository
fun someLogicalOp() {
channel.send(false) // This gets sent to the ViewModel/Presenter and printed.
}
Console:
false
If you wish to only receive values after you start collecting you should use a BroadcastChannel instead.
To make it clear:
Behaves as an Rx's PublishedSubject
private val channel = BroadcastChannel<Boolean>(1)
fun broadcastChannelTest() {
// 1. Send event
channel.send(true)
// 2. Start collecting
channel
.asFlow()
.collect {
println(it)
}
// 3. Send another event
channel.send(false)
}
false
Only false gets printed as the first event was sent before collect { }.
Behaves as an Rx's BehaviourSubject
private val confChannel = ConflatedBroadcastChannel<Boolean>()
fun conflatedBroadcastChannelTest() {
// 1. Send event
confChannel.send(true)
// 2. Start collecting
confChannel
.asFlow()
.collect {
println(it)
}
// 3. Send another event
confChannel.send(false)
}
true
false
Both events are printed, you always get the latest value (if present).
Also, want to mention Kotlin's team development on DataFlow (name pending):
https://github.com/Kotlin/kotlinx.coroutines/pull/1354
Which seems better suited to this use case (as it will be a cold stream).
Take a look at MutableStateFlow documentation as it is a replacement for ConflatedBroadcastChannel that is going to be deprecated, very soon.
For a better context, look at the whole discussion on the original issue on Kotlin's repository on Github.
UPDATE:
Kotlin Coroutines 1.4.0 is now available with MutableSharedFlow, which replaces the need for Channel. MutableSharedFlow cleanup is also built in so you don't need to manually OPEN & CLOSE it, unlike Channel. Please use MutableSharedFlow if you need a Subject-like api for Flow
ORIGINAL ANSWER
Since your question had the android tag I'll add an Android implementation that allows you to easily create a BehaviorSubject or a PublishSubject that handles its own lifecycle.
This is relevant in Android because you don't want to forget to close the channel and leak memory. This implementation avoids the need to explicitly "dispose" of the reactive stream by tying it to the creation and destruction of the Fragment/Activity. Similar to LiveData
interface EventReceiver<Message> {
val eventFlow: Flow<Message>
}
interface EventSender<Message> {
fun postEvent(message: Message)
val initialMessage: Message?
}
class LifecycleEventSender<Message>(
lifecycle: Lifecycle,
private val coroutineScope: CoroutineScope,
private val channel: BroadcastChannel<Message>,
override val initialMessage: Message?
) : EventSender<Message>, LifecycleObserver {
init {
lifecycle.addObserver(this)
}
override fun postEvent(message: Message) {
if (!channel.isClosedForSend) {
coroutineScope.launch { channel.send(message) }
} else {
Log.e("LifecycleEventSender","Channel is closed. Cannot send message: $message")
}
}
#OnLifecycleEvent(Lifecycle.Event.ON_CREATE)
fun create() {
channel.openSubscription()
initialMessage?.let { postEvent(it) }
}
#OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
fun destroy() {
channel.close()
}
}
class ChannelEventReceiver<Message>(channel: BroadcastChannel<Message>) :
EventReceiver<Message> {
override val eventFlow: Flow<Message> = channel.asFlow()
}
abstract class EventRelay<Message>(
lifecycle: Lifecycle,
coroutineScope: CoroutineScope,
channel: BroadcastChannel<Message>,
initialMessage: Message? = null
) : EventReceiver<Message> by ChannelEventReceiver<Message>(channel),
EventSender<Message> by LifecycleEventSender<Message>(
lifecycle,
coroutineScope,
channel,
initialMessage
)
By using the Lifecycle library from Android, I can now create a BehaviorSubject that cleans itself up after the activity/fragment has been destroyed
class BehaviorSubject<String>(
lifecycle: Lifecycle,
coroutineScope: CoroutineScope,
initialMessage = "Initial Message"
) : EventRelay<String>(
lifecycle,
coroutineScope,
ConflatedBroadcastChannel(),
initialMessage
)
or I can create a PublishSubject by using a buffered BroadcastChannel
class PublishSubject<String>(
lifecycle: Lifecycle,
coroutineScope: CoroutineScope,
initialMessage = "Initial Message"
) : EventRelay<String>(
lifecycle,
coroutineScope,
BroadcastChannel(Channel.BUFFERED),
initialMessage
)
And now I can do something like this
class MyActivity: Activity() {
val behaviorSubject = BehaviorSubject(
this#MyActivity.lifecycle,
this#MyActivity.lifecycleScope
)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (savedInstanceState == null) {
behaviorSubject.eventFlow
.onEach { stringEvent ->
Log.d("BehaviorSubjectFlow", stringEvent)
// "BehaviorSubjectFlow: Initial Message"
// "BehaviorSubjectFlow: Next Message"
}
.flowOn(Dispatchers.Main)
.launchIn(this#MyActivity.lifecycleScope)
}
}
override fun onResume() {
super.onResume()
behaviorSubject.postEvent("Next Message")
}
}
Some JVM frameworks use ThreadLocal to store the call context of a application, like the SLF4j MDC, transaction managers, security managers, and others.
However, Kotlin coroutines are dispatched on different threads, so how it can be made to work?
(The question is inspired by GitHub issue)
Coroutine's analog to ThreadLocal is CoroutineContext.
To interoperate with ThreadLocal-using libraries you need to implement a custom ContinuationInterceptor that supports framework-specific thread-locals.
Here is an example. Let us assume that we use some framework that relies on a specific ThreadLocal to store some application-specific data (MyData in this example):
val myThreadLocal = ThreadLocal<MyData>()
To use it with coroutines, you'll need to implement a context that keeps the current value of MyData and puts it into the corresponding ThreadLocal every time the coroutine is resumed on a thread. The code should look like this:
class MyContext(
private var myData: MyData,
private val dispatcher: ContinuationInterceptor
) : AbstractCoroutineContextElement(ContinuationInterceptor), ContinuationInterceptor {
override fun <T> interceptContinuation(continuation: Continuation<T>): Continuation<T> =
dispatcher.interceptContinuation(Wrapper(continuation))
inner class Wrapper<T>(private val continuation: Continuation<T>): Continuation<T> {
private inline fun wrap(block: () -> Unit) {
try {
myThreadLocal.set(myData)
block()
} finally {
myData = myThreadLocal.get()
}
}
override val context: CoroutineContext get() = continuation.context
override fun resume(value: T) = wrap { continuation.resume(value) }
override fun resumeWithException(exception: Throwable) = wrap { continuation.resumeWithException(exception) }
}
}
To use it in your coroutines, you wrap the dispatcher that you want to use with MyContext and give it the initial value of your data. This value will be put into the thread-local on the thread where the coroutine is resumed.
launch(MyContext(MyData(), CommonPool)) {
// do something...
}
The implementation above would also track any changes to the thread-local that was done and store it in this context, so this way multiple invocation can share "thread-local" data via context.
UPDATE: Starting with kotlinx.corutines version 0.25.0 there is direct support for representing Java ThreadLocal instances as coroutine context elements. See this documentation for details. There is also out-of-the-box support for SLF4J MDC via kotlinx-coroutines-slf4j integration module.
Though this question is quite an old one, but I would want to add to Roman's answer another possible approach with CopyableThreadContextElement. Maybe it will be helpful for somebody else.
// Snippet from the source code's comment
class TraceContextElement(private val traceData: TraceData?) : CopyableThreadContextElement<TraceData?> {
companion object Key : CoroutineContext.Key<TraceContextElement>
override val key: CoroutineContext.Key<TraceContextElement> = Key
override fun updateThreadContext(context: CoroutineContext): TraceData? {
val oldState = traceThreadLocal.get()
traceThreadLocal.set(traceData)
return oldState
}
override fun restoreThreadContext(context: CoroutineContext, oldState: TraceData?) {
traceThreadLocal.set(oldState)
}
override fun copyForChild(): TraceContextElement {
// Copy from the ThreadLocal source of truth at child coroutine launch time. This makes
// ThreadLocal writes between resumption of the parent coroutine and the launch of the
// child coroutine visible to the child.
return TraceContextElement(traceThreadLocal.get()?.copy())
}
override fun mergeForChild(overwritingElement: CoroutineContext.Element): CoroutineContext {
// Merge operation defines how to handle situations when both
// the parent coroutine has an element in the context and
// an element with the same key was also
// explicitly passed to the child coroutine.
// If merging does not require special behavior,
// the copy of the element can be returned.
return TraceContextElement(traceThreadLocal.get()?.copy())
}
}
Note that copyForChild method allows you to propagate thread local data taken from the parent coroutine's last resumption phase to the local context of the child coroutine (as Copyable in CopyableThreadContextElement implies), because method copyForChild will be invoked on the parent coroutine's thread associated with the corresponding resumption phase when a child coroutine was created.
Just by adding TraceContextElement context element to the root coroutine's context it will be propagated to all child coroutines as context element.
runBlocking(Dispatchers.IO + TraceContextElement(someTraceDataInstance)){...}
Whereas with ContinuationInterceptor approach additional wrapping can be necessary for child coroutines' builders, if you redefine dispatchers for child coroutines.
fun main() {
runBlocking(WrappedDispatcher(Dispatchers.IO)) {
delay(100)
println("It is wrapped!")
delay(100)
println("It is also wrapped!")
// NOTE: we don't wrap with the WrappedDispatcher class here
// redefinition of the dispatcher leads to replacement of our custom ContinuationInterceptor
// with logic taken from specified dispatcher (in the case below from Dispatchers.Default)
withContext(Dispatchers.Default) {
delay(100)
println("It is nested coroutine, and it isn't wrapped!")
delay(100)
println("It is nested coroutine, and it isn't wrapped!")
}
delay(100)
println("It is also wrapped!")
}
}
with wrapper overriding ContinuationInterceptor interface
class WrappedDispatcher(
private val dispatcher: ContinuationInterceptor
) : AbstractCoroutineContextElement(ContinuationInterceptor), ContinuationInterceptor {
override fun <T> interceptContinuation(continuation: Continuation<T>): Continuation<T> =
dispatcher.interceptContinuation(ContinuationWrapper(continuation))
private class ContinuationWrapper<T>(val base: Continuation<T>) : Continuation<T> by base {
override fun resumeWith(result: Result<T>) {
println("------WRAPPED START-----")
base.resumeWith(result)
println("------WRAPPED END-------")
}
}
}
output:
------WRAPPED START-----
------WRAPPED END-------
------WRAPPED START-----
It is wrapped!
------WRAPPED END-------
------WRAPPED START-----
It is also wrapped!
------WRAPPED END-------
It is nested coroutine, and it isn't wrapped!
It is nested coroutine, and it isn't wrapped!
------WRAPPED START-----
------WRAPPED END-------
------WRAPPED START-----
It is also wrapped!
------WRAPPED END-------
as you can see for the child (nested) coroutine our wrapper wasn't applied, since we reassigned a ContinuationInterceptor supplying another dispatcher as a parameter. This can lead to a problem as you can mistakenly forget to wrap a child coroutine's dispatcher.
As a side note, if you decide to choose this approach with ContinuationInterceptor, then consider to add such extension
fun ContinuationInterceptor.withMyProjectWrappers() = WrappedDispatcher(this)
wrapping your dispatcher with all necessary wrappers you have in the project, obviously it can be easily extended taking specific beans (wrappers) from an IoC container such as Spring.
And also as an extra example of CopyableThreadContextElement where thread local changes are saved in all resumptions phases.
Executors.newFixedThreadPool(..).asCoroutineDispatcher() is used to
better illustrate that different threads can be working between
resumptions phases.
val counterThreadLocal: ThreadLocal<Int> = ThreadLocal.withInitial{ 1 }
fun showCounter(){
println("-------------------------------------------------")
println("Thread: ${Thread.currentThread().name}\n Counter value: ${counterThreadLocal.get()}")
}
fun main() {
runBlocking(Executors.newFixedThreadPool(10).asCoroutineDispatcher() + CounterPropagator(1)) {
showCounter()
delay(100)
showCounter()
counterThreadLocal.set(2)
delay(100)
showCounter()
counterThreadLocal.set(3)
val nested = async(Executors.newFixedThreadPool(10).asCoroutineDispatcher()) {
println("-----------NESTED START---------")
showCounter()
delay(100)
counterThreadLocal.set(4)
showCounter()
println("------------NESTED END-----------")
}
nested.await()
showCounter()
println("---------------END------------")
}
}
class CounterPropagator(private var counterFromParenCoroutine: Int) : CopyableThreadContextElement<Int> {
companion object Key : CoroutineContext.Key<CounterPropagator>
override val key: CoroutineContext.Key<CounterPropagator> = Key
override fun updateThreadContext(context: CoroutineContext): Int {
// initialize thread local on the resumption
counterThreadLocal.set(counterFromParenCoroutine)
return 0
}
override fun restoreThreadContext(context: CoroutineContext, oldState: Int) {
// propagate thread local changes between resumption phases in the same coroutine
counterFromParenCoroutine = counterThreadLocal.get()
}
override fun copyForChild(): CounterPropagator {
// propagate thread local changes to children
return CounterPropagator(counterThreadLocal.get())
}
override fun mergeForChild(overwritingElement: CoroutineContext.Element): CoroutineContext {
return CounterPropagator(counterThreadLocal.get())
}
}
output:
-------------------------------------------------
Thread: pool-1-thread-1
Counter value: 1
-------------------------------------------------
Thread: pool-1-thread-2
Counter value: 1
-------------------------------------------------
Thread: pool-1-thread-3
Counter value: 2
-----------NESTED START---------
-------------------------------------------------
Thread: pool-2-thread-1
Counter value: 3
-------------------------------------------------
Thread: pool-2-thread-2
Counter value: 4
------------NESTED END-----------
-------------------------------------------------
Thread: pool-1-thread-4
Counter value: 3
---------------END------------
You can achieve similar behavior with ContinuationInterceptor (but don't forget to re-wrap dispatchers of child (nested) coroutines in the coroutine builder as was mentioned above)
val counterThreadLocal: ThreadLocal<Int> = ThreadLocal()
class WrappedDispatcher(
private val dispatcher: ContinuationInterceptor,
private var savedCounter: Int = counterThreadLocal.get() ?: 0
) : AbstractCoroutineContextElement(ContinuationInterceptor), ContinuationInterceptor {
override fun <T> interceptContinuation(continuation: Continuation<T>): Continuation<T> =
dispatcher.interceptContinuation(ContinuationWrapper(continuation))
private inner class ContinuationWrapper<T>(val base: Continuation<T>) : Continuation<T> by base {
override fun resumeWith(result: Result<T>) {
counterThreadLocal.set(savedCounter)
try {
base.resumeWith(result)
} finally {
savedCounter = counterThreadLocal.get()
}
}
}
}