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. View Slide

  2. 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

    View Slide

  3. 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...)

    View Slide

  4. Compose compiler ⚙
    Composer will drive the composition / recomposition at runtime.

    View Slide

  5. 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}")
    }
    }

    View Slide

  6. 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.

    View Slide

  7. 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.

    View Slide

  8. 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?

    View Slide

  9. 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.

    View Slide

  10. 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>( // 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)
    // ...
    }

    View Slide

  11. 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 = (
    applier: Applier, // 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)
    }

    View Slide

  12. 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).
    ...

    View Slide

  13. Compose Runtime

    Slot table in depth.
    https://www.youtube.com/watch?v=6BRlI5zfCCk

    View Slide

  14. 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.

    View Slide

  15. 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)
    }
    }

    View Slide

  16. 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
    )

    View Slide

  17. 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.

    View Slide

  18. Compose UI

    Materialize all recorded changes into ultimate Android UI.
    Bridges the gap between the Runtime and the Platform.
    The chosen Applier implementation does the job.
    Provides integration with the device: layout, drawing (skia), user input...

    View Slide

  19. 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 {
    private val stack = Stack()
    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()

    View Slide

  20. 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.

    View Slide

  21. 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.
    ...

    View Slide

  22. 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.
    https://medium.com/@shikasd/composing-in-the-wild-145761ad62c3

    View Slide

  23. 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.

    View Slide

  24. 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!

    View Slide

  25. 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.

    View Slide

  26. 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

    View Slide

  27. 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)

    View Slide

  28. 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
    }
    // ...
    }

    View Slide

  29. 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})
    }

    View Slide

  30. 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(null) }
    var items by remember { mutableStateOf>(emptyList()) }
    Column {
    Row {
    TextInput(
    afterTextChange = { text ->
    currentJob?.cancel()
    currentJob = scope.async {
    delay(threshold)

    View Slide

  31. 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>(emptyList()) }
    LaunchedEffect(eventId) { // cancelled / relaunched when eventId varies
    speakers = viewModel.loadSpeakers(eventId) // suspended effect
    }
    ItemsVerticalList(speakers)
    }

    View Slide

  32. 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(), eventId) {
    viewModel.loadSpeakers(eventId) // suspended effect
    }
    ItemsVerticalList(uiState.value)
    }

    View Slide

  33. Surviving con g changes?
    No time! Will need to write a post

    Special thanks to Adam Powell

    View Slide

  34. Thank you!

    @JorgeCastilloPr

    View Slide