KEMBAR78
Compose in Theory | PPTX
© Instil Software 2020
Compose Part 1
(the theory)
Belfast Google Dev Group
September 2022
@GarthGilmour
https://instil.co
– Compose isn’t an Android thing
– It is not (just) a GUI thing either
– If you use the JVM it’s of interest
TLDR
TLDR: Compose was meant for you
1
Hello Compose
Hello Compose / Mosaic
fun printPyramid(builder: StringBuilder, height: Int) {
(1..height).forEach { row ->
val spaces = height - row
val hashes = (row * 2) - 1
repeat(spaces) { builder.append(" ") }
repeat(hashes) { builder.append("#") }
builder.append("n")
}
}
Hello Compose / Mosaic
fun main() = runMosaic {
var height by mutableStateOf(3)
setContent {
Column {
Text("Here is a Pyramid of height $height")
Text(buildString {
printPyramid(this, height)
})
}
}
Hello Compose / Mosaic
managing state
called via DSL
withContext(IO) {
val terminal = TerminalBuilder.terminal().apply {
enterRawMode()
}
val reader = terminal.reader()
while (true) {
when (val letter = reader.read()) {
'q'.code -> break
in '1'.code .. '9'.code -> {
height = letter - '0'.code
}
}
}
}
}
Hello Compose / Mosaic
state change
– Our UI is a single block of DSL
– This would be very hard to maintain
– Instead, we create composable functions
Refactoring Our Example
Creating composable functions
fun main() = runMosaic {
var height by mutableStateOf(3)
setContent { PyramidDisplay(height) }
// as before
}
@Composable
fun PyramidDisplay(height: Int) {
Column {
PyramidTitle(height)
PyramidBody(height)
}
}
Refactoring Our Example
@Composable
fun PyramidTitle(height: Int) {
Text("Here is a Pyramid of height $height")
}
@Composable
fun PyramidBody(height: Int) {
Text(buildString {
printPyramid(this, height)
})
}
Refactoring Our Example
– Composable functions can be used like normal
– A compiler plugin performs magic on our behalf
– The annotation acts more like the suspend keyword
– As opposed to annotations in JUnit, Spring, JPA etc...
Refactoring Our Example
Creating composable functions
2
What is Compose?
– It builds and manage tree-based structures
– The manage part is what where the innovation lies
– The Compose Runtime is agnostic
– It doesn’t care what the purpose of the tree is
– It could be rendering HTML, JSON or Bytecode
– When the state changes the tree is rebuilt
– In as efficient a way as possible
What is Compose?
At its core, Compose is designed to efficiently
build and maintain tree-like data structures. More
specifically, it provides a programming model to
describe how that tree will change over time.
Leland
Richardson
Compose From First Principles
– Compose is about managing trees
– All UI’s are trees of components
– Therefore, Compose is about UI’s
What is Compose?
The error everyone makes
Compose Compiler
Compose Runtime
Compose UI
Android Desktop Web Other
Remember Kotlin Is Multiplatform
So Compose can be as well
COMMON
KOTLIN
KOTLIN ANDROID
KOTLIN JS
KOTLIN NATIVE
KOTLIN JVM
NATIVE
ARTEFACT
JS BUNDLE
JAR
The Molecule Library
– Reactive API’s are commonly used for async functions
– RxJava in Android and Reactor / WebFlux in Spring
– The coding style is complex compared to procedural code
– Especially when combining multiple streams via flatMap
Combining Compose and Coroutines
The Molecule Library
– Molecule let’s you use Compose to create Flows
– The Compose Runtime is used to help aggregate data
– It will recompose each time observed data changes
– You can produce reactive outputs via procedural code
Combining Compose and Coroutines
fun messenger(max: Int) = moleculeFlow(clock = Immediate) {
var count by remember { mutableStateOf(1) }
LaunchedEffect(Unit) {
while (count < max) {
delay(1000)
count++
}
}
"Flow message $count"
}
The Molecule Library
fun main(): Unit = runBlocking {
launch {
while (true) {
println("Main is not blocked")
delay(500)
}
}
messenger(5).collect(::println)
}
Flow message 1
Main is not blocked
Main is not blocked
Main is not blocked
Flow message 2
Main is not blocked
Main is not blocked
Flow message 3
Main is not blocked
Flow message 4
Main is not blocked
Main is not blocked
Flow message 5
Main is not blocked
Main is not blocked
Main is not blocked
...
The Molecule Library
Initial Conclusions
– A declarative library for User Interfaces
– The default UI toolkit for Android Apps
– An emerging multiplatform library for UI’s
– A general way to manage tree-based data
– A neat way to use coroutines and flows
What exactly is compose?
3
Looking Deeper
Looking Deeper
Via Desktop Compose
– Let’s switch to Desktop Compose
– It uses the full Compiler and Runtime
– Our demo will be a simple calculator
fun main() {
application {
Window(
onCloseRequest = ::exitApplication,
title = "Calculator via Compose for Desktop",
state = rememberWindowState().apply {
size = DpSize(Dp(350f), Dp(400f))
}
) {
Calculator()
}
}
}
The Container For Our Calculator
@Composable
fun Calculator() {
val savedTotal = remember { mutableStateOf(0) }
val displayedTotal = remember { mutableStateOf(0) }
val operationOngoing = remember { mutableStateOf(Operation.None) }
val operationJustChanged = remember { mutableStateOf(false) }
// event handlers here
GDGBelfastTheme {
// UI layout here
}
}
The Calculator In Outline
val clearSelected = {
displayedTotal.value = 0
savedTotal.value = 0
operationOngoing.value = Operation.None
operationJustChanged.value = false
}
val operationSelected = { op: Operation ->
operationJustChanged.value = true
if (operationOngoing.value != None) {
doOperation()
savedTotal.value = displayedTotal.value
}
operationOngoing.value = op
}
Sample Event Handlers
@Composable
fun NumberButton(onClick: () -> Unit, number: Int) =
Button(
onClick = onClick,
modifier = Modifier.padding(all = 5.dp)
) {
Text(
number.toString(),
style = TextStyle(color = Color.Black, fontSize = 18.sp)
)
}
A Sample Component
introducing javap
the java bytecode decompiler
% javap
Usage: javap <options> <classes>
where possible options include:
...
-p -private Show all classes and members
-c Disassemble the code
-s Print internal type signatures
...
--module-path <path> Specify where to find application modules
--system <jdk> Specify where to find system modules
...
-cp <path> Specify where to find user class files
...
Decompiling The Calculator
Output with package names stripped
Compose: javap org.gdg.belfast.compose.calculator.CalculatorKt
Compiled from "Calculator.kt"
public final class CalculatorKt {
public static final void GDGBelfastTheme(
Function2<? super Composer, ? super Integer, kotlin.Unit>, Composer, int
);
public static final void DisplayText(String, Composer, int);
public static final void NumberButton(Function0<kotlin.Unit>, int, Composer, int);
public static final void OperationButton(Function0<kotlin.Unit>, String, Composer, int);
public static final void Calculator(Composer, int);
}
– The compiler plugin adds a Composer parameter
– This has methods to create and maintain the tree
– It also adds an integer parameter, which acts as a key
Understanding Composable Functions
What is the Compose Compiler up to?
Decompiling Composable Functions
Note the extra parameters
public static final void DisplayText(
java.lang.String,
androidx.compose.runtime.Composer,
int
);
public static final void NumberButton(
kotlin.jvm.functions.Function0<kotlin.Unit>,
int,
androidx.compose.runtime.Composer,
int
);
The Composer Interface
– Composable functions emit nodes
– Which are added to the tree being (re)built
– They may also return a value
Understanding Composable Functions
What is the Compose Runtime up to?
@Composable
inline fun <T> remember(calculation: @DisallowComposableCalls () -> T): T =
currentComposer.cache(false, calculation)
– Composable functions are assumed to be pure
– No implied sequencing, IO, use of global data etc..
– They can then be run in any order and/or in parallel
– There are types available to encapsulate side-effects
Understanding Composable Functions
What is the Compose Runtime up to?
Calls To The Composer
Package names are stripped
Compiled from "Calculator.kt"
public final class CalculatorKt {
public static final void NumberButton(Function0<kotlin.Unit>, int, Composer, int);
Code:
9: invokeinterface #25, 2 // InterfaceMethod Composer.startRestartGroup:
35: invokeinterface #37, 2 // InterfaceMethod Composer.changed
62: invokeinterface #171, 2 // InterfaceMethod Composer.changed
92: invokeinterface #41, 1 // InterfaceMethod Composer.getSkipping
166: invokeinterface #79, 1 // InterfaceMethod Composer.skipToGroupEnd
172: invokeinterface #83, 1 // InterfaceMethod Composer.endRestartGroup
198: invokeinterface #97, 2 // InterfaceMethod ScopeUpdateScope.updateScope
203: return
– Composable functions will be reused in the same layout
– For example, a Table used twice with different data sets
– Compose must remember the state at each call site
– It cannot globally cache data just once for each control
Positional Memoization
How Compose caches nodes and state
– Caching data per call is termed Positional Memoization
– Remembering what was built for each call to the function
– Tricky when the calls are in conditionals or loops
Positional Memoization
How Compose caches nodes and state
– This is the purpose of the second additional parameter
– Added to composable functions by the compiler plugin
– It is a relative key that identifies an individual call
Positional Memoization
How Compose caches nodes and state
– The underlying structure is a Gap Buffer / Slot Table
– This is a flat array / list to optimize performance
– A hierarchical structure is created via groups
Positional Memoization
How Compose caches nodes and state
Group
(123)
Group
(456)
Group
(789)
... ... ...
– Nested components add groups for child components
– We walk the graph depth first to build a linear structure
– On every call we can insert a new group into the table
– But the bet is that the tree structure will rarely change
Positional Memoization
How Compose caches nodes and state
– The Composer is not tightly coupled to the Gap Table
– Instead, the dependency is on the Applier interface
– This contains general methods for manipulating trees
– The implementation can be anything you like
The Applier Interface
Specifying your own data structure
interface Applier<N> {
val current: N
fun onBeginChanges() {}
fun onEndChanges() {}
fun down(node: N)
fun up()
fun insertTopDown(index: Int, instance: N)
fun insertBottomUp(index: Int, instance: N)
fun remove(index: Int, count: Int)
fun move(from: Int, to: Int, count: Int)
fun clear()
}
The Applier Interface
4
Conclusions
– Compose addresses a critical need in Android
– It also fixes an outstanding pain point on the Desktop
– Whether it takes off elsewhere remains to the seen
– It continues the trend of Multiplatform Kotlin
– A lot more innovation can be expected
Conclusions
One DSL to rule them all?
Questions?

Compose in Theory

  • 1.
    © Instil Software2020 Compose Part 1 (the theory) Belfast Google Dev Group September 2022
  • 2.
  • 3.
    – Compose isn’tan Android thing – It is not (just) a GUI thing either – If you use the JVM it’s of interest TLDR
  • 4.
    TLDR: Compose wasmeant for you
  • 5.
  • 6.
  • 7.
    fun printPyramid(builder: StringBuilder,height: Int) { (1..height).forEach { row -> val spaces = height - row val hashes = (row * 2) - 1 repeat(spaces) { builder.append(" ") } repeat(hashes) { builder.append("#") } builder.append("n") } } Hello Compose / Mosaic
  • 8.
    fun main() =runMosaic { var height by mutableStateOf(3) setContent { Column { Text("Here is a Pyramid of height $height") Text(buildString { printPyramid(this, height) }) } } Hello Compose / Mosaic managing state called via DSL
  • 9.
    withContext(IO) { val terminal= TerminalBuilder.terminal().apply { enterRawMode() } val reader = terminal.reader() while (true) { when (val letter = reader.read()) { 'q'.code -> break in '1'.code .. '9'.code -> { height = letter - '0'.code } } } } } Hello Compose / Mosaic state change
  • 10.
    – Our UIis a single block of DSL – This would be very hard to maintain – Instead, we create composable functions Refactoring Our Example Creating composable functions
  • 11.
    fun main() =runMosaic { var height by mutableStateOf(3) setContent { PyramidDisplay(height) } // as before } @Composable fun PyramidDisplay(height: Int) { Column { PyramidTitle(height) PyramidBody(height) } } Refactoring Our Example
  • 12.
    @Composable fun PyramidTitle(height: Int){ Text("Here is a Pyramid of height $height") } @Composable fun PyramidBody(height: Int) { Text(buildString { printPyramid(this, height) }) } Refactoring Our Example
  • 13.
    – Composable functionscan be used like normal – A compiler plugin performs magic on our behalf – The annotation acts more like the suspend keyword – As opposed to annotations in JUnit, Spring, JPA etc... Refactoring Our Example Creating composable functions
  • 15.
  • 16.
    – It buildsand manage tree-based structures – The manage part is what where the innovation lies – The Compose Runtime is agnostic – It doesn’t care what the purpose of the tree is – It could be rendering HTML, JSON or Bytecode – When the state changes the tree is rebuilt – In as efficient a way as possible What is Compose?
  • 17.
    At its core,Compose is designed to efficiently build and maintain tree-like data structures. More specifically, it provides a programming model to describe how that tree will change over time. Leland Richardson Compose From First Principles
  • 18.
    – Compose isabout managing trees – All UI’s are trees of components – Therefore, Compose is about UI’s What is Compose? The error everyone makes
  • 19.
    Compose Compiler Compose Runtime ComposeUI Android Desktop Web Other
  • 20.
    Remember Kotlin IsMultiplatform So Compose can be as well COMMON KOTLIN KOTLIN ANDROID KOTLIN JS KOTLIN NATIVE KOTLIN JVM NATIVE ARTEFACT JS BUNDLE JAR
  • 24.
    The Molecule Library –Reactive API’s are commonly used for async functions – RxJava in Android and Reactor / WebFlux in Spring – The coding style is complex compared to procedural code – Especially when combining multiple streams via flatMap Combining Compose and Coroutines
  • 25.
    The Molecule Library –Molecule let’s you use Compose to create Flows – The Compose Runtime is used to help aggregate data – It will recompose each time observed data changes – You can produce reactive outputs via procedural code Combining Compose and Coroutines
  • 26.
    fun messenger(max: Int)= moleculeFlow(clock = Immediate) { var count by remember { mutableStateOf(1) } LaunchedEffect(Unit) { while (count < max) { delay(1000) count++ } } "Flow message $count" } The Molecule Library
  • 27.
    fun main(): Unit= runBlocking { launch { while (true) { println("Main is not blocked") delay(500) } } messenger(5).collect(::println) } Flow message 1 Main is not blocked Main is not blocked Main is not blocked Flow message 2 Main is not blocked Main is not blocked Flow message 3 Main is not blocked Flow message 4 Main is not blocked Main is not blocked Flow message 5 Main is not blocked Main is not blocked Main is not blocked ... The Molecule Library
  • 28.
    Initial Conclusions – Adeclarative library for User Interfaces – The default UI toolkit for Android Apps – An emerging multiplatform library for UI’s – A general way to manage tree-based data – A neat way to use coroutines and flows What exactly is compose?
  • 29.
  • 30.
    Looking Deeper Via DesktopCompose – Let’s switch to Desktop Compose – It uses the full Compiler and Runtime – Our demo will be a simple calculator
  • 31.
    fun main() { application{ Window( onCloseRequest = ::exitApplication, title = "Calculator via Compose for Desktop", state = rememberWindowState().apply { size = DpSize(Dp(350f), Dp(400f)) } ) { Calculator() } } } The Container For Our Calculator
  • 32.
    @Composable fun Calculator() { valsavedTotal = remember { mutableStateOf(0) } val displayedTotal = remember { mutableStateOf(0) } val operationOngoing = remember { mutableStateOf(Operation.None) } val operationJustChanged = remember { mutableStateOf(false) } // event handlers here GDGBelfastTheme { // UI layout here } } The Calculator In Outline
  • 33.
    val clearSelected ={ displayedTotal.value = 0 savedTotal.value = 0 operationOngoing.value = Operation.None operationJustChanged.value = false } val operationSelected = { op: Operation -> operationJustChanged.value = true if (operationOngoing.value != None) { doOperation() savedTotal.value = displayedTotal.value } operationOngoing.value = op } Sample Event Handlers
  • 34.
    @Composable fun NumberButton(onClick: ()-> Unit, number: Int) = Button( onClick = onClick, modifier = Modifier.padding(all = 5.dp) ) { Text( number.toString(), style = TextStyle(color = Color.Black, fontSize = 18.sp) ) } A Sample Component
  • 35.
    introducing javap the javabytecode decompiler % javap Usage: javap <options> <classes> where possible options include: ... -p -private Show all classes and members -c Disassemble the code -s Print internal type signatures ... --module-path <path> Specify where to find application modules --system <jdk> Specify where to find system modules ... -cp <path> Specify where to find user class files ...
  • 36.
    Decompiling The Calculator Outputwith package names stripped Compose: javap org.gdg.belfast.compose.calculator.CalculatorKt Compiled from "Calculator.kt" public final class CalculatorKt { public static final void GDGBelfastTheme( Function2<? super Composer, ? super Integer, kotlin.Unit>, Composer, int ); public static final void DisplayText(String, Composer, int); public static final void NumberButton(Function0<kotlin.Unit>, int, Composer, int); public static final void OperationButton(Function0<kotlin.Unit>, String, Composer, int); public static final void Calculator(Composer, int); }
  • 37.
    – The compilerplugin adds a Composer parameter – This has methods to create and maintain the tree – It also adds an integer parameter, which acts as a key Understanding Composable Functions What is the Compose Compiler up to?
  • 38.
    Decompiling Composable Functions Notethe extra parameters public static final void DisplayText( java.lang.String, androidx.compose.runtime.Composer, int ); public static final void NumberButton( kotlin.jvm.functions.Function0<kotlin.Unit>, int, androidx.compose.runtime.Composer, int );
  • 39.
  • 40.
    – Composable functionsemit nodes – Which are added to the tree being (re)built – They may also return a value Understanding Composable Functions What is the Compose Runtime up to? @Composable inline fun <T> remember(calculation: @DisallowComposableCalls () -> T): T = currentComposer.cache(false, calculation)
  • 41.
    – Composable functionsare assumed to be pure – No implied sequencing, IO, use of global data etc.. – They can then be run in any order and/or in parallel – There are types available to encapsulate side-effects Understanding Composable Functions What is the Compose Runtime up to?
  • 42.
    Calls To TheComposer Package names are stripped Compiled from "Calculator.kt" public final class CalculatorKt { public static final void NumberButton(Function0<kotlin.Unit>, int, Composer, int); Code: 9: invokeinterface #25, 2 // InterfaceMethod Composer.startRestartGroup: 35: invokeinterface #37, 2 // InterfaceMethod Composer.changed 62: invokeinterface #171, 2 // InterfaceMethod Composer.changed 92: invokeinterface #41, 1 // InterfaceMethod Composer.getSkipping 166: invokeinterface #79, 1 // InterfaceMethod Composer.skipToGroupEnd 172: invokeinterface #83, 1 // InterfaceMethod Composer.endRestartGroup 198: invokeinterface #97, 2 // InterfaceMethod ScopeUpdateScope.updateScope 203: return
  • 43.
    – Composable functionswill be reused in the same layout – For example, a Table used twice with different data sets – Compose must remember the state at each call site – It cannot globally cache data just once for each control Positional Memoization How Compose caches nodes and state
  • 44.
    – Caching dataper call is termed Positional Memoization – Remembering what was built for each call to the function – Tricky when the calls are in conditionals or loops Positional Memoization How Compose caches nodes and state
  • 45.
    – This isthe purpose of the second additional parameter – Added to composable functions by the compiler plugin – It is a relative key that identifies an individual call Positional Memoization How Compose caches nodes and state
  • 46.
    – The underlyingstructure is a Gap Buffer / Slot Table – This is a flat array / list to optimize performance – A hierarchical structure is created via groups Positional Memoization How Compose caches nodes and state Group (123) Group (456) Group (789) ... ... ...
  • 47.
    – Nested componentsadd groups for child components – We walk the graph depth first to build a linear structure – On every call we can insert a new group into the table – But the bet is that the tree structure will rarely change Positional Memoization How Compose caches nodes and state
  • 48.
    – The Composeris not tightly coupled to the Gap Table – Instead, the dependency is on the Applier interface – This contains general methods for manipulating trees – The implementation can be anything you like The Applier Interface Specifying your own data structure
  • 49.
    interface Applier<N> { valcurrent: N fun onBeginChanges() {} fun onEndChanges() {} fun down(node: N) fun up() fun insertTopDown(index: Int, instance: N) fun insertBottomUp(index: Int, instance: N) fun remove(index: Int, count: Int) fun move(from: Int, to: Int, count: Int) fun clear() } The Applier Interface
  • 50.
  • 51.
    – Compose addressesa critical need in Android – It also fixes an outstanding pain point on the Desktop – Whether it takes off elsewhere remains to the seen – It continues the trend of Multiplatform Kotlin – A lot more innovation can be expected Conclusions One DSL to rule them all?
  • 54.