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/

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

    View full-size slide

  2. Jetpack Compose
    is Android’s modern
    toolkit for building
    native UI.
    d.android.com/compose

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  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?

    View full-size slide

  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?

    View full-size slide

  8. What is Jetpack Compose?

    View full-size slide


  9. What is Jetpack Compose?
    Declarative

    View full-size slide



  10. What is Jetpack Compose?
    Declarative
    Built with Kotlin

    View full-size slide




  11. What is Jetpack Compose?
    Declarative
    Built with Kotlin
    Open source

    View full-size slide

  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")
    }
    }

    View full-size slide

  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
    // ...

    View full-size slide

  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

    View full-size slide

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

    View full-size slide

  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 = ...
    )

    View full-size slide

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

    View full-size slide

  18. Modifier.
    Extension functions

    View full-size slide

  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

    View full-size slide

  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

    View full-size slide

  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

    View full-size slide

  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
    )

    View full-size slide

  23. Modifier.
    Extension functions

    View full-size slide

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

    View full-size slide

  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

    View full-size slide

  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

    View full-size slide

  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

    View full-size slide

  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)

    View full-size slide

  29. Naming conventions
    Modifier.clip(shape: Shape)

    View full-size slide

  30. Modifier.clip(CircleShape)
    Naming conventions

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  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

    View full-size slide

  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

    View full-size slide

  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

    View full-size slide

  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

    View full-size slide

  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

    View full-size slide

  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

    View full-size slide

  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() }
    )
    }

    View full-size slide

  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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  49. Column(modifier = modifier.fillMaxSize()) {
    JetNewsLogo(
    Modifier
    .padding(16.dp)
    .align(Alignment.CenterHorizontally)
    )
    }
    Scopes
    this: ColumnScope

    View full-size slide

  50. Column(modifier = modifier.fillMaxSize()) {
    Button(onClick = { /* ... */ }) {
    JetNewsLogo(
    Modifier
    .padding(16.dp)
    .align(Alignment.CenterHorizontally)
    )
    }
    }
    Scopes
    this: ColumnScope

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  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,

    View full-size slide

  55. object CustomGridCells : GridCells {
    override fun Density.calculateCrossAxisCellSizes(
    availableSize: Int,
    spacing: Int
    ): List {
    val sideCellSize = 60.dp.roundToPx()
    return listOf(
    sideCellSize,
    availableSize - 2 * sideCellSize,
    sideCellSize,
    )
    }
    }
    Scopes
    fun Dp.toPx(): Float = value * density
    interface Density {
    }

    View full-size slide

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

    View full-size slide

  57. Inline classes

    View full-size slide

  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)

    View full-size slide

  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

    View full-size slide

  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

    View full-size slide

  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

    View full-size slide

  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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  68. ScrollToTopButton(
    onClick = {
    coroutineScope.launch {
    listState.animateScrollToItem(index = 0)
    }
    }
    )
    Coroutines

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  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
    val firstItemIndex = listState.firstVisibleItemIndex
    val firstItemOffset = listState.firstVisibleItemScrollOffset

    View full-size slide

  73. 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

    View full-size slide

  74. 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

    View full-size slide

  75. Thank You!
    Márton Braun
    @zsmb13
    Composing an API with Kotlin

    View full-size slide