On this talk we cover the compiler and runtime sides of Compose, the nature of Composable functions, effect handlers, composable lifecycles, scoping tasks to those and much more.
Runtime needs. Scans for @Composable functions generates convenient IR to include relevant info when called (1.4 IR) Makes them restartable / cacheable. Unlocks runtime optimizations. (Smart recomposition, parallel composition, ag "stable" apis...)
to all @Composable functions. How can we ensure Composer is passed across levels? @Composable fun Counter($composer: Composer, $key: Int) { val count = remember($composer, 123) { mutableStateOf(0) } Button($composer, 456, onClick = { count.value += 1 }) { Text($composer, 789, "Current count ${count.value}") } }
the Compiler frontend phase (static checks) fast feedback loop. Can only be called from other @Composable functions. Compiler can make the Composer available at all levels.
Composer during composition. First composition Adds nodes to the tree. Every recomposition Updates the table. Table interpreted later on materializes UI on screen But does it need to be UI?
type N. Slot table generic node structure. Reading composable tree emits changes over the table. Changes to add, remove, replace, move nodes based on logics (think of conditional logics). Changes emitted are generic Change<N>.
/ onLeave LifecycleObservers are called when adding / removing / updating elements on the table. Another type of Change can be recording a side e ect. internal typealias Change<N> = ( applier: Applier<N>, // interpreter materializes changes slots: SlotWriter, // write changes to the table lifecycleManager: LifecycleManager // lifecycle is relevant when applying changes ) -> Unit internal interface LifecycleManager { fun entering(instance: CompositionLifecycleObserver) fun leaving(instance: CompositionLifecycleObserver) fun sideEffect(effect: () -> Unit) }
Any relevant data required to materialize a UI snapshot. Operations to add / remove / replace UI nodes. Operations to store State. Operations to store remembered data (remember). Composable function calls and their parameters. Providers and Ambients. Side e ects of composition lifecycle (onEnter / onLeave). ...
when composition fails, and also smartly skip recomposition via the RecomposerScope. $composer.end() returns null if no observable model was read during the composition recomposition not needed. @Composable fun Counter($composer: Composer) { $composer.start() // ...our composable logics $composer.end()?.updateScope { nextComposer -> // this block will drive recomposition! Counter(nextComposer) } }
Remember result of a @Composable call and return it without computing it again. Think of the remember function. @Composable fun Modifier.verticalGradientScrim(color: Color, numStops: Int = 16): Modifier = composed { val colors = remember(color, numStops) { computeColors(color, numStops) } var height by remember { mutableStateOf(0f) } val brush = remember(color, numStops, startYPercentage, endYPercentage, height) { VerticalGradient( colors = colors, startY = height * startYPercentage, endY = height * endYPercentage )
Structured concurrency Parallel recomposition, o oad recomposition to di erent threads... Automatic Cancellation in e ect handlers ⏩ Can't replace it, but we can provide your own Applier impl and node types.
Bridges the gap between the Runtime and the Platform. The chosen Applier<N> implementation does the job. Provides integration with the device: layout, drawing (skia), user input...
both ViewGroups and Composable LayoutNodes The Applier is a visitor that visits the whole node tree element by element. class UiApplier(private val root: Any) : Applier<Any> { private val stack = Stack<Any>() override var current: Any = root override fun down(node: Any) { // adds a node stack.push(current) current = node } override fun up() { // pops a node to materialize it val instance = current val parent = stack.pop()
UI node Attached to an Owner when materialized. Owner Connection with the View system. It keeps a reference to its parent or the Owner when it's root. It keeps track of all children and how to measure / place those and itself.
UI testing libs that interpret changes by creating abstractions of the UI elements to assert over. Support other platforms like desktop or web. Control hardware minimize commands to re ect a change. ...
(Andrei Shikov @shikasd_) Building a web app with Compose Server side composition and client communication via websocket. Custom Applier<HtmlNode>. https://medium.com/@shikasd/composing-in-the-wild-145761ad62c3
e ects. Don't run e ects directly from composables. ➡ Composables are restartable (might run multiple times). Wrap them in e ect handlers to make the e ect lifecycle aware Make sure e ects run on the correct lifecycle step + correct environment + are bound by the Composable lifecycle.
Handlers. Non suspending e ects E.g: Run a side e ect to initialize a callback when the Composable enters the composition, and dipose it when it leaves. Suspending e ects E.g: Load data from network to feed some UI state.
e ect of composition lifecycle (observing onEnter / onLeave). Fired rst time and every time the inputs change. Requires onDispose at the end disposed on leaving composition and every time inputs change. @Composable fun backPressHandler(onBackPressed: () -> Unit, enabled: Boolean = true) { val dispatcher = BackPressedDispatcherAmbient.current.onBackPressedDispatcher val backCallback = remember { object : OnBackPressedCallback(enabled) { override fun handleOnBackPressed() { onBackPressed() } } } DisposableEffect(dispatcher) { // dispose/relaunch if dispatcher changes
thing, but given constant argument red only rst time and never more. Disposed on leaving composition. @Composable fun backPressHandler(onBackPressed: () -> Unit, enabled: Boolean = true) { val dispatcher = BackPressedDispatcherAmbient.current.onBackPressedDispatcher val backCallback = remember { object : OnBackPressedCallback(enabled) { override fun handleOnBackPressed() { onBackPressed() } } } DisposableEffect(Unit) { // Will never relaunch (constant key)
this composition or forget. (Discarded if composition fails). For e ects that do not require disposing. Runs after every composition / recomposition. Useful to publish updates to external states. @Composable fun MyScreen(drawerTouchHandler: TouchHandler) { val drawerState = rememberDrawerState(DrawerValue.Closed) SideEffect { drawerTouchHandler.enabled = drawerState.isOpen } // ... }
Use sparingly! ⚠ observe state instead smart recomposition when changes. For animations there are APIs to await for next frame. Requires handling thread safety manually. When source of truth is not a compose State. @Composable fun MyComposable(presenter: Presenter) { val user = presenter.loadUser { invalidate() } // not a State! Text(text = "The loaded user: ${user.name}) }
bound to this composition. Scope cancelled when leaving composition. Same scope returned across recompositions. Use this scope to launch jobs in response to user interactions. @Composable fun SearchScreen() { val scope = rememberCoroutineScope() var currentJob by remember { mutableStateOf<Job?>(null) } var items by remember { mutableStateOf<List<Item>>(emptyList()) } Column { Row { TextInput( afterTextChange = { text -> currentJob?.cancel() currentJob = scope.async { delay(threshold)
the e ect on the applier dispatcher (Usually AndroidUiDispatcher.Main) when entering. Cancels the e ect when leaving. Cancels and relaunches the e ect when subject changes. Useful to span a job across recompositions. @Composable fun SpeakerList(eventId: String) { var speakers by remember { mutableStateOf<List<Speaker>>(emptyList()) } LaunchedEffect(eventId) { // cancelled / relaunched when eventId varies speakers = viewModel.loadSpeakers(eventId) // suspended effect } ItemsVerticalList(speakers) }
produceState When used to feed a state. Relies on LaunchedEffect. @Composable fun SearchScreen(eventId: String) { val uiState = produceState(initialValue = emptyList<Speaker>(), eventId) { viewModel.loadSpeakers(eventId) // suspended effect } ItemsVerticalList(uiState.value) }