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

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

Composing an API the *right* way (Droidcon Italy 2023)

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.

Avatar for Márton Braun

Márton Braun

October 13, 2023
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. Modifiers @Composable fun MenuItems( smallScreen: Boolean, modifier: Modifier = Modifier,

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

    First optional parameter › Applied to the root node › Default value Modifier
  42. 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" } }
  43. Modifiers @Composable fun Topics( topic: List<Topic>, modifier: Modifier = Modifier,

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

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

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

    content() } } Button { } this: RowScope () -> Unit Row { ) ) Icon(Icons.Default.Build, "Build" Text("Build project"
  47. 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"
  48. 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"
  49. Slot lifecycle @Composable fun PreferenceItem( checked: Boolean, content: @Composable ()

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

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

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

    -> Unit, ) { if (checked) { Row { Text("Checked") content() } } else { Column { Text("Unchecked") content() } } } PreferenceItem content
  53. 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
  54. 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) }
  55. 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
  56. 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
  57. 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, )
  58. 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, )
  59. 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, ) ) {
  60. 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, ) ) {
  61. 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, ) {
  62. 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) }
  63. 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) } ) {
  64. 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
  65. 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
  66. @Composable fun Scroller( offset: State<Float>, onOffsetChange: (Float) -> Unit, )

    @Composable fun ContextMenu(offset: Float, onOffsetChange: (Float) -> Unit) { Row { MenuItems() Scroller(offset, onOffsetChange) } } State
  67. @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
  68. @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
  69. @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
  70. State holders @Composable fun VerticalScroller( , , onScrollPositionChange: (Int) ->

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

    verticalScrollerState: VerticalScrollerState ) VerticalScrollerState scrollPosition: Int scrollRange: Int var var
  72. 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
  73. 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
  74. 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
  75. 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
  76. State holders @Stable class VerticalScrollerState { var scrollPosition: Int by

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

    mutableStateOf(0) var scrollRange: Int by mutableStateOf(0) }
  78. 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 Composing an API the way zsmb.co/talks Márton Braun @[email protected]