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

Composing an API with Kotlin (Kotlin Budapest M...

Composing an API with Kotlin (Kotlin Budapest Meetup 2022 October)

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/talks/composing-an-api-with-kotlin/

Márton Braun

October 18, 2022
Tweet

More Decks by Márton Braun

Other Decks in Programming

Transcript

  1. 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?
  2. 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?
  3. // ... 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") } }
  4. 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 // ...
  5. 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
  6. Box( modifier = Modifier .size(216.dp) .background( brush = Brush.radialGradient( colors

    = listOf(Color.Black, Color.White) ) ) .padding(bottom = 12.dp) ) Extension functions
  7. 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 = ... )
  8. Box( modifier = .size(216.dp) .background( brush = Brush.radialGradient( colors =

    listOf(Color.Black, Color.White) ) ) .padding(bottom = 12.dp) ) Modifier. Extension functions
  9. @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
  10. @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
  11. @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
  12. @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 )
  13. 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
  14. 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
  15. 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
  16. 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)
  17. Modifier.clip(CircleShape) val CircleShape = RoundedCornerShape(50) class RoundedCornerShape( topStart: CornerSize, topEnd:

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

    fun RoundedCornerShape(size: Float) = ... fun RoundedCornerShape(size: Dp) = ... Naming conventions
  19. 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
  20. 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
  21. 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
  22. 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
  23. 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
  24. 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
  25. 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() } ) }
  26. 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
  27. Column(modifier = modifier.fillMaxSize()) { Button(onClick = { /* ... */

    }) { JetNewsLogo( Modifier .padding(16.dp) .align(Alignment.CenterHorizontally) ) } } Scopes this: ColumnScope
  28. 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,
  29. 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 { }
  30. 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
  31. 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)
  32. 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
  33. 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
  34. 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
  35. 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
  36. val listState = rememberLazyListState() val coroutineScope = rememberCoroutineScope() Box {

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

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

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

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

    result = snackbarHostState.showSnackbar( message = "Scrolled to the top!", actionLabel = "Revert", ) if (result == SnackbarResult.ActionPerformed) { } } } ) Coroutines
  41. 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
  42. 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
  43. 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