Skip to content

Commit 4312022

Browse files
authored
Lazy initiate services to avoid allocations in PlatformOwner/Ctx (#2960)
Fixes [CMP-10204](https://youtrack.jetbrains.com/issue/CMP-10204) Lazily initiate services from Platform to avoid initializing services when not needed or not initially requested. This is in part to prepare for https://r.android.com/4054989 merge onto CMP, which will defer some CompositionLocals initialization until they're read (using providesComputed instead of provides). This will most certainly improve initial TimeToFirstPixel by avoiding redundant or not needed allocations on init. This also adds a common way to provide a HapticFeedback object in a centralized way in the PlatformContext. This prevents each platform adding its own LocalHapticFeedback when it is already provided through the Owner As per Google Compose's Team, I'm avoiding using "lazy delegation" and instead using a private backing variable for lazy initialization. Lazy delegate allocates a class anyways and more internal code inside it and it would defeat the purpose of no allocation until needed. Google uses the same approach in their Owner implementation when deferring services (avoiding lazy {}) <details><summary>Previous smaller scope PR description</summary> <p> RootForTest is only used in Tests and in JVM (forcefully in the latter, since it implements RootTestListener in DesktopPlatformContext, which if it is not null, it is invoked in the init block of RootNodeOwner). All other platforms prevent this since they do not implement RootTestListener, so the invoke call will be ignored, thus lazy RootForTest will not be called. Same for DesktopTextInputService2, but it is used, just not initially and only after reacting to some Keyboard or InputText change </p> </details> ## Release Notes N/A
1 parent 23be895 commit 4312022

9 files changed

Lines changed: 108 additions & 84 deletions

File tree

compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/scene/ComposeSceneMediator.desktop.kt

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -144,8 +144,15 @@ internal class ComposeSceneMediator(
144144
private val navigationEventInput = BackNavigationEventInput()
145145

146146
private val platformComponent = DesktopPlatformComponent()
147-
private val textInputService = DesktopTextInputService(platformComponent)
148-
private val textInputService2 = DesktopTextInputService2(platformComponent)
147+
148+
private val textInputService by lazy(LazyThreadSafetyMode.NONE) {
149+
DesktopTextInputService(platformComponent)
150+
}
151+
152+
private val textInputService2 by lazy(LazyThreadSafetyMode.NONE) {
153+
DesktopTextInputService2(platformComponent)
154+
}
155+
149156
private val _platformContext = DesktopPlatformContext()
150157
val platformContext: PlatformContext get() = _platformContext
151158

@@ -807,8 +814,12 @@ internal class ComposeSceneMediator(
807814

808815
override val measureDrawLayerBounds: Boolean = this@ComposeSceneMediator.measureDrawLayerBounds
809816
override val viewConfiguration: ViewConfiguration = DesktopViewConfiguration()
810-
override val inputModeManager: InputModeManager = DefaultInputModeManager()
811-
override val textInputService = this@ComposeSceneMediator.textInputService
817+
818+
override val inputModeManager: InputModeManager by lazy(LazyThreadSafetyMode.NONE) {
819+
DefaultInputModeManager()
820+
}
821+
822+
override val textInputService get() = this@ComposeSceneMediator.textInputService
812823

813824
override suspend fun startInputMethod(request: PlatformTextInputMethodRequest): Nothing {
814825
textInputService2.startInputMethod(request)

compose/ui/ui/src/iosMain/kotlin/androidx/compose/ui/platform/UIKitTextInputService.ios.kt

Lines changed: 34 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -136,43 +136,44 @@ internal class UIKitTextInputService(
136136
fun onPreviewKeyEvent(event: KeyEvent): Boolean =
137137
currentInputConnection?.onPreviewKeyEvent(event) ?: false
138138

139-
val textToolbar: TextToolbar = object : TextToolbar {
140-
141-
override val status: TextToolbarStatus
142-
get() = (currentInputConnection as? TextToolbar)?.status ?: TextToolbarStatus.Hidden
143-
144-
override fun showMenu(
145-
rect: Rect,
146-
onCopyRequested: (() -> Unit)?,
147-
onPasteRequested: (() -> Unit)?,
148-
onCutRequested: (() -> Unit)?,
149-
onSelectAllRequested: (() -> Unit)?
150-
) {
151-
if (currentInputConnection == null) {
152-
// Entry point for showing the context menu in SelectionContainer scenarios, where
153-
// there is no active text input session. iOS requires a UIView that can become first
154-
// responder in order to host the context menu, so we create a dedicated connection
155-
// backed by a hidden view for this purpose.
156-
// Note: start() is intentionally not called here — it establishes a text editing
157-
// session (requiring a PlatformTextInputMethodRequest) which is not applicable for
158-
// SelectionContainer.
159-
currentInputConnection = SelectionContainerConnection(
160-
view, coroutineScope, viewConfiguration, focusManager
139+
val textToolbar: TextToolbar by lazy(LazyThreadSafetyMode.NONE) {
140+
object : TextToolbar {
141+
override val status: TextToolbarStatus
142+
get() = (currentInputConnection as? TextToolbar)?.status ?: TextToolbarStatus.Hidden
143+
144+
override fun showMenu(
145+
rect: Rect,
146+
onCopyRequested: (() -> Unit)?,
147+
onPasteRequested: (() -> Unit)?,
148+
onCutRequested: (() -> Unit)?,
149+
onSelectAllRequested: (() -> Unit)?
150+
) {
151+
if (currentInputConnection == null) {
152+
// Entry point for showing the context menu in SelectionContainer scenarios, where
153+
// there is no active text input session. iOS requires a UIView that can become first
154+
// responder in order to host the context menu, so we create a dedicated connection
155+
// backed by a hidden view for this purpose.
156+
// Note: start() is intentionally not called here — it establishes a text editing
157+
// session (requiring a PlatformTextInputMethodRequest) which is not applicable for
158+
// SelectionContainer.
159+
currentInputConnection = SelectionContainerConnection(
160+
view, coroutineScope, viewConfiguration, focusManager
161+
)
162+
}
163+
(currentInputConnection as? TextToolbar)?.showMenu(
164+
rect, onCopyRequested, onPasteRequested, onCutRequested, onSelectAllRequested
161165
)
162166
}
163-
(currentInputConnection as? TextToolbar)?.showMenu(
164-
rect, onCopyRequested, onPasteRequested, onCutRequested, onSelectAllRequested
165-
)
166-
}
167167

168-
override fun hide() {
169-
(currentInputConnection as? TextToolbar)?.hide()
168+
override fun hide() {
169+
(currentInputConnection as? TextToolbar)?.hide()
170170

171-
if (currentInputConnection is SelectionContainerConnection) {
172-
// stop() removes the view from the hierarchy and resigns first responder,
173-
// without requiring a prior start() call.
174-
currentInputConnection?.stop()
175-
currentInputConnection = null
171+
if (currentInputConnection is SelectionContainerConnection) {
172+
// stop() removes the view from the hierarchy and resigns first responder,
173+
// without requiring a prior start() call.
174+
currentInputConnection?.stop()
175+
currentInputConnection = null
176+
}
176177
}
177178
}
178179
}

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

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,8 @@ import androidx.compose.runtime.mutableStateOf
2424
import androidx.compose.ui.LocalSystemTheme
2525
import androidx.compose.ui.SystemTheme
2626
import androidx.compose.ui.graphics.asComposeCanvas
27-
import androidx.compose.ui.hapticfeedback.CupertinoHapticFeedback
2827
import androidx.compose.ui.navigationevent.UIKitNavigationEventInput
2928
import androidx.compose.ui.platform.DefaultArchitectureComponentsOwner
30-
import androidx.compose.ui.platform.LocalHapticFeedback
3129
import androidx.compose.ui.platform.MotionDurationScaleImpl
3230
import androidx.compose.ui.platform.PlatformContext
3331
import androidx.compose.ui.platform.PlatformWindowContext
@@ -79,7 +77,6 @@ internal class ComposeContainer(
7977
private val coroutineContext: CoroutineContext,
8078
private val lifecycleDelegate: ComposeContainerLifecycleDelegate
8179
) {
82-
private val hapticFeedback = CupertinoHapticFeedback()
8380

8481
val view = ComposeContainerView(
8582
transparentForTouches = false,
@@ -374,7 +371,6 @@ internal class ComposeContainer(
374371
@Composable
375372
private fun ProvideContainerCompositionLocals(content: @Composable () -> Unit) =
376373
CompositionLocalProvider(
377-
LocalHapticFeedback provides hapticFeedback,
378374
LocalUIViewController provides containingViewController,
379375
LocalSystemTheme provides systemThemeState.value,
380376
content = content

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

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,17 @@ import androidx.compose.runtime.State
2323
import androidx.compose.runtime.getValue
2424
import androidx.compose.runtime.mutableStateOf
2525
import androidx.compose.runtime.setValue
26+
import androidx.compose.ui.InternalComposeUiApi
2627
import androidx.compose.ui.draganddrop.UIKitDragAndDropManager
2728
import androidx.compose.ui.geometry.Offset
2829
import androidx.compose.ui.geometry.Rect
2930
import androidx.compose.ui.geometry.Size
3031
import androidx.compose.ui.geometry.lerp
3132
import androidx.compose.ui.graphics.Canvas
33+
import androidx.compose.ui.hapticfeedback.CupertinoHapticFeedback
34+
import androidx.compose.ui.hapticfeedback.HapticFeedback
3235
import androidx.compose.ui.input.InputMode
36+
import androidx.compose.ui.input.InputModeManager
3337
import androidx.compose.ui.input.key.Key
3438
import androidx.compose.ui.input.key.KeyEvent
3539
import androidx.compose.ui.input.key.KeyEventType
@@ -63,19 +67,18 @@ import androidx.compose.ui.uikit.LocalUIView
6367
import androidx.compose.ui.uikit.OnFocusBehavior
6468
import androidx.compose.ui.uikit.density
6569
import androidx.compose.ui.uikit.toNanoSeconds
66-
import androidx.compose.ui.InternalComposeUiApi
6770
import androidx.compose.ui.unit.Density
6871
import androidx.compose.ui.unit.DpOffset
6972
import androidx.compose.ui.unit.IntRect
7073
import androidx.compose.ui.unit.IntSize
7174
import androidx.compose.ui.unit.LayoutDirection
72-
import androidx.compose.ui.unit.toDpOffset
73-
import androidx.compose.ui.unit.toDpRect
74-
import androidx.compose.ui.unit.toDpSize
7575
import androidx.compose.ui.unit.dp
7676
import androidx.compose.ui.unit.round
7777
import androidx.compose.ui.unit.roundToIntRect
7878
import androidx.compose.ui.unit.roundToIntSize
79+
import androidx.compose.ui.unit.toDpOffset
80+
import androidx.compose.ui.unit.toDpRect
81+
import androidx.compose.ui.unit.toDpSize
7982
import androidx.compose.ui.unit.toOffset
8083
import androidx.compose.ui.unit.toSize
8184
import androidx.compose.ui.viewinterop.LocalInteropContainer
@@ -89,7 +92,6 @@ import androidx.compose.ui.window.KeyboardVisibilityListener
8992
import androidx.compose.ui.window.MetalRedrawer
9093
import androidx.compose.ui.window.OverlayInputView
9194
import androidx.compose.ui.window.TouchesEventKind
92-
import kotlin.Float
9395
import kotlin.coroutines.CoroutineContext
9496
import kotlin.time.Duration.Companion.seconds
9597
import kotlinx.cinterop.CValue
@@ -711,6 +713,10 @@ internal class ComposeSceneMediator(
711713
override val architectureComponentsOwner get() = this@ComposeSceneMediator.architectureComponentsOwner
712714
override val screenReader: PlatformScreenReader get() = platformScreenReader
713715

716+
override val hapticFeedback: HapticFeedback by lazy(LazyThreadSafetyMode.NONE) {
717+
CupertinoHapticFeedback()
718+
}
719+
714720
override fun convertLocalToWindowPosition(localPosition: Offset): Offset =
715721
windowContext.convertLocalToWindowPosition(_overlayView, localPosition)
716722

@@ -724,7 +730,11 @@ internal class ComposeSceneMediator(
724730
windowContext.convertScreenToLocalPosition(_overlayView, positionOnScreen)
725731

726732
override val viewConfiguration get() = this@ComposeSceneMediator.viewConfiguration
727-
override val inputModeManager = DefaultInputModeManager(InputMode.Touch)
733+
734+
override val inputModeManager by lazy(LazyThreadSafetyMode.NONE) {
735+
DefaultInputModeManager(InputMode.Touch)
736+
}
737+
728738
override val textInputService get() = this@ComposeSceneMediator.textInputServiceAdapter
729739
override val textToolbar get() = this@ComposeSceneMediator.textInputService.textToolbar
730740
override val semanticsOwnerListener get() = this@ComposeSceneMediator.semanticsOwnerListener

compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/node/RootNodeOwner.skiko.kt

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,6 @@ import androidx.compose.ui.layout.RootMeasurePolicy
6868
import androidx.compose.ui.layout.RulerProviderModifierElement
6969
import androidx.compose.ui.modifier.ModifierLocalManager
7070
import androidx.compose.ui.platform.DefaultAccessibilityManager
71-
import androidx.compose.ui.platform.DefaultHapticFeedback
7271
import androidx.compose.ui.platform.DelegatingSoftwareKeyboardController
7372
import androidx.compose.ui.platform.GraphicsLayerOwnerLayer
7473
import androidx.compose.ui.platform.LegacyRenderNodeLayer
@@ -162,7 +161,9 @@ internal class RootNodeOwner(
162161
owner.root.layoutDirection = value
163162
}
164163

165-
private val rootForTest = PlatformRootForTestImpl()
164+
private val rootForTest by lazy(LazyThreadSafetyMode.NONE) {
165+
PlatformRootForTestImpl()
166+
}
166167
private val ownedLayerManager = OwnedLayerManagerImpl()
167168
private val pointerInputEventProcessor = PointerInputEventProcessor(owner.root)
168169
private val measureAndLayoutDelegate = MeasureAndLayoutDelegate(owner.root)
@@ -469,7 +470,7 @@ internal class RootNodeOwner(
469470
override val sharedDrawScope = LayoutNodeDrawScope()
470471
override val layoutNodes: MutableIntObjectMap<LayoutNode> = mutableIntObjectMapOf()
471472
override val rootForTest get() = this@RootNodeOwner.rootForTest
472-
override val hapticFeedBack = DefaultHapticFeedback()
473+
override val hapticFeedBack get() = platformContext.hapticFeedback
473474
override val inputModeManager get() = platformContext.inputModeManager
474475
override val clipboardManager = createPlatformClipboardManager()
475476
override val clipboard = createPlatformClipboard()
@@ -484,10 +485,12 @@ internal class RootNodeOwner(
484485
// TODO https://youtrack.jetbrains.com/issue/CMP-1572
485486
override val autofillManager: AutofillManager? get() = null
486487
override val density get() = this@RootNodeOwner.density
487-
override val textInputService =
488+
override val textInputService by lazy(LazyThreadSafetyMode.NONE) {
488489
TextInputService(platformContext.textInputService)
489-
override val softwareKeyboardController =
490+
}
491+
override val softwareKeyboardController by lazy(LazyThreadSafetyMode.NONE) {
490492
DelegatingSoftwareKeyboardController(textInputService)
493+
}
491494

492495
private val textInputSessionMutex = SessionMutex<TextInputSession>()
493496
private inner class TextInputSession(
@@ -529,7 +532,11 @@ internal class RootNodeOwner(
529532
}
530533

531534
override val dragAndDropManager = this@RootNodeOwner.dragAndDropOwner
532-
override val pointerIconService = PointerIconServiceImpl()
535+
536+
override val pointerIconService by lazy(LazyThreadSafetyMode.NONE) {
537+
PointerIconServiceImpl()
538+
}
539+
533540
override val semanticsOwner = SemanticsOwner(root, rootSemanticsNode, layoutNodes)
534541
override val windowInfo get() = platformContext.windowInfo
535542
override val retainedValuesStore: RetainedValuesStore get() = ForgetfulRetainedValuesStore

compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/platform/DefaultHapticFeedback.skiko.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import androidx.compose.ui.hapticfeedback.HapticFeedback
2020
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
2121

2222
// TODO(demin): implement HapticFeedback
23-
internal class DefaultHapticFeedback : HapticFeedback {
23+
internal object DefaultHapticFeedback : HapticFeedback {
2424
override fun performHapticFeedback(hapticFeedbackType: HapticFeedbackType) {
2525
}
2626
}

compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/platform/PlatformContext.skiko.kt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import androidx.compose.ui.focus.FocusDirection
2626
import androidx.compose.ui.focus.FocusManager
2727
import androidx.compose.ui.geometry.Offset
2828
import androidx.compose.ui.geometry.Rect
29+
import androidx.compose.ui.hapticfeedback.HapticFeedback
2930
import androidx.compose.ui.input.InputMode
3031
import androidx.compose.ui.input.InputModeManager
3132
import androidx.compose.ui.input.pointer.PointerIcon
@@ -146,6 +147,7 @@ interface PlatformContext {
146147
}
147148

148149
val textToolbar: TextToolbar get() = EmptyTextToolbar
150+
val hapticFeedback: HapticFeedback get() = DefaultHapticFeedback
149151
fun setPointerIcon(pointerIcon: PointerIcon) = Unit
150152

151153
val parentFocusManager: FocusManager get() = EmptyFocusManager
@@ -237,7 +239,9 @@ interface PlatformContext {
237239
isWindowFocused = true
238240
}
239241

240-
override val inputModeManager: InputModeManager = DefaultInputModeManager()
242+
override val inputModeManager: InputModeManager by lazy(LazyThreadSafetyMode.NONE) {
243+
DefaultInputModeManager()
244+
}
241245
}
242246

243247
// This object must be immutable because it is used as a delegate in other ViewConfiguration

compose/ui/ui/src/webMain/kotlin/androidx/compose/ui/platform/DefaultHapticFeedback.web.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ internal class WebHapticFeedback : HapticFeedback {
4040
private val LongPressVibrationPattern = vibrationPatternOf(0, 30)
4141
private val VirtualKeyVibrationPattern = vibrationPatternOf(0, 20)
4242

43-
fun webHapticFeedbackOrDefault(): HapticFeedback = if (isVibrationSupported()) WebHapticFeedback() else DefaultHapticFeedback()
43+
fun webHapticFeedbackOrDefault(): HapticFeedback = if (isVibrationSupported()) WebHapticFeedback() else DefaultHapticFeedback
4444
}
4545

4646
override fun performHapticFeedback(hapticFeedbackType: HapticFeedbackType) {

0 commit comments

Comments
 (0)