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

Composing an API the *right* way (Droidcon New ...

Márton Braun
September 19, 2024

Composing an API the *right* way (Droidcon New York 2024)

Everyone who writes code using Jetpack Compose designs Composable functions and components all the time. In this talk, we’ll take a look at some highlights from the official guidelines around designing Compose APIs, to see how we can do a better job of building with Compose.

More: https://zsmb.co/talks/composing-an-api-the-right-way/

Márton Braun

September 19, 2024
Tweet

More Decks by Márton Braun

Other Decks in Programming

Transcript

  1. Constants const val DefaultKeyName = "__defaultKey" val StructurallyEqual: ComparisonPolicy =

    StructurallyEqualsImpl() object ReferenceEqual : ComparisonPolicy {} sealed class LoadResult<T> { object Loading : LoadResult<Nothing>() class Done<T>(val result: T) : LoadResult<T>() class Error(val cause: Throwable) : LoadResult<Nothing>() }
  2. Constants const val DefaultKeyName = "__defaultKey" val StructurallyEqual: ComparisonPolicy =

    StructurallyEqualsImpl() object ReferenceEqual : ComparisonPolicy {} sealed class LoadResult<T> { object Loading : LoadResult<Nothing>() class Done<T>(val result: T) : LoadResult<T>() class Error(val cause: Throwable) : LoadResult<Nothing>() } enum class Status { Idle, Busy, }
  3. Functions Return a value Emit content @Composable fun Column(...) @Composable

    fun LaunchedEffect(...) @Composable fun Button(...)
  4. Functions Return a value @Composable fun stringResource( @StringRes id: Int

    ): String @Composable fun rememberScrollState( initial: Int = 0 ): ScrollState Emit content @Composable fun Column(...) @Composable fun LaunchedEffect(...) @Composable fun Button(...)
  5. Functions @Composable fun ColorScheme(darkTheme: Boolean): ColorScheme @Composable fun colorScheme(darkTheme: Boolean):

    ColorScheme @Composable fun rememberCoroutineScope(): CoroutineScope @Composable fun CoroutineScope(): CoroutineScope
  6. Components @Composable fun Header() { Column { Text("Kotlin programming language")

    Text("Concise. Cross-platform. Fun.") Row { Button { Text("Docs") } Button { Text("Blog") } } } }
  7. Components @Composable fun Header() { Column { Text("Kotlin programming language")

    Text("Concise. Cross-platform. Fun.") Row { Button { Text("Docs") } Button { Text("Blog") } } } }
  8. Components @Composable fun Header() { Column { Row { Button

    { Text("Docs") } Button { Text("Blog") } } } } Text("Kotlin programming language") Text("Concise. Cross-platform. Fun.")
  9. Text("Kotlin programming language") Text("Concise. Cross-platform. Fun.") @Composable fun Title() {

    } Components @Composable fun Header() { Column { Row { Button { Text("Docs") } Button { Text("Blog") } } } }
  10. Components Text("Kotlin programming language") Text("Concise. Cross-platform. Fun.") @Composable fun Title()

    { } @Composable fun Header() { Column { Title() Row { Button { Text("Docs") } Button { Text("Blog") } } } }
  11. Components Text("Kotlin programming language") Text("Concise. Cross-platform. Fun.") @Composable fun Title()

    { } @Composable fun Header() { Column { Title() Row { Button { Text("Docs") } Button { Text("Blog") } } } }
  12. Components Kotlin programming language Concise. Cross-platform. Fun. Text("Kotlin programming language")

    Text("Concise. Cross-platform. Fun.") @Composable fun Title() { Column { } }
  13. Components Concise. Cross-platform. Fun. Kotlin programming language Text("Kotlin programming language")

    Text("Concise. Cross-platform. Fun.") @Composable fun Title() { Column { } }
  14. Components @Composable fun InputField : { // ... } val

    inputState = InputField Button(onClick = { inputState.value = "" }) { Text("Clear input") } UserInputState () ()
  15. Components @Composable fun InputField : { // ... } Button(onClick

    = { inputState.value = "" }) { Text("Clear input") } InputField UserInputState val inputState = () ()
  16. Components @Composable fun InputField : UserInputState { // ... }

    InputField () () Button(onClick = { inputState.value = "" }) { Text("Clear input") } val inputState =
  17. Components @Composable fun InputField inputState: { // ... } remember

    { UserInputState() } InputField inputState UserInputState val inputState = ( ) ( ) Button(onClick = { inputState.value = "" }) { Text("Clear input") }
  18. Components @Composable fun InputField(inputState: UserInputState) { // ... } val

    inputState = remember { UserInputState() } InputField(inputState) Button(onClick = { inputState.value = "" }) { Text("Clear input") }
  19. Components @Composable fun InputField(inputState: UserInputState) { // ... } val

    inputState = remember { UserInputState() } InputField(inputState) Button(onClick = { inputState.value = "" }) { Text("Clear input") }
  20. Components @Composable fun InputField(inputState: UserInputState) { // ... } val

    inputState = remember { UserInputState() } Button(onClick = { inputState.value = "" }) { Text("Clear input") } InputField(inputState)
  21. Default arguments @Composable fun Text( text: String, modifier: Modifier =

    Modifier, color: Color = Color.Unspecified, lineHeight: TextUnit = TextUnit.Unspecified, overflow: TextOverflow = TextOverflow.Clip, softWrap: Boolean = true, maxLines: Int = Int.MAX_VALUE, onTextLayout: (TextLayoutResult) -> Unit = {}, style: TextStyle = LocalTextStyle.current, )
  22. Default arguments @Composable fun Text( text: String, modifier: Modifier =

    Modifier, color: Color = Color.Unspecified, lineHeight: TextUnit = TextUnit.Unspecified, overflow: TextOverflow = TextOverflow.Clip, softWrap: Boolean = true, maxLines: Int = Int.MAX_VALUE, onTextLayout: (TextLayoutResult) -> Unit = {}, style: TextStyle = LocalTextStyle.current, )
  23. @Composable fun Text( text: String, modifier: Modifier = Modifier, color:

    Color = Color.Unspecified, lineHeight: TextUnit = TextUnit.Unspecified, overflow: TextOverflow = TextOverflow.Clip, softWrap: Boolean = true, maxLines: Int = Int.MAX_VALUE, onTextLayout: (TextLayoutResult) -> Unit = {}, style: TextStyle = LocalTextStyle.current, Default arguments )
  24. Default arguments @Composable fun Text( text: String, modifier: Modifier =

    Modifier, color: Color = Color.Unspecified, lineHeight: TextUnit = TextUnit.Unspecified, overflow: TextOverflow = TextOverflow.Clip, softWrap: Boolean = true, maxLines: Int = Int.MAX_VALUE, onTextLayout: (TextLayoutResult) -> Unit = {}, style: TextStyle? = null, val actualStyle = style ?: LocalTextStyle.current // ... } ) {
  25. Default arguments @Composable fun Button( onClick: () -> Unit, modifier:

    Modifier = Modifier, enabled: Boolean = true, shape: Shape = ButtonDefaults.shape, colors: ButtonColors = ButtonDefaults.buttonColors(), elevation: ButtonElevation? = ButtonDefaults.buttonElevation(), border: BorderStroke? = null, contentPadding: PaddingValues = ButtonDefaults.ContentPadding, content: @Composable RowScope.() -> Unit, )
  26. Default arguments @Composable fun Button( onClick: () -> Unit, modifier:

    Modifier = Modifier, enabled: Boolean = true, shape: Shape = ButtonDefaults.shape, colors: ButtonColors = ButtonDefaults.buttonColors(), elevation: ButtonElevation? = ButtonDefaults.buttonElevation(), border: BorderStroke? = null, contentPadding: PaddingValues = ButtonDefaults.ContentPadding, content: @Composable RowScope.() -> Unit, )
  27. Default arguments object ButtonDefaults { val ContentPadding: PaddingValues val MinWidth:

    Dp val MinHeight: Dp val IconSize: Dp val IconSpacing: Dp val shape: Shape @Composable get @Composable fun buttonElevation(...): ButtonElevation = ButtonElevation(...) } @Composable fun buttonColors(...): ButtonColors = ButtonColors(...) Button( onClick = { viewModel.savePage() }, colors = customColors ?: ButtonDefaults.buttonColors(),
  28. Default arguments object ButtonDefaults { } @Composable fun buttonColors(...): ButtonColors

    = ButtonColors(...) Button( onClick = { viewModel.savePage() }, colors = customColors ?: ButtonDefaults.buttonColors(), ) { Text("Save") }
  29. Default arguments internal object ButtonDefaults { @Composable fun buttonColors(...): ButtonColors

    = ButtonColors(...) } Button( onClick = { viewModel.savePage() }, colors = customColors ?: ButtonDefaults.buttonColors(), ) { Text("Save") }
  30. modifier: Modifier = Modifier, Modifiers PageTitle( text = "Home", modifier

    = Modifier .width(100.dp) .height(40.dp) .background(Color.LightGray) .padding(horizontal = 12.dp) ) @Composable fun PageTitle( text: String, )
  31. modifier: Modifier = Modifier, Modifiers PageTitle( text = "Home", modifier

    = Modifier .width(100.dp) .height(40.dp) .background(Color.LightGray) .padding(horizontal = 12.dp) ) padding: Dp = 0.dp, @Composable fun PageTitle( text: String, )
  32. Modifiers @Composable fun PageTitle( text: String, ) Box( Modifier .padding(12.dp)

    .background(Color.Blue) ) { PageTitle("Home") } @Composable fun PageTitle( text: String, modifier: Modifier = Modifier, ) PageTitle( "Home", Modifier .padding(12.dp) .background(Color.Blue), )
  33. Modifiers › Accept a Modifier parameter › Named modifier ›

    First optional parameter › Applied to the root node
  34. Modifiers › Accept a Modifier parameter › Named modifier ›

    First optional parameter › Applied to the root node › Default value Modifier
  35. Modifiers › Accept a Modifier parameter › Named modifier ›

    First optional parameter › Applied to the root node › Default value Modifier
  36. Modifiers › Accept a Modifier parameter › Named modifier ›

    First optional parameter › Applied to the root node › Default value Modifier
  37. Modifiers › Accept a Modifier parameter › Named modifier ›

    First optional parameter › Applied to the root node › Default value Modifier
  38. (modifier) (modifier) Modifiers @Composable fun Header( modifier: Modifier = Modifier,

    ) { Column Title() Row Button(onClick = { ... }) { Text("Docs") } Button(onClick = { ... }) { Text("Blog") } } } } { {
  39. (modifier) (modifier) Modifiers @Composable fun Header( modifier: Modifier = Modifier,

    ) { Column Title() Row Button(onClick = { ... }) { Text("Docs") } Button(onClick = { ... }) { Text("Blog") } } } } { {
  40. (modifier) (modifier) Modifiers @Composable fun Header( modifier: Modifier = Modifier,

    ) { Column Title() Row Button(onClick = { ... }) { Text("Docs") } Button(onClick = { ... }) { Text("Blog") } } } } { {
  41. Modifiers @Composable fun MenuItems( smallScreen: Boolean, modifier: Modifier = Modifier,

    ) { if (smallScreen) { Column(modifier) { Text(...) Text(...) } } else { Row(modifier) { Text(...) Text(...) } } }
  42. Modifiers › Accept a Modifier parameter › Named modifier ›

    First optional parameter › Applied to the root node › Default value Modifier
  43. Modifiers modifier: Modifier = Modifier interface Modifier { infix fun

    then(other: Modifier): Modifier = CombinedModifier(this, other) companion object : Modifier { ... override infix fun then(other: Modifier): Modifier = other override fun toString() = "Modifier" } }
  44. Modifiers @Composable fun Topics( topic: List<Topic>, modifier: Modifier = Modifier,

    ) { Row( modifier = modifier ) { // ... } } .padding(12.dp),
  45. Modifiers @Composable fun Topics( topic: List<Topic>, modifier: Modifier = Modifier,

    ) { Row( modifier = Modifier .then(modifier) ) { // ... } } .padding(12.dp) ,
  46. Slots @Composable fun Button( content: @Composable , ) Button {

    } } () -> Unit Row { ) ) Icon(Icons.Default.Build, "Build" Text("Build project" this: RowScope
  47. Slots @Composable fun Button( content: @Composable RowScope. , ) {

    content() } } Button { } this: RowScope () -> Unit Row { ) ) Icon(Icons.Default.Build, "Build" Text("Build project"
  48. Slots @Composable fun Button( content: @Composable RowScope. , ) {

    content() } } Button { } this: RowScope () -> Unit Row { ) ) Icon(Icons.Default.Build, "Build" Text("Build project"
  49. Slots Button { , Modifier.weight(1f) , Modifier.weight(4f) } this: RowScope

    ) ) @Composable fun Button( content: @Composable RowScope. , ) { content() } } () -> Unit Row { Icon(Icons.Default.Build, "Build" Text("Build project"
  50. Slots @Composable fun Button( content: @Composable RowScope.() -> Unit, )

    { Row { content() } } Button { , Modifier.weight(1f)) , Modifier.weight(4f)) } this: RowScope Icon(Icons.Default.Build, "Build" Text("Build project"
  51. Slot lifecycle @Composable fun PreferenceItem( checked: Boolean, content: @Composable ()

    -> Unit, ) { if (checked) { Row { Text("Checked") content() } } else { Column { Text("Unchecked") content() } } }
  52. Slot lifecycle @Composable fun PreferenceItem( checked: Boolean, content: @Composable ()

    -> Unit, ) { if (checked) { Row { Text("Checked") content() } } else { Column { Text("Unchecked") content() } } }
  53. Slot lifecycle @Composable fun PreferenceItem( checked: Boolean, content: @Composable ()

    -> Unit, ) { if (checked) { Row { Text("Checked") content() } } else { Column { Text("Unchecked") content() } } } PreferenceItem
  54. Slot lifecycle @Composable fun PreferenceItem( checked: Boolean, content: @Composable ()

    -> Unit, ) { if (checked) { Row { Text("Checked") content() } } else { Column { Text("Unchecked") content() } } } PreferenceItem content
  55. Slot lifecycle val movableContent = remember(content) { movableContentOf(content) } @Composable

    fun PreferenceItem( checked: Boolean, content: @Composable () -> Unit, ) { if (checked) { Row { Text("Checked") content() } } else { Column { Text("Unchecked") content() } } } checked = false checked = true checked = false PreferenceItem content
  56. Slot lifecycle @Composable fun PreferenceItem( checked: Boolean, content: @Composable ()

    -> Unit, ) { if (checked) { Row { Text("Checked") movableContent() } } else { Column { Text("Unchecked") movableContent() } } } val movableContent = remember(content) { movableContentOf(content) }
  57. Slot lifecycle @Composable fun PreferenceItem( checked: Boolean, content: @Composable ()

    -> Unit, ) { val movableContent = remember(content) { movableContentOf(content) } if (checked) { Row { Text("Checked") movableContent() } } else { Column { Text("Unchecked") movableContent() } } } jb.gg/movable-content-of
  58. Ordering parameters fun NiaTopicTag( modifier: Modifier = Modifier, followed: Boolean,

    onClick: () -> Unit, enabled: Boolean = true, text: @Composable () -> Unit, ) › Required parameters › Optional parameters › First one: Modifier › Trailing content lambda @Composable
  59. Ordering parameters › Required parameters › Optional parameters › First

    one: Modifier › Trailing content lambda fun NiaTopicTag( modifier: Modifier = Modifier, followed: Boolean, onClick: () -> Unit, enabled: Boolean = true, text: @Composable () -> Unit, ) @Composable
  60. Ordering parameters NiaTopicTag(true, { toggleFollowed(id) }) { Text("Compose") } ›

    Required parameters › Optional parameters › First one: Modifier › Trailing content lambda fun NiaTopicTag( modifier: Modifier = Modifier, followed: Boolean, onClick: () -> Unit, enabled: Boolean = true, text: @Composable () -> Unit, ) @Composable
  61. Ordering parameters NiaTopicTag(true, { toggleFollowed(id) } Text("Compose") } › Required

    parameters › Optional parameters › First one: Modifier › Trailing content lambda fun NiaTopicTag( modifier: Modifier = Modifier, followed: Boolean, onClick: () -> Unit, enabled: Boolean = true, text: @Composable () -> Unit, ) ) { @Composable
  62. Ordering parameters NiaTopicTag(true, { toggleFollowed(id) } Text("Compose") } › Required

    parameters › Optional parameters › First one: Modifier › Trailing content lambda fun NiaTopicTag( followed: Boolean, onClick: () -> Unit, modifier: Modifier = Modifier, enabled: Boolean = true, text: @Composable () -> Unit, ) ) { @Composable
  63. Ordering parameters NiaTopicTag(followed = true, onClick = { toggleFollowed(id) }

    Text("Compose") } › Required parameters › Optional parameters › First one: Modifier › Trailing content lambda fun NiaTopicTag( followed: Boolean, onClick: () -> Unit, text: @Composable () -> Unit, ) modifier: Modifier = Modifier, enabled: Boolean = true, ) { @Composable
  64. Ordering parameters NiaTopicTag(true, Text("Compose") } › Required parameters › Optional

    parameters › First one: Modifier › Trailing content lambda fun NiaTopicTag( followed: Boolean, onClick: () -> Unit, text: @Composable () -> Unit, ) modifier: Modifier = Modifier, enabled: Boolean = true, ) { { toggleFollowed(id) } @Composable
  65. Ordering parameters NiaTopicTag(true, , Modifier.padding(8.dp) Text("Compose") } fun NiaTopicTag( followed:

    Boolean, onClick: () -> Unit, modifier: Modifier = Modifier, enabled: Boolean = true, text: @Composable () -> Unit, ) › Required parameters › Optional parameters › First one: Modifier › Trailing content lambda { toggleFollowed(id) } ) { @Composable
  66. Ordering parameters NiaTopicTag(true, { toggleFollowed(id) }, Modifier.padding(8.dp)) { Text("Compose") }

    fun NiaTopicTag( followed: Boolean, onClick: () -> Unit, modifier: Modifier = Modifier, enabled: Boolean = true, text: @Composable () -> Unit, ) › Required parameters › Optional parameters › First one: Modifier › Trailing content lambda @Composable
  67. Ordering parameters NiaTopicTag(true, { toggleFollowed(id) }, Modifier.padding(8.dp)) { Text("Compose") }

    fun NiaTopicTag( followed: Boolean, onClick: () -> Unit, modifier: Modifier = Modifier, enabled: Boolean = true, content: @Composable () -> Unit, ) › Required parameters › Optional parameters › First one: Modifier › Trailing content lambda @Composable
  68. @Composable fun Scroller( offset: State<Float>, onOffsetChange: (Float) -> Unit, )

    @Composable fun ContextMenu(offset: Float, onOffsetChange: (Float) -> Unit) { Row { MenuItems() Scroller(offset, onOffsetChange) } } State
  69. @Composable fun Scroller( offset: State<Float>, onOffsetChange: (Float) -> Unit, )

    @Composable fun ContextMenu(offset: Float, onOffsetChange: (Float) -> Unit) { val offsetState = remember { mutableStateOf(offset) } Row { MenuItems() Scroller(offsetState, onOffsetChange) } } State
  70. @Composable fun Scroller( offset: () -> Float, onOffsetChange: (Float) ->

    Unit, ) // Constant Scroller(offset = { 0f }, ...) // Plain value Scroller(offset = { offset }, ...) // State Scroller(offset = { offsetState.value }, ...) // Value from an object Scroller(offset = { someObject.offset }, ...) State
  71. @Composable fun Scroller( offset: () -> Float, onOffsetChange: (Float) ->

    Unit, ) // Constant Scroller(offset = { 0f }, ...) // Plain value Scroller(offset = { offset }, ...) // State Scroller(offset = { offsetState.value }, ...) // Value from an object Scroller(offset = { someObject.offset }, ...) State jb.gg/debugging-recomposition
  72. State holders @Composable fun VerticalScroller( , , onScrollPositionChange: (Int) ->

    Unit, onScrollRangeChange: (Int) -> Unit, ) scrollPosition: Int scrollRange: Int
  73. State holders @Stable interface { } jb.gg/compose-stability @Composable fun VerticalScroller(

    verticalScrollerState: VerticalScrollerState ) VerticalScrollerState scrollPosition: Int scrollRange: Int var var
  74. State holders class Impl( scrollPosition: Int = 0, scrollRange: Int

    = 0, ) : VerticalScrollerState { override by mutableStateOf(scrollPosition) override by mutableStateOf(scrollRange) } @Composable fun VerticalScroller( verticalScrollerState: VerticalScrollerState ) VerticalScrollerState scrollPosition: Int scrollRange: Int var var
  75. State holders @Composable fun VerticalScroller( verticalScrollerState: VerticalScrollerState ) fun VerticalScrollerState():

    VerticalScrollerState = VerticalScrollerStateImpl() private class Impl( scrollPosition: Int = 0, scrollRange: Int = 0, ) : VerticalScrollerState { override var scrollPosition: Int by mutableStateOf(scrollPosition) override var scrollRange: Int by mutableStateOf(scrollRange) } VerticalScrollerState
  76. State holders @Composable fun VerticalScroller( verticalScrollerState: VerticalScrollerState = remember {

    VerticalScrollerState() } ) fun VerticalScrollerState(): VerticalScrollerState = VerticalScrollerStateImpl() private class Impl( scrollPosition: Int = 0, scrollRange: Int = 0, ) : VerticalScrollerState { override var scrollPosition: Int by mutableStateOf(scrollPosition) override var scrollRange: Int by mutableStateOf(scrollRange) } VerticalScrollerState
  77. State holders @Composable fun VerticalScroller( verticalScrollerState: VerticalScrollerState = remember {

    VerticalScrollerState() } ) fun VerticalScrollerState(): VerticalScrollerState = VerticalScrollerStateImpl() @Stable interface VerticalScrollerState { var scrollPosition: Int var scrollRange: Int } private class Impl( scrollPosition: Int = 0, scrollRange: Int = 0, ) : VerticalScrollerState { override var scrollPosition: Int by mutableStateOf(scrollPosition) override var scrollRange: Int by mutableStateOf(scrollRange) } VerticalScrollerState
  78. State holders @Stable class VerticalScrollerState { var scrollPosition: Int by

    mutableStateOf(0) var scrollRange: Int by mutableStateOf(0) }
  79. State holders @Stable class VerticalScrollerState { var scrollPosition: Int by

    mutableStateOf(0) var scrollRange: Int by mutableStateOf(0) }
  80. Compose Multiplatform – jb.gg/compose Guidelines – jb.gg/compose-api-guidelines – jb.gg/compose-component-api- guidelines

    In order of appearance – jb.gg/composition-locals – jb.gg/compose-modifiers – jb.gg/movable-content-of – jb.gg/debugging-recomposition – jb.gg/compose-stability – jb.gg/io24-compose-apis – jb.gg/compose-rules Composing an API the way zsmb.co/talks Márton Braun @[email protected]