Skip to content

Commit 743ae73

Browse files
authored
Use dedicated coroutine context for Compose scene (#2646)
Use dedicated coroutine context with separate Job for compose scene. Use nested coroutine scope tight to the Compose scene context for frame animations. Make `ComposeSceneMediator`'s lifetime tight to the context. Fixes https://youtrack.jetbrains.com/issue/CMP-7727/Use-dedicated-coroutine-scopes-for-compose-scenes ## Release Notes N/A
1 parent 722fca9 commit 743ae73

File tree

10 files changed

+157
-132
lines changed

10 files changed

+157
-132
lines changed

compose/ui/ui/src/iosMain/kotlin/androidx/compose/ui/scene/ComposeContainer.ios.kt

Lines changed: 30 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,11 @@ import androidx.compose.ui.window.SceneActiveStateListener
5252
import androidx.lifecycle.enableSavedStateHandles
5353
import androidx.savedstate.SavedState
5454
import kotlin.coroutines.CoroutineContext
55+
import kotlin.coroutines.EmptyCoroutineContext
5556
import kotlinx.cinterop.CPointed
5657
import kotlinx.cinterop.CPointer
58+
import kotlinx.coroutines.CoroutineScope
59+
import kotlinx.coroutines.Job
5760
import org.jetbrains.skiko.OS
5861
import org.jetbrains.skiko.OSVersion
5962
import org.jetbrains.skiko.available
@@ -75,7 +78,7 @@ import platform.UIKit.UIWindowScene
7578
internal class ComposeContainer(
7679
private val configuration: ComposeContainerConfiguration,
7780
private val content: @Composable () -> Unit,
78-
coroutineContext: CoroutineContext,
81+
private val coroutineContext: CoroutineContext,
7982
private val lifecycleDelegate: ComposeContainerLifecycleDelegate
8083
) {
8184
private val hapticFeedback = CupertinoHapticFeedback()
@@ -91,8 +94,11 @@ internal class ComposeContainer(
9194
private val layoutDirection get() = getApplicationLayoutDirection()
9295
private val motionDurationScale = MotionDurationScaleImpl()
9396
private var activeStateListener: SceneActiveStateListener? = null
94-
val composeCoroutineContext: CoroutineContext = coroutineContext + motionDurationScale
95-
97+
private var sceneJob: Job = Job().also {
98+
// The initial state of the container considered as "not active".
99+
// The `initializeComposeScene` must be called to set the active `sceneJob`.
100+
it.cancel()
101+
}
96102
private var savedState: SavedState? = null
97103
private var mediatorComponentsOwner: DefaultArchitectureComponentsOwner? = null
98104
private val architectureComponentsOwner: DefaultArchitectureComponentsOwner
@@ -124,6 +130,15 @@ internal class ComposeContainer(
124130
if (configuration.enforceStrictPlistSanityCheck) {
125131
PlistSanityCheck.performIfNeeded()
126132
}
133+
lifecycleDelegate.runOnDeinit {
134+
windowContext.dispose()
135+
}
136+
}
137+
138+
fun nestedCoroutineScope(
139+
addedContext: CoroutineContext = EmptyCoroutineContext
140+
): CoroutineScope {
141+
return CoroutineScope(coroutineContext + addedContext + Job(parent = sceneJob))
127142
}
128143

129144
fun prepareAndGetSizeTransitionAnimation(withProgress: suspend ((Float) -> Unit) -> Unit): suspend () -> Unit {
@@ -190,6 +205,8 @@ internal class ComposeContainer(
190205
}
191206

192207
fun initializeComposeScene() {
208+
sceneJob = Job()
209+
val sceneCoroutineContext = coroutineContext + motionDurationScale + sceneJob
193210
val metalView = MetalView(
194211
retrieveInteropTransaction = {
195212
mediator?.retrieveInteropTransaction() ?: object : UIKitInteropTransaction {
@@ -205,7 +222,7 @@ internal class ComposeContainer(
205222
metalView.canBeOpaque = configuration.opaque
206223
val holder = ComposeLayersHolder(
207224
useSeparateRenderThreadWhenPossible = configuration.parallelRendering,
208-
context = composeCoroutineContext,
225+
coroutineContext = sceneCoroutineContext,
209226
getWindow = { view.window }
210227
).also {
211228
layersHolder = it
@@ -221,10 +238,10 @@ internal class ComposeContainer(
221238
focusedViewsList = focusedViewsList,
222239
windowContext = windowContext,
223240
architectureComponentsOwner = architectureComponentsOwner,
224-
coroutineContext = composeCoroutineContext,
241+
coroutineContext = sceneCoroutineContext,
225242
redrawer = metalView.redrawer,
226243
composeSceneFactory = { invalidate, context ->
227-
createComposeScene(invalidate, context, holder)
244+
createComposeScene(invalidate, context, holder, sceneCoroutineContext)
228245
},
229246
navigationEventInput = navigationEventInput,
230247
interfaceOrientationState = interfaceOrientationState,
@@ -259,6 +276,7 @@ internal class ComposeContainer(
259276
}
260277

261278
fun disposeComposeScene() {
279+
sceneJob.cancel()
262280
// Store the current state in the local savedState property. It is used to
263281
// provide the saved state to the next Compose scene when the container re-enters
264282
// the window hierarchy.
@@ -269,18 +287,14 @@ internal class ComposeContainer(
269287
navigationEventInput.onDidMoveToWindow(null, view)
270288
architectureComponentsOwner.navigationEventDispatcher.removeInput(navigationEventInput)
271289

272-
mediator?.dispose()
273290
mediator = null
274291

275292
activeStateListener?.dispose()
276293
activeStateListener = null
277294

278-
layersHolder?.disposeIfNeeded()
279295
layersHolder = null
280296

281297
interfaceOrientationObserver.isObservingEnabled = false
282-
283-
windowContext.dispose()
284298
}
285299

286300
private fun createComposeSceneContext(
@@ -308,9 +322,8 @@ internal class ComposeContainer(
308322
configuration = configuration,
309323
onAccessibilityChanged = ::onAccessibilityChanged,
310324
focusedViewsList = if (focusable) focusedViewsList.childFocusedViewsList() else null,
311-
compositionContext = compositionContext,
325+
parentCoroutineContext = compositionContext.effectCoroutineContext,
312326
ownerProvider = architectureComponentsOwner,
313-
coroutineContext = composeCoroutineContext,
314327
interfaceOrientationState = interfaceOrientationState,
315328
)
316329

@@ -325,11 +338,12 @@ internal class ComposeContainer(
325338
private fun createComposeScene(
326339
invalidate: () -> Unit,
327340
platformContext: PlatformContext,
328-
layersHolder: ComposeLayersHolder
341+
layersHolder: ComposeLayersHolder,
342+
coroutineContext: CoroutineContext
329343
): ComposeScene = PlatformLayersComposeScene(
330344
density = view.density,
331345
layoutDirection = layoutDirection,
332-
coroutineContext = composeCoroutineContext,
346+
coroutineContext = coroutineContext,
333347
composeSceneContext = createComposeSceneContext(
334348
platformContext = platformContext,
335349
layersHolder = layersHolder
@@ -402,7 +416,7 @@ private fun getApplicationLayoutDirection() =
402416

403417
private class ComposeLayersHolder(
404418
private val useSeparateRenderThreadWhenPossible: Boolean,
405-
private val context: CoroutineContext,
419+
private val coroutineContext: CoroutineContext,
406420
private val getWindow: () -> UIWindow?
407421
) {
408422
var layersViewController: ComposeLayersViewController? = null
@@ -412,18 +426,13 @@ private class ComposeLayersHolder(
412426
return layersViewController ?: run {
413427
val layers = ComposeLayersViewController(
414428
useSeparateRenderThreadWhenPossible = useSeparateRenderThreadWhenPossible,
415-
context = context
429+
coroutineContext = coroutineContext
416430
)
417431
layers.referenceWindow = getWindow()
418432
layersViewController = layers
419433
layers
420434
}
421435
}
422-
423-
fun disposeIfNeeded() {
424-
layersViewController?.dispose()
425-
layersViewController = null
426-
}
427436
}
428437

429438
private class SceneGeometryObserver(

compose/ui/ui/src/iosMain/kotlin/androidx/compose/ui/scene/ComposeHostingView.ios.kt

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@ import kotlin.math.abs
3030
import kotlinx.cinterop.BetaInteropApi
3131
import kotlinx.cinterop.CValue
3232
import kotlinx.cinterop.ExportObjCClass
33-
import kotlinx.coroutines.CoroutineScope
3433
import kotlinx.coroutines.Dispatchers
3534
import kotlinx.coroutines.cancel
3635
import kotlinx.coroutines.isActive
@@ -51,7 +50,6 @@ internal class ComposeHostingView(
5150
coroutineContext = coroutineContext,
5251
lifecycleDelegate = lifecycleDelegate
5352
)
54-
private val scope = CoroutineScope(container.composeCoroutineContext)
5553

5654
// Used for testing
5755
val rootRedrawer: MetalRedrawer? get() = container.view.redrawer
@@ -77,7 +75,15 @@ internal class ComposeHostingView(
7775
container.updateInterfaceOrientationState()
7876

7977
val initialSize = layer.presentationLayer()?.bounds?.dpSize()
80-
if (initialSize == null || initialSize == bounds.dpSize() || container.hasInteropViews) {
78+
if (initialSize == null ||
79+
initialSize == bounds.dpSize() ||
80+
container.hasInteropViews) {
81+
container.view.setFrame(bounds)
82+
return
83+
}
84+
85+
val scope = container.nestedCoroutineScope()
86+
if (!scope.isActive) {
8187
container.view.setFrame(bounds)
8288
return
8389
}
@@ -127,13 +133,8 @@ internal class ComposeHostingView(
127133
isAnimating = true
128134

129135
val displayLinkListener = DisplayLinkListener()
130-
val sizeTransitionScope = CoroutineScope(
131-
container.composeCoroutineContext + displayLinkListener.frameClock
132-
)
136+
val sizeTransitionScope = container.nestedCoroutineScope(displayLinkListener.frameClock)
133137
displayLinkListener.start()
134-
if (!container.composeCoroutineContext.isActive) {
135-
return
136-
}
137138

138139
fun progress(): Float {
139140
var progress: Float

compose/ui/ui/src/iosMain/kotlin/androidx/compose/ui/scene/ComposeHostingViewController.ios.kt

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@ import kotlin.time.toDuration
3333
import kotlinx.cinterop.BetaInteropApi
3434
import kotlinx.cinterop.CValue
3535
import kotlinx.cinterop.ExportObjCClass
36-
import kotlinx.coroutines.CoroutineScope
3736
import kotlinx.coroutines.Dispatchers
3837
import kotlinx.coroutines.cancel
3938
import platform.CoreGraphics.CGSize
@@ -172,17 +171,13 @@ internal class ComposeHostingViewController(
172171
duration: Duration
173172
) {
174173
val displayLinkListener = DisplayLinkListener()
175-
val sizeTransitionScope = CoroutineScope(
176-
container.composeCoroutineContext + displayLinkListener.frameClock
177-
)
174+
val sizeTransitionScope = container.nestedCoroutineScope(displayLinkListener.frameClock)
178175
displayLinkListener.start()
179176

180177
val animations = container.prepareAndGetSizeTransitionAnimation { onFrame ->
181178
withAnimationProgress(duration, update = onFrame)
182179
}
183-
container.view.animateSizeTransition(sizeTransitionScope) {
184-
animations()
185-
}
180+
container.view.animateSizeTransition(sizeTransitionScope, animations)
186181

187182
transitionCoordinator.animateAlongsideTransition(
188183
animation = {},
@@ -196,7 +191,7 @@ internal class ComposeHostingViewController(
196191
private fun animateCrossFadeSizeTransition(
197192
transitionCoordinator: UIViewControllerTransitionCoordinatorProtocol
198193
) {
199-
val transitionScope = CoroutineScope(container.composeCoroutineContext)
194+
val transitionScope = container.nestedCoroutineScope()
200195
val viewAnimationClosure = container.view.animateCrossFadeTransition(transitionScope)
201196

202197
transitionCoordinator.animateAlongsideTransition(

compose/ui/ui/src/iosMain/kotlin/androidx/compose/ui/scene/ComposeLayersViewController.ios.kt

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,9 @@ import kotlin.time.DurationUnit
3232
import kotlin.time.toDuration
3333
import kotlinx.cinterop.CValue
3434
import kotlinx.coroutines.CoroutineScope
35+
import kotlinx.coroutines.Job
3536
import kotlinx.coroutines.cancel
37+
import kotlinx.coroutines.job
3638
import kotlinx.coroutines.joinAll
3739
import kotlinx.coroutines.launch
3840
import org.jetbrains.skia.Canvas
@@ -52,7 +54,7 @@ import platform.UIKit.UIWindowLevelNormal
5254
*/
5355
internal class ComposeLayersViewController(
5456
useSeparateRenderThreadWhenPossible: Boolean,
55-
private val context: CoroutineContext
57+
private val coroutineContext: CoroutineContext
5658
): UIViewController(nibName = null, bundle = null) {
5759
val windowContext = PlatformWindowContext()
5860

@@ -77,6 +79,10 @@ internal class ComposeLayersViewController(
7779
)
7880
}
7981

82+
init {
83+
coroutineContext.job.invokeOnCompletion { dispose() }
84+
}
85+
8086
fun withLayers(block: (List<UIKitComposeSceneLayer>) -> Unit) = layersCache.withCopy(block)
8187

8288
override fun loadView() {
@@ -115,7 +121,7 @@ internal class ComposeLayersViewController(
115121
window.setHidden(true)
116122
}
117123

118-
fun dispose() {
124+
private fun dispose() {
119125
// `dispose` is called instead of `close`, because `close` is also used imperatively
120126
// to remove the layer from the array based on user interaction.
121127
while (this.layers.isNotEmpty()) {
@@ -266,7 +272,8 @@ internal class ComposeLayersViewController(
266272
duration: Duration,
267273
) {
268274
val displayLinkListener = DisplayLinkListener()
269-
val sizeTransitionScope = CoroutineScope(context + displayLinkListener.frameClock)
275+
val sizeTransitionScope =
276+
CoroutineScope(coroutineContext + displayLinkListener.frameClock + Job())
270277
displayLinkListener.start()
271278

272279
animateSizeTransition(sizeTransitionScope, duration)
@@ -283,7 +290,7 @@ internal class ComposeLayersViewController(
283290
private fun crossFadeSizeTransition(
284291
transitionCoordinator: UIViewControllerTransitionCoordinatorProtocol
285292
) {
286-
val transitionScope = CoroutineScope(context)
293+
val transitionScope = CoroutineScope(coroutineContext + Job())
287294
val layersAnimationClosure = rootView.animateCrossFadeTransition(transitionScope)
288295

289296
transitionCoordinator.animateAlongsideTransition(

compose/ui/ui/src/iosMain/kotlin/androidx/compose/ui/scene/ComposeSceneMediator.ios.kt

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,8 @@ import kotlinx.cinterop.CValue
9696
import kotlinx.cinterop.useContents
9797
import kotlinx.coroutines.coroutineScope
9898
import kotlinx.coroutines.flow.filterNotNull
99+
import kotlinx.coroutines.isActive
100+
import kotlinx.coroutines.job
99101
import kotlinx.coroutines.launch
100102
import kotlinx.coroutines.suspendCancellableCoroutine
101103
import org.jetbrains.skiko.OS
@@ -203,7 +205,7 @@ internal class ComposeSceneMediator(
203205
override var isActive by mutableStateOf(false)
204206
}
205207

206-
private var disposed = false
208+
private val isActive get() = coroutineContext.isActive
207209

208210
private val viewConfiguration: ViewConfiguration =
209211
object : ViewConfiguration by PlatformContext.DefaultViewConfiguration {
@@ -224,7 +226,7 @@ internal class ComposeSceneMediator(
224226
private var size: IntSize?
225227
get() = scene.size
226228
set(value) {
227-
if (!disposed) {
229+
if (isActive) {
228230
scene.size = value
229231
if (value != null) {
230232
windowInsetsManager.sceneSize.value = value
@@ -242,7 +244,7 @@ internal class ComposeSceneMediator(
242244
var composeSceneDensity: Density
243245
get() = scene.density
244246
set(value) {
245-
if (!disposed) {
247+
if (isActive) {
246248
scene.density = value
247249
}
248250
}
@@ -258,15 +260,15 @@ internal class ComposeSceneMediator(
258260
var layoutDirection: LayoutDirection
259261
get() = scene.layoutDirection
260262
set(value) {
261-
if (!disposed) {
263+
if (isActive) {
262264
scene.layoutDirection = value
263265
}
264266
}
265267

266268
var compositionLocalContext: CompositionLocalContext?
267269
get() = scene.compositionLocalContext
268270
set(value) {
269-
if (!disposed) {
271+
if (isActive) {
270272
scene.compositionLocalContext = value
271273
}
272274
}
@@ -387,6 +389,10 @@ internal class ComposeSceneMediator(
387389
semanticsOwnerListener.hasInvalidations ||
388390
textInputService.hasInvalidations
389391

392+
init {
393+
coroutineContext.job.invokeOnCompletion { dispose() }
394+
}
395+
390396
private fun hitTestInteropView(point: CValue<CGPoint>): UIView? =
391397
point.useContents {
392398
val position = asDpOffset().toOffset(composeSceneDensity)
@@ -622,8 +628,7 @@ internal class ComposeSceneMediator(
622628
}
623629
}
624630

625-
fun dispose() {
626-
disposed = true
631+
private fun dispose() {
627632
onPreviewKeyEvent = { false }
628633
onKeyEvent = { false }
629634

0 commit comments

Comments
 (0)