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

Composing an API with Kotlin (Kotlin Dev Day 2022)

Composing an API with Kotlin (Kotlin Dev Day 2022)

Kotlin offers many powerful language features to API authors. We’ll explore how Jetpack Compose’s syntax builds on these features, and how its APIs were shaped by Kotlin idioms and conventions. These ideas and best practices give you a deeper understanding of the language, and they are valuable for any Kotlin developer designing APIs – which is actually every Kotlin developer!

More details and resources: https://zsmb.co/appearances/kotlin-dev-day-2022/

4047c64e3a1e2f81addd4ba675ddc451?s=128

Marton Braun

May 19, 2022
Tweet

More Decks by Marton Braun

Other Decks in Programming

Transcript

  1. Márton Braun @zsmb13 Developer Relations Engineer Google Composing an API

    with Kotlin
  2. Jetpack Compose is Android’s modern toolkit for building native UI.

    d.android.com/compose
  3. What is Jetpack Compose? @Composable fun Greeting(name: String) { Text(text

    = "Hello $name!") }
  4. What is Jetpack Compose? @Composable fun Greeting(name: String) { Text(text

    = "Hello $name!") } Hello Android!
  5. What is Jetpack Compose? @Composable fun Greeting(name: String) { Text(text

    = "Hello $name!") } Hello Compose!
  6. class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) {

    super.onCreate(savedInstanceState) setContent { DevDayDemoTheme { Surface( modifier = Modifier.fillMaxSize(), color = MaterialTheme.colors.background, ) { Greeting(name = "Android") } } } } } What is Jetpack Compose?
  7. class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) {

    super.onCreate(savedInstanceState) setContent { DevDayDemoTheme { Surface( modifier = Modifier.fillMaxSize(), color = MaterialTheme.colors.background, ) { Greeting(name = "Android") } } } } } What is Jetpack Compose?
  8. What is Jetpack Compose?

  9. • What is Jetpack Compose? Declarative

  10. • • What is Jetpack Compose? Declarative Built with Kotlin

  11. • • • What is Jetpack Compose? Declarative Built with

    Kotlin Open source
  12. // ... Extension functions class MainActivity : ComponentActivity() { override

    fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { } } } DevDayDemoTheme { Surface( modifier = Modifier.fillMaxSize(), color = MaterialTheme.colors.background, ) { Greeting(name = "Android") } }
  13. DevDayDemoTheme { Surface( modifier = Modifier.fillMaxSize(), color = MaterialTheme.colors.background, )

    { Greeting(name = "Android") } } class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { } } } public fun ComponentActivity.setContent( parent: CompositionContext? = null, content: @Composable () -> Unit ) Extension functions // ...
  14. public fun CoroutineScope.launch( context: CoroutineContext = EmptyCoroutineContext, start: CoroutineStart =

    CoroutineStart.DEFAULT, block: suspend CoroutineScope.() -> Unit ) public fun ComponentActivity.setContent( parent: CompositionContext? = null, content: @Composable () -> Unit ) Extension functions
  15. Box( modifier = Modifier .size(216.dp) .background( brush = Brush.radialGradient( colors

    = listOf(Color.Black, Color.White) ) ) .padding(bottom = 12.dp) ) Extension functions
  16. Box( modifier = Modifier .size(216.dp) .background( brush = Brush.radialGradient( colors

    = listOf(Color.Black, Color.White) ) ) .padding(bottom = 12.dp) ) Extension functions @Composable fun Greeting( name: String, modifier: Modifier = ... )
  17. Box( modifier = .size(216.dp) .background( brush = Brush.radialGradient( colors =

    listOf(Color.Black, Color.White) ) ) .padding(bottom = 12.dp) ) Modifier. Extension functions
  18. Modifier. Extension functions

  19. @Stable interface Modifier { infix fun then(other: Modifier): Modifier =

    if (other === Modifier) this else CombinedModifier(this, other) companion object : Modifier { override infix fun then(other: Modifier): Modifier = other } } Extension functions
  20. @Stable interface Modifier { infix fun then(other: Modifier): Modifier =

    if (other === Modifier) this else CombinedModifier(this, other) companion object : Modifier { override infix fun then(other: Modifier): Modifier = other } } Extension functions
  21. @Stable interface Modifier { infix fun then(other: Modifier): Modifier =

    if (other === Modifier) this else CombinedModifier(this, other) companion object : Modifier { override infix fun then(other: Modifier): Modifier = other } } Modifier. Extension functions
  22. @Stable interface Modifier { infix fun then(other: Modifier): Modifier =

    if (other === Modifier) this else CombinedModifier(this, other) companion object : Modifier { override infix fun then(other: Modifier): Modifier = other } } Extension functions @Composable fun Greeting( name: String, modifier: Modifier = Modifier )
  23. Modifier. Extension functions

  24. package androidx.compose.foundation fun Modifier.background(color: Color, shape: Shape = RectangleShape) Extension

    functions
  25. package androidx.compose.foundation fun Modifier.background(color: Color, shape: Shape = RectangleShape) package

    androidx.compose.foundation.gestures fun Modifier.scrollable(state: ScrollableState, orientation: Orientation) fun Modifier.draggable(state: DraggableState, orientation: Orientation) Extension functions
  26. package androidx.compose.foundation fun Modifier.background(color: Color, shape: Shape = RectangleShape) package

    androidx.compose.foundation.gestures fun Modifier.scrollable(state: ScrollableState, orientation: Orientation) fun Modifier.draggable(state: DraggableState, orientation: Orientation) package androidx.compose.foundation.layout fun Modifier.padding(all: Dp) fun Modifier.size(width: Dp, height: Dp) Extension functions
  27. package androidx.compose.foundation fun Modifier.background(color: Color, shape: Shape = RectangleShape) package

    androidx.compose.foundation.gestures fun Modifier.scrollable(state: ScrollableState, orientation: Orientation) fun Modifier.draggable(state: DraggableState, orientation: Orientation) package androidx.compose.foundation.layout fun Modifier.padding(all: Dp) fun Modifier.size(width: Dp, height: Dp) package androidx.compose.ui.draw fun Modifier.clip(shape: Shape) fun Modifier.rotate(degrees: Float) fun Modifier.scale(scaleX: Float, scaleY: Float) Extension functions
  28. package androidx.compose.foundation fun Modifier.background(color: Color, shape: Shape = RectangleShape) package

    androidx.compose.foundation.gestures fun Modifier.scrollable(state: ScrollableState, orientation: Orientation) fun Modifier.draggable(state: DraggableState, orientation: Orientation) package androidx.compose.foundation.layout fun Modifier.padding(all: Dp) fun Modifier.size(width: Dp, height: Dp) package androidx.compose.ui.draw fun fun Modifier.rotate(degrees: Float) fun Modifier.scale(scaleX: Float, scaleY: Float) Extension functions Modifier.clip(shape: Shape)
  29. Naming conventions Modifier.clip(shape: Shape)

  30. Modifier.clip(CircleShape) Naming conventions

  31. Modifier.clip(CircleShape) val CircleShape = RoundedCornerShape(50) Naming conventions

  32. Modifier.clip(CircleShape) val circleShape = RoundedCornerShape(50) Naming conventions

  33. Modifier.clip(CircleShape) val CIRCLE_SHAPE = RoundedCornerShape(50) Naming conventions

  34. Modifier.clip(CircleShape) val CircleShape = RoundedCornerShape(50) Naming conventions

  35. Modifier.clip(CircleShape) val CircleShape = RoundedCornerShape(50) class RoundedCornerShape( topStart: CornerSize, topEnd:

    CornerSize, bottomEnd: CornerSize, bottomStart: CornerSize ) Naming conventions
  36. Modifier.clip(CircleShape) val CircleShape = RoundedCornerShape(50) fun RoundedCornerShape(percent: Int) = ...

    Naming conventions
  37. Modifier.clip(CircleShape) val CircleShape = RoundedCornerShape(50) fun RoundedCornerShape(percent: Int) = ...

    Naming conventions
  38. Modifier.clip(CircleShape) val CircleShape = RoundedCornerShape(50) fun RoundedCornerShape(percent: Int) = ...

    fun RoundedCornerShape(size: Float) = ... fun RoundedCornerShape(size: Dp) = ... Naming conventions
  39. fun RoundedCornerShape( topStart: Dp = 0.dp, topEnd: Dp = 0.dp,

    bottomEnd: Dp = 0.dp, bottomStart: Dp = 0.dp ) = RoundedCornerShape( topStart = CornerSize(topStart), topEnd = CornerSize(topEnd), bottomEnd = CornerSize(bottomEnd), bottomStart = CornerSize(bottomStart) ) Naming conventions
  40. fun RoundedCornerShape( topStart: Dp = 0.dp, topEnd: Dp = 0.dp,

    bottomEnd: Dp = 0.dp, bottomStart: Dp = 0.dp ) = RoundedCornerShape( topStart = CornerSize(topStart), topEnd = CornerSize(topEnd), bottomEnd = CornerSize(bottomEnd), bottomStart = CornerSize(bottomStart) ) Hello world Naming conventions
  41. fun RoundedCornerShape( topStart: Dp = 0.dp, topEnd: Dp = 0.dp,

    bottomEnd: Dp = 0.dp, bottomStart: Dp = 0.dp ) = RoundedCornerShape( topStart = CornerSize(topStart), topEnd = CornerSize(topEnd), bottomEnd = CornerSize(bottomEnd), bottomStart = CornerSize(bottomStart) ) RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp, bottomEnd = 16.dp) Hello world Naming conventions
  42. fun RoundedCornerShape( topStart: Dp = 0.dp, topEnd: Dp = 0.dp,

    bottomEnd: Dp = 0.dp, bottomStart: Dp = 0.dp ) = RoundedCornerShape( topStart = CornerSize(topStart), topEnd = CornerSize(topEnd), bottomEnd = CornerSize(bottomEnd), bottomStart = CornerSize(bottomStart) ) RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp, bottomEnd = 16.dp) Hello world RoundedCornerShape(16.dp).copy(bottomStart = CornerSize(0.dp)) Naming conventions
  43. Column(modifier = modifier.fillMaxSize()) { JetNewsLogo(Modifier .padding(16.dp) .align(Alignment.CenterHorizontally) ) Divider() DrawerButton(

    icon = Icons.Filled.Home, label = stringResource(id = R.string.home_title), isSelected = currentRoute == JetnewsDestinations.HOME_ROUTE, action = { navigateToHome(); closeDrawer() } ) DrawerButton( icon = Icons.Filled.ListAlt, label = stringResource(id = R.string.interests_title), isSelected = currentRoute == JetnewsDestinations.INTERESTS_ROUTE, action = { navigateToInterests(); closeDrawer() } ) } Naming conventions
  44. Column(modifier = modifier.fillMaxSize()) { JetNewsLogo(Modifier .padding(16.dp) .align(Alignment.CenterHorizontally) ) Divider() DrawerButton(

    icon = Icons.Filled.Home, label = stringResource(id = R.string.home_title), isSelected = currentRoute == JetnewsDestinations.HOME_ROUTE, action = { navigateToHome(); closeDrawer() } ) DrawerButton( icon = Icons.Filled.ListAlt, label = stringResource(id = R.string.interests_title), isSelected = currentRoute == JetnewsDestinations.INTERESTS_ROUTE, action = { navigateToInterests(); closeDrawer() } ) } Naming conventions
  45. Scopes Column(modifier = modifier.fillMaxSize()) { JetNewsLogo(Modifier .padding(16.dp) .align(Alignment.CenterHorizontally) ) Divider()

    DrawerButton( icon = Icons.Filled.Home, label = stringResource(id = R.string.home_title), isSelected = currentRoute == JetnewsDestinations.HOME_ROUTE, action = { navigateToHome(); closeDrawer() } ) DrawerButton( icon = Icons.Filled.ListAlt, label = stringResource(id = R.string.interests_title), isSelected = currentRoute == JetnewsDestinations.INTERESTS_ROUTE, action = { navigateToInterests(); closeDrawer() } ) }
  46. Column(modifier = modifier.fillMaxSize()) { JetNewsLogo(Modifier .padding(16.dp) .align(Alignment.CenterHorizontally) ) Divider() DrawerButton(

    icon = Icons.Filled.Home, label = stringResource(id = R.string.home_title), isSelected = currentRoute == JetnewsDestinations.HOME_ROUTE, action = { navigateToHome(); closeDrawer() } ) DrawerButton( icon = Icons.Filled.ListAlt, label = stringResource(id = R.string.interests_title), isSelected = currentRoute == JetnewsDestinations.INTERESTS_ROUTE, action = { navigateToInterests(); closeDrawer() } ) } this: ColumnScope Scopes
  47. @LayoutScopeMarker @Immutable interface ColumnScope { @Stable fun Modifier.align(alignment: Alignment.Horizontal): Modifier

    } Scopes
  48. @LayoutScopeMarker @Immutable interface ColumnScope { @Stable fun Modifier.align(alignment: Alignment.Horizontal): Modifier

    } @DslMarker annotation class LayoutScopeMarker Scopes
  49. Column(modifier = modifier.fillMaxSize()) { JetNewsLogo( Modifier .padding(16.dp) .align(Alignment.CenterHorizontally) ) }

    Scopes this: ColumnScope
  50. Column(modifier = modifier.fillMaxSize()) { Button(onClick = { /* ... */

    }) { JetNewsLogo( Modifier .padding(16.dp) .align(Alignment.CenterHorizontally) ) } } Scopes this: ColumnScope
  51. val sizeInPx = 16.dp.toPx() Scopes

  52. Scopes val sizeInPx = 16.dp.toPx()

  53. val sizeInPx = with(LocalDensity.current) { } this: Density Scopes 16.dp.toPx()

  54. val sizeInPx = with(LocalDensity.current) { 16.dp.toPx() } this: Density @Immutable

    @Stable val density: Float @Stable Scopes interface Density { } fun Dp.toPx(): Float = value * density object CustomGridCells : GridCells { override fun Density.calculateCrossAxisCellSizes( availableSize: Int,
  55. object CustomGridCells : GridCells { override fun Density.calculateCrossAxisCellSizes( availableSize: Int,

    spacing: Int ): List<Int> { val sideCellSize = 60.dp.roundToPx() return listOf( sideCellSize, availableSize - 2 * sideCellSize, sideCellSize, ) } } Scopes fun Dp.toPx(): Float = value * density interface Density { }
  56. Not a real Compose API context(Density) object CustomGridCells : GridCells

    { context(Density) override fun calculateCrossAxisCellSizes( availableSize: Int, spacing: Int ): List<Int> { val sideCellSize = 60.dp.roundToPx() return listOf( sideCellSize, availableSize - 2 * sideCellSize, sideCellSize, ) } } Scopes – with context receivers? fun Dp.toPx(): Float = value * density
  57. Inline classes

  58. Inline classes @JvmInline value class Dp(val value: Float) { inline

    operator fun plus(other: Dp) inline operator fun minus(other: Dp) inline operator fun div(other: Float): Dp inline operator fun div(other: Int): Dp inline operator fun div(other: Dp): Float } inline val Int.dp: Dp get() = Dp(value = this.toFloat()) inline operator fun Int.times(other: Dp) = Dp(this * other.value)
  59. val availableWidth: Dp = 200.dp val itemWidth: Dp = 30.dp

    val itemCount: Int = (availableWidth / itemWidth).toInt() val remaining: Dp = (availableWidth - itemCount * itemWidth) val separatorWidth: Dp = remaining / (itemCount - 1) Inline classes
  60. float availableWidth = Dp.constructor-impl((float) 200); float itemWidth = Dp.constructor-impl((float) 30);

    int itemCount = (int)(availableWidth / itemWidth); float other$iv = Dp.constructor-impl((float) itemCount * itemWidth); float remaining = Dp.constructor-impl(availableWidth - other$iv); float separatorWidth = Dp.constructor-impl(remaining / (float) (itemCount - 1)); val availableWidth: Dp = 200.dp val itemWidth: Dp = 30.dp val itemCount: Int = (availableWidth / itemWidth).toInt() val remaining: Dp = (availableWidth - itemCount * itemWidth) val separatorWidth: Dp = remaining / (itemCount - 1) Inline classes
  61. float availableWidth = Dp.constructor-impl((float) 200); float itemWidth = Dp.constructor-impl((float) 30);

    int itemCount = (int)(availableWidth / itemWidth); float other$iv = Dp.constructor-impl((float) itemCount * itemWidth); float remaining = Dp.constructor-impl(availableWidth - other$iv); float separatorWidth = Dp.constructor-impl(remaining / (float) (itemCount - 1)); val availableWidth: Dp = 200.dp val itemWidth: Dp = 30.dp val itemCount: Int = (availableWidth / itemWidth).toInt() val remaining: Dp = (availableWidth - itemCount * itemWidth) val separatorWidth: Dp = remaining / (itemCount - 1) Inline classes
  62. float availableWidth = Dp.constructor-impl((float) 200); float itemWidth = Dp.constructor-impl((float) 30);

    int itemCount = (int)(availableWidth / itemWidth); float other$iv = Dp.constructor-impl((float) itemCount * itemWidth); float remaining = Dp.constructor-impl(availableWidth - other$iv); float separatorWidth = Dp.constructor-impl(remaining / (float) (itemCount - 1)); public final class Dp { private final float value; public static float constructor_impl(float value) { return value; } } Inline classes
  63. Coroutines

  64. Coroutines chris.banes.dev/suspending-views/

  65. val listState = rememberLazyListState() val coroutineScope = rememberCoroutineScope() Box {

    LazyColumn(state = listState) { // ... } AnimatedVisibility(/* ... */) { ScrollToTopButton( onClick = { coroutineScope.launch { listState.animateScrollToItem(index = 0) } } ) } } Coroutines
  66. val listState = rememberLazyListState() val coroutineScope = rememberCoroutineScope() Box {

    LazyColumn(state = listState) { // ... } AnimatedVisibility(/* ... */) { ScrollToTopButton( onClick = { coroutineScope.launch { listState.animateScrollToItem(index = 0) } } ) } } Coroutines
  67. val listState = rememberLazyListState() val coroutineScope = rememberCoroutineScope() Box {

    LazyColumn(state = listState) { // ... } AnimatedVisibility(/* ... */) { } } Coroutines ScrollToTopButton( onClick = { coroutineScope.launch { listState.animateScrollToItem(index = 0) } } )
  68. val listState = rememberLazyListState() val coroutineScope = rememberCoroutineScope() Box {

    LazyColumn(state = listState) { // ... } AnimatedVisibility(/* ... */) { } } Coroutines ScrollToTopButton( onClick = { coroutineScope.launch { listState.animateScrollToItem(index = 0) } } )
  69. ScrollToTopButton( onClick = { coroutineScope.launch { listState.animateScrollToItem(index = 0) }

    } ) Coroutines
  70. ScrollToTopButton( onClick = { coroutineScope.launch { listState.animateScrollToItem(index = 0) snackbarHostState.showSnackbar(

    message = "Scrolled to the top!", ) } } ) Coroutines
  71. ScrollToTopButton( onClick = { coroutineScope.launch { listState.animateScrollToItem(index = 0) snackbarHostState.showSnackbar(

    message = "Scrolled to the top!", actionLabel = "Revert", ) } } ) Coroutines
  72. ScrollToTopButton( onClick = { coroutineScope.launch { listState.animateScrollToItem(index = 0) val

    result = snackbarHostState.showSnackbar( message = "Scrolled to the top!", actionLabel = "Revert", ) if (result == SnackbarResult.ActionPerformed) { } } } ) Coroutines
  73. ScrollToTopButton( onClick = { coroutineScope.launch { listState.animateScrollToItem(index = 0) val

    result = snackbarHostState.showSnackbar( message = "Scrolled to the top!", actionLabel = "Revert", ) if (result == SnackbarResult.ActionPerformed) { } } } ) Coroutines val firstItemIndex = listState.firstVisibleItemIndex val firstItemOffset = listState.firstVisibleItemScrollOffset
  74. ScrollToTopButton( onClick = { coroutineScope.launch { listState.animateScrollToItem(index = 0) val

    result = snackbarHostState.showSnackbar( message = "Scrolled to the top!", actionLabel = "Revert", ) if (result == SnackbarResult.ActionPerformed) { listState.animateScrollToItem(firstItemIndex, firstItemOffset) } } } ) Coroutines val firstItemIndex = listState.firstVisibleItemIndex val firstItemOffset = listState.firstVisibleItemScrollOffset
  75. Coroutines

  76. Resources • Compose API guidelines • Kotlin for Jetpack Compose

    • Understanding Compose (ADS '19) • Extension oriented design • Suspending over Views by Chris Banes NEW! goo.gle/compose-api-guidelines goo.gle/kotlin-for-compose goo.gle/understanding-compose goo.gle/extension-oriented-design goo.gle/suspending-over-views
  77. Thank You! Márton Braun @zsmb13 Composing an API with Kotlin