Slide 1

Slide 1 text

Márton Braun @zsmb13 Developer Relations Engineer Google Composing an API with Kotlin

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

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

Slide 4

Slide 4 text

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

Slide 5

Slide 5 text

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

Slide 6

Slide 6 text

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?

Slide 7

Slide 7 text

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?

Slide 8

Slide 8 text

What is Jetpack Compose?

Slide 9

Slide 9 text

● What is Jetpack Compose? Declarative

Slide 10

Slide 10 text

● ● What is Jetpack Compose? Declarative Built with Kotlin

Slide 11

Slide 11 text

● ● ● What is Jetpack Compose? Declarative Built with Kotlin Open source

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

Modifier. Extension functions

Slide 19

Slide 19 text

@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

Slide 20

Slide 20 text

@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

Slide 21

Slide 21 text

@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

Slide 22

Slide 22 text

@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 )

Slide 23

Slide 23 text

Modifier. Extension functions

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

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)

Slide 29

Slide 29 text

Naming conventions Modifier.clip(shape: Shape)

Slide 30

Slide 30 text

Modifier.clip(CircleShape) Naming conventions

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

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

Slide 39

Slide 39 text

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

Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

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

Slide 42

Slide 42 text

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

Slide 43

Slide 43 text

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

Slide 44

Slide 44 text

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

Slide 45

Slide 45 text

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

Slide 46

Slide 46 text

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

Slide 47

Slide 47 text

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

Slide 48

Slide 48 text

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

Slide 49

Slide 49 text

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

Slide 50

Slide 50 text

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

Slide 51

Slide 51 text

val sizeInPx = 16.dp.toPx() Scopes

Slide 52

Slide 52 text

Scopes val sizeInPx = 16.dp.toPx()

Slide 53

Slide 53 text

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

Slide 54

Slide 54 text

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,

Slide 55

Slide 55 text

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 { }

Slide 56

Slide 56 text

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

Slide 57

Slide 57 text

Inline classes

Slide 58

Slide 58 text

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)

Slide 59

Slide 59 text

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

Slide 60

Slide 60 text

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

Slide 61

Slide 61 text

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

Slide 62

Slide 62 text

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

Slide 63

Slide 63 text

Coroutines

Slide 64

Slide 64 text

Coroutines chris.banes.dev/suspending-views/

Slide 65

Slide 65 text

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

Slide 66

Slide 66 text

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

Slide 67

Slide 67 text

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

Slide 68

Slide 68 text

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

Slide 69

Slide 69 text

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

Slide 70

Slide 70 text

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

Slide 71

Slide 71 text

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

Slide 72

Slide 72 text

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

Slide 73

Slide 73 text

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

Slide 74

Slide 74 text

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

Slide 75

Slide 75 text

Coroutines

Slide 76

Slide 76 text

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

Slide 77

Slide 77 text

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