Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Yet another Compose talk, or maybe not.

Yet another Compose talk, or maybe not.

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.

Jorge Castillo

November 16, 2020
Tweet

More Decks by Jorge Castillo

Other Decks in Programming

Transcript

  1. Compose Architecture The Jetpack Compose libraries Compiler plugin - compose.compiler

    Runtime - compose.runtime UI - compose.ui Material - compose.material Foundation - compose.foundation Animation - compose.animation UI Tooling - androidx.ui
  2. Compose compiler ⚙ Plugin to generate metadata to satisfy the

    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...)
  3. Compose compiler ⚙ Compiler generated unique keys are also passed

    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}") } }
  4. Compiler checks ⚙ Impose a calling context Requirement imposed by

    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.
  5. Compose Runtime Compose runtime is declarative. The interpreter has the

    big picture Can decide how to execute / consume the program. Interpreter decoupled from the description of the program.
  6. Compose Runtime In-memory representation The Slot table Built by the

    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?
  7. Compose Runtime No. Compose runtime works with generic nodes of

    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>.
  8. Compose Runtime See how Layout emits a change to the

    table. emit "records" a change for inserting a UI node @Composable inline fun Layout(...) { emit<LayoutNode, Applier<Any>>( // emits change to insert a node ctor = { LayoutNode() }, update = { set(measureBlocks, LayoutEmitHelper.setMeasureBlocks) set(DensityAmbient.current, LayoutEmitHelper.setDensity) set(LayoutDirectionAmbient.current, LayoutEmitHelper.setLayoutDirection) }, ... children = children ) } recordApplierOperation { applier, _, _ -> applier.insert(insertIndex, node) // ... }
  9. Compose Runtime Change lambda that represents an e ect. onEnter

    / 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) }
  10. Compose Runtime What can be stored in the slot table?

    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). ...
  11. Compose Runtime Recomposition required? back to step one ⏮ Recorded

    side e ects run after lifecycle events to ensure onEnter before. Side e ects are discarded after a @Composable leaves the composition.
  12. Compose Runtime What about recomposition? Composer can discard pending compositions

    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) } }
  13. Compose Runtime Positional Memoization when reading from the slot table.

    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 )
  14. Compose Runtime Composition / recomposition intentionally coupled to KotlinX Coroutines.

    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.
  15. Compose UI Materialize all recorded changes into ultimate Android UI.

    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...
  16. Compose UI Built-in Applier implementation for Android: The UiApplier. Supports

    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()
  17. Compose UI LayoutNode is an in memory representation of a

    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.
  18. Compose UI Other use cases for custom Appliers / nodes?

    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. ...
  19. Compose UI Custom nodes and Applier - Practical use case

    (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
  20. E ect handlers They belong to the Runtime Apps contain

    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.
  21. E ect handlers They're under heavy development iterations. Disclaimer: Names

    and existing variants vary frequently. This covers e ect handlers as of today (1.0.0-SNAPSHOT). Expect changes! ⚠
  22. E ect handlers There are two categories of E ect

    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.
  23. E ect handlers DisposableE ect (old onCommit + onDispose) Side

    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
  24. E ect handlers DisposableE ect(Unit) (old onActive / onCommit(Unit)) Same

    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)
  25. E ect handlers SideE ect More like a re on

    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 } // ... }
  26. E ect handlers invalidate Invalidates composition locally enforces recomposition. ⚠

    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}) }
  27. E ect handlers rememberCoroutineScope - suspended e ects. Creates CoroutineScope

    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)
  28. E ect handlers LaunchedE ect - suspended e ects. Runs

    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) }
  29. E ect handlers LaunchedE ect can be simpli ed with

    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) }
  30. Surviving con g changes? No time! Will need to write

    a post Special thanks to Adam Powell