Slide 1

Slide 1 text

Let’s build an Android UI with Jetpack Compose @ Alex Zhukovich

Slide 2

Slide 2 text

No content

Slide 3

Slide 3 text

View framework

Slide 4

Slide 4 text

No content

Slide 5

Slide 5 text

setFirstBaselineToTopHeight API level 28 setTextLocale API level 17 onRtlPropertiesChanged API level 17 setPaddingRelative API level 16

Slide 6

Slide 6 text

View 29.000+ LOC TextView 13.000+ LOC Button 180 LOC CompoundButton 650 LOC Switch 1500+LOC CheckBox 76 LOC

Slide 7

Slide 7 text

public class CheckBox extends CompoundButton { public CheckBox(Context context) { this(context, null); } public CheckBox(Context context, AttributeSet attrs) { this(context, attrs, com.android.internal.R.attr.checkboxStyle); } public CheckBox(Context context, AttributeSet attrs, int defStyleAttr) { this(context, attrs, defStyleAttr, 0); } public CheckBox(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); } @Override public CharSequence getAccessibilityClassName() { return CheckBox.class.getName(); } }

Slide 8

Slide 8 text

Jetpack Compose

Slide 9

Slide 9 text

Unbundled from Android Interoperability with View framework Android Studio 4.1+

Slide 10

Slide 10 text

UI Runtime Plugin

Slide 11

Slide 11 text

@Composable fun CalendarItem( month: String, date: String, day: String ) { Surface( modifier = Modifier.preferredSize(80.dp), shape = CircleShape, border = Border(0.5.dp, Color.Gray) ) { Box( gravity = Alignment.TopCenter, modifier = Modifier.padding(4.dp) ) { Column { Text(text = month) Text( text = date, style = TextStyle( fontSize = 24.sp, fontWeight = FontWeight.Bold ) ) Text(text = day) } } } }

Slide 12

Slide 12 text

@Preview @Composable fun previewDate() { CalendarItem("APR", "20", "Mon") } @Preview @Composable fun previewAndroid() { Row(modifier = Modifier.padding(8.dp)) { CalendarItem("APR", "20", "Mon") Spacer(modifier = Modifier.preferredWidth(16.dp)) CalendarItem("APR", "21", "Tue") } }

Slide 13

Slide 13 text

Calendar { repeat(4) { CalendarWeek { repeat(7) { CalendarItem( month = "APR", date = "20", day = "Mon" ) } } } }

Slide 14

Slide 14 text

Calendar { repeat(4) { CalendarWeek { repeat(7) { CalendarItem( month = "APR", date = "20", day = "Mon" ) } } } }

Slide 15

Slide 15 text

Calendar { repeat(4) { CalendarWeek { repeat(7) { CalendarItem( month = "APR", date = "20", day = "Mon" ) } } } }

Slide 16

Slide 16 text

class MainActivity : AppCompatActivity() { override fun onCreate( savedInstanceState: Bundle? ) { super.onCreate(savedInstanceState) setContent { AppContent() } } ... @Composable fun AppContent() { ... } } class MainFragment : Fragment() { override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { val view = inflater.inflate( R.layout.fragment_main, container, false ) (view as ViewGroup).setContent { AppContent() } return fragmentView } @Composable fun AppContent() { ... } }

Slide 17

Slide 17 text

ModelList State @Composable data class Counter( var count: MutableState = mutableStateOf(0) ) @Composable fun Counter(counter: Counter) { Row { Button( onClick = { counter.count.value++ } ) { Text(text = "+") } Text(text = counter.count.value.toString()) Button( onClick = { counter.count.value-- } ) { Text(text = "-") } } }

Slide 18

Slide 18 text

@Composable ModelList State data class Counter( var count: MutableState = mutableStateOf(0) ) @Composable fun Counter(counter: Counter) { Row { Button( onClick = { counter.count.value++ } ) { Text(text = "+") } Text(text = counter.count.value.toString()) Button( onClick = { counter.count.value-- } ) { Text(text = "-") } } }

Slide 19

Slide 19 text

var counters = ModelList() counters.add(Counter(0)) @Composable fun Counter(counters: MutableState>) { Row { Button(onClick = { val newList = (counter.value).also { it.add(Counter(0)) } counter.value = newList }) { Text(text = "+") } Text(text = counter.value.size.toString()) … } } } @Composable ModelList State

Slide 20

Slide 20 text

Layouts

Slide 21

Slide 21 text

Row Column ConstraintLayout Stack

Slide 22

Slide 22 text

Row { Image( painter = ImagePainter( imageResource(R.drawable.latte_small) ), modifier = Modifier.preferredSize(160.dp) ) Text( text = "Latte", style = TextStyle( fontSize = 72.sp, color = Color.White ), modifier = Modifier.padding(end = 30.dp) ) }

Slide 23

Slide 23 text

Column { Image( painter = ImagePainter( imageResource(R.drawable.latte_small) ), modifier = Modifier.preferredSize(160.dp) ) Text( text = "Latte", style = TextStyle( fontSize = 72.sp, color = Color.White ), modifier = Modifier.padding(end = 30.dp) ) }

Slide 24

Slide 24 text

Stack( modifier = Modifier.preferredSize(400.dp, 160.dp) ) { Image( painter = ImagePainter( imageResource(R.drawable.latte_small) ), modifier = Modifier.preferredSize(160.dp) + Modifier.gravity(Alignment.CenterStart) ) Text( text = "Latte", style = TextStyle( fontSize = 72.sp, color = Color.White ), modifier = Modifier.gravity(Alignment.BottomEnd) ) Image( painter = ImagePainter( imageResource(R.drawable.ic_favorite_border) ), modifier = Modifier.padding(20.dp) + Modifier.gravity(Alignment.TopEnd) ) }

Slide 25

Slide 25 text

ConstraintLayout( modifier = Modifier.preferredSize(400.dp, 160.dp), constraintSet = ConstraintSet { val logo = tag("logo") val favourite = tag("favourite") val title = tag("title") favourite.apply { right constrainTo parent.right } title.apply { left constrainTo logo.right centerVertically() } } ) { Image( painter = ImagePainter(imageResource(...)), modifier = Modifier.tag("logo") ) Text( text = "Latte", style = TextStyle(...), modifier = Modifier.tag("title") ) Image( painter = ImagePainter(...), modifier = Modifier.padding(20.dp) + Modifier.tag("favourite") ) }

Slide 26

Slide 26 text

modifier = Modifier.fillMaxWidth() + Modifier.padding( start = 16.dp, top = 8.dp, end = 16.dp ) + Modifier.drawOpacity(0.54f) + Modifier.tag(DRINK_INGREDIENTS_TAG) modifier = Modifier.fillMaxWidth() .padding( start = 16.dp, top = 8.dp, end = 16.dp ) .drawOpacity(0.54f) .tag(DRINK_INGREDIENTS_TAG)

Slide 27

Slide 27 text

https://github.com/AlexZhukovich/CoffeeDrinksWithJetpackCompose

Slide 28

Slide 28 text

No content

Slide 29

Slide 29 text

@Composable fun CoffeeDrinkListItem( drink: CoffeeDrinkItem ) { ... } data class CoffeeDrinkItem( val id: Long, val name: String, val imageUrl: Int, val ingredients: String, var isFavourite: MutableState )

Slide 30

Slide 30 text

@Composable fun CoffeeDrinkListItem( drink: CoffeeDrinkItem ) { Row { Box( modifier = Modifier.preferredSize(72.dp) ) Box( modifier = Modifier.weight(1f) + Modifier.preferredHeight(72.dp) ) Box( modifier = Modifier.preferredSize( width = 40.dp, height = 72.dp ) ) } }

Slide 31

Slide 31 text

@Composable fun CoffeeDrinkListItem( drink: CoffeeDrinkItem ) { Row { Surface( modifier = Modifier.preferredSize(72.dp) + Modifier.padding(16.dp), shape = CircleShape, color = Color(0xFFFAFAFA) ) { Image( painter = ImagePainter( imageResource(drink.imageUrl) ), modifier = Modifier.fillMaxSize() ) } ... } }

Slide 32

Slide 32 text

@Composable fun CoffeeDrinkListItem( drink: CoffeeDrinkItem ) { Row { ... Box( modifier = Modifier.weight(1f) ) { Column { Text( text = drink.title, modifier = Modifier.padding( top = 8.dp, end = 8.dp ), style = TextStyle(fontSize = 24.sp), maxLines = 1 ) Text( text = drink.ingredients, modifier = Modifier.drawOpacity(0.54f), maxLines = 1, overflow = TextOverflow.Ellipsis ) } } } }

Slide 33

Slide 33 text

@Composable fun CoffeeDrinkListItem( drink: CoffeeDrinkItem ) { Row { ... Toggleable( value = coffeeDrink.isFavourite.value, onValueChange = { drink.value.isFavourite.value = !drink.isFavourite.value }, modifier = Modifier.ripple(radius = 24.dp) ) { Box( modifier = Modifier.preferredSize(48.dp) ) { val iconId = if (drink.isFavourite.value) { R.drawable.ic_favourite } else { R.drawable.ic_non_favourite } Image(...) } } } }

Slide 34

Slide 34 text

@Composable fun CoffeeDrinkListItem( drink: CoffeeDrinkItem ) { Row { Surface( modifier = Modifier.preferredSize(72.dp) + Modifier.padding(16.dp), shape = CircleShape, color = Color(0xFFFAFAFA) ) { Image( painter = ImagePainter(imageResource(drink.imageUrl)), modifier = Modifier.fillMaxSize() ) } Box( modifier = Modifier.weight(1f) ) { Column { Text( text = drink.title, modifier = Modifier.padding(top = 8.dp, end = 8.dp), style = TextStyle(fontSize = 24.sp), maxLines = 1 ) Text( text = drinkingredients, modifier = Modifier.drawOpacity(0.54f), maxLines = 1, overflow = TextOverflow.Ellipsis, ) } } Toggleable( value = drink.isFavourite.value, onValueChange = { drink.isFavourite.value = !drink.isFavourite.value }, modifier = Modifier.ripple(radius = 24.dp) ) { Box( modifier = Modifier.preferredSize(48.dp) ) { val iconId = ... Image(...) } } } }

Slide 35

Slide 35 text

@Composable fun CoffeeDrinkListItem( drink:CoffeeDrinkItem ) { Row { Logo(drink.imageUrl) AdditionalInformation { Title(drink.title) Ingredients(drink.ingredients) } Favourite(drink) } }

Slide 36

Slide 36 text

AdapterList( data = coffeeDrinks ) { coffeeDrink -> Box( modifier = Modifier.ripple(bounded = true) + Modifier.clickable(onClick = {}) ) { CoffeeDrinkListItem(coffeeDrink) } }

Slide 37

Slide 37 text

@Composable private fun showDrinks() { AdapterList( data = coffeeDrinks ) { coffeeDrink -> Box( modifier = Modifier.ripple(bounded = true) + Modifier.clickable(onClick = {}) ) { Column { CoffeeDrinkListItem(coffeeDrink) CoffeeDrinkDivider() } } } } @Composable private fun CoffeeDrinkDivider() { Divider( modifier = Modifier.padding(start = 72.dp) + Modifier.drawOpacity(0.12f), color = Color.Black ) }

Slide 38

Slide 38 text

@Composable fun CoffeeDrinkAppBar() { TopAppBar( title = { Text( text = "Coffee Drinks", style = TextStyle(color = Color.White, ...) ) }, backgroundColor = Color(0xFF562A1F), actions = { IconButton( onClick = { } ) { Icon( painter = ImagePainter( imageResource( R.drawable.ic_extended_list_white ) ), tint = Color.White ) } } ) }

Slide 39

Slide 39 text

@Composable fun CoffeeDrinkAppBar(cardType: CardType) { TopAppBar( title = { ... }, actions = { IconButton( onClick = { ... } ) { Icon( painter = ImagePainter( imageResource(id = if (cardType.isDetailedItem.value) R.drawable.ic_list_white else R.drawable.ic_extended_list_white ) ), tint = Color.White ) } } ) } data class CardType( var isDetailedItem: MutableState = mutableStateOf(false) )

Slide 40

Slide 40 text

private const val HEADER_TAG = "header" private const val LOGO_TAG = "logo" @Composable fun CoffeeDrinkDetailsScreen( coffeeDrinks: MutableState ) { ConstraintLayout( constraintSet = ConstraintSet { val header = tag(HEADER_TAG) val logo= tag(LOGO_TAG) ... logo.apply { top constrainTo parent.top bottom constrainTo header.bottom left constrainTo parent.left right constrainTo parent.right } } ) { ... Image( painter = ImagePainter( imageResource(id = R.drawable.americano_small) ), modifier = Modifier.tag(LOGO_TAG) ) ... } }

Slide 41

Slide 41 text

private const val HEADER_TAG = "header" private const val LOGO_TAG = "logo" @Composable fun CoffeeDrinkDetailsScreen( coffeeDrinks: MutableState ) { ConstraintLayout( constraintSet = ConstraintSet { val header = tag(HEADER_TAG) val logo = tag(LOGO_TAG) ... logo.apply { top constrainTo parent.top bottom constrainTo header.bottom left constrainTo parent.left right constrainTo parent.right } } ) { ... Image( painter = ImagePainter( imageResource(id = R.drawable.americano_small) ), modifier = Modifier.tag(LOGO_TAG) ) ... } }

Slide 42

Slide 42 text

private const val SURFACE_TAG = "surface" private const val HEADER_TAG = "header" @Composable fun CoffeeDrinkDetailsScreen( coffeeDrinks: MutableState ) { ConstraintLayout( constraintSet = ConstraintSet { val header = tag(HEADER_TAG) val logo = tag(LOGO_TAG) ... logo.apply { top constrainTo parent.top bottom constrainTo header.bottom left constrainTo parent.left right constrainTo parent.right } } ) { ... Image( painter = ImagePainter( imageResource(id = R.drawable.americano_small) ), modifier = Modifier.tag(LOGO_TAG) ) ... } }

Slide 43

Slide 43 text

data class Substring( val value: String, val startPosition: Int ) { val endPosition = startPosition + value.length } val nameSubstring = getSubstring(coffeeDrink.name, coffeeDrink.description) val substrings = getSubstrings(dictionary, coffeeDrink.description) val description = AnnotatedString { append(coffeeDrink.description) addStyle( style = SpanStyle( color = Color(0xFF562a1f), fontWeight = FontWeight.Bold, fontSize = 18.sp ), start = nameSubstring.startPosition, end = nameSubstring.endPosition ) substrings.forEach { addStyle( style = SpanStyle( fontStyle = FontStyle.Italic, textDecoration = TextDecoration.Underline ), start = it.startPosition, end = it.endPosition ) } }

Slide 44

Slide 44 text

data class Substring( val value: String, val startPosition: Int ) { val endPosition = startPosition + value.length } val nameSubstring = getSubstring(coffeeDrink.name, coffeeDrink.description) val substrings = getSubstrings(dictionary, coffeeDrink.description) val description = AnnotatedString { append(coffeeDrink.description) addStyle( style = SpanStyle( color = Color(0xFF562a1f), fontWeight = FontWeight.Bold, fontSize = 18.sp ), start = nameSubstring.startPosition, end = nameSubstring.endPosition ) substrings.forEach { addStyle( style = SpanStyle( fontStyle = FontStyle.Italic, textDecoration = TextDecoration.Underline ), start = it.startPosition, end = it.endPosition ) } }

Slide 45

Slide 45 text

data class Substring( val value: String, val startPosition: Int ) { val endPosition = startPosition + value.length } val nameSubstring = getSubstring(coffeeDrink.name, coffeeDrink.description) val substrings = getSubstrings(dictionary, coffeeDrink.description) val description = AnnotatedString { append(coffeeDrink.description) addStyle( style = SpanStyle( color = Color(0xFF562a1f), fontWeight = FontWeight.Bold, fontSize = 18.sp ), start = nameSubstring.startPosition, end = nameSubstring.endPosition ) substrings.forEach { addStyle( style = SpanStyle( fontStyle = FontStyle.Italic, textDecoration = TextDecoration.Underline ), start = it.startPosition, end = it.endPosition ) } }

Slide 46

Slide 46 text

Routing

Slide 47

Slide 47 text

Activity/Fragment State Clickable(onClick = { startActivity( Intent( this@MainActivity, TestActivity::class.java ) ) }

Slide 48

Slide 48 text

Activity/Fragment State sealed class RouterDestination { object CoffeeDrinks : RouterDestination() data class CoffeeDrinkDetails( val coffeeDrinkId: Long ) : RouterDestination() object OrderCoffeeDrinks : RouterDestination() } data class RouteState( var currentState: MutableState ) class Router(var state: RouteState) { fun navigateTo(destination: RouterDestination) { state.currentState.value = destination } } @Composable fun AppContent() { val routeState = state { RouterDestination.CoffeeDrinks } val router = Router(RouteState(routeState)) MaterialTheme(...) { Crossfade(router.state.currentState.value) { screen -> when (screen) { is StateRouterDestination.CoffeeDrinks -> CoffeeDrinksScreen(router, ...) ... } } } }

Slide 49

Slide 49 text

Activity/Fragment State sealed class RouterDestination { object CoffeeDrinks : RouterDestination() data class CoffeeDrinkDetails( val coffeeDrinkId: Long ) : RouterDestination() object OrderCoffeeDrinks : RouterDestination() } data class RouteState( var currentState: MutableState ) class Router(var state: RouteState) { fun navigateTo(destination: RouterDestination) { state.currentState.value = destination } } @Composable fun AppContent() { val routeState = state { RouterDestination.CoffeeDrinks } val router = Router(RouteState(routeState)) MaterialTheme(...) { Crossfade(router.state.currentState.value) { screen -> when (screen) { is StateRouterDestination.CoffeeDrinks -> CoffeeDrinksScreen(router, ...) ... } } } }

Slide 50

Slide 50 text

Activity/Fragment State sealed class RouterDestination { object CoffeeDrinks : RouterDestination() data class CoffeeDrinkDetails( val coffeeDrinkId: Long ) : RouterDestination() object OrderCoffeeDrinks : RouterDestination() } data class RouteState( var currentState: MutableState ) class Router(var state: RouteState) { fun navigateTo(destination: RouterDestination) { state.currentState.value = destination } } @Composable fun AppContent() { val routeState = state { RouterDestination.CoffeeDrinks } val router = Router(RouteState(routeState)) MaterialTheme(...) { Crossfade(router.state.currentState.value) { screen -> when (screen) { is StateRouterDestination.CoffeeDrinks -> CoffeeDrinksScreen(router, ...) ... } } } }

Slide 51

Slide 51 text

Theme

Slide 52

Slide 52 text

No content

Slide 53

Slide 53 text

val lightThemeColors = lightColorPalette( primary = Color(0xFF663e34), primaryVariant = Color(0xFF562a1f), secondary = Color(0xFF855446), secondaryVariant = Color(0xFFb68171), background = Color.White, surface = Color.White, error = Color(0xFFB00020), onPrimary = Color.White, onSecondary = Color.White, onBackground = Color.Black, onSurface = Color.Black, onError = Color.White ) https://material.io/design/color/applying-color-to-ui.html

Slide 54

Slide 54 text

@Composable fun AppContent() { val colorPalette = if (isSystemInDarkTheme()) { darkThemeColors } else { lightThemeColors } MaterialTheme( colors = colorPalette ) { ... } } @Preview @Composable fun previewAppContent() { MaterialTheme(colors = lightThemeColors) { ... } } Text( style = MaterialTheme.typography.h4.copy( color = MaterialTheme.colors.onSurface ), ... )

Slide 55

Slide 55 text

Typography

Slide 56

Slide 56 text

private val appFontFamily = fontFamily( listOf( ResourceFont( resId = R.font.roboto_black, weight = FontWeight.W900, style = FontStyle.Normal ), ResourceFont( resId = R.font.roboto_medium_italic, weight = FontWeight.W500, style = FontStyle.Italic ), ResourceFont( resId = R.font.roboto_thin, weight = FontWeight.W100, style = FontStyle.Normal ), ... ) ) private val defaultTypography = Typography() val appTypography = Typography( h1 = defaultTypography.h1.copy( fontFamily = appFontFamily ), ... body1 = defaultTypography.body1.copy( fontFamily = appFontFamily ), body2 = defaultTypography.body2.copy( fontFamily = appFontFamily ), button = defaultTypography.button.copy( fontFamily = appFontFamily ), ... )

Slide 57

Slide 57 text

@Composable fun AppContent() { MaterialTheme( colors = lightThemeColors, typography = appTypography ) { ... Text( ... style = MaterialTheme.typography.h5 ) Text( ... style = MaterialTheme.typography.h5.copy( color = MaterialTheme.colors.onSurface ) ) } }

Slide 58

Slide 58 text

Testing

Slide 59

Slide 59 text

fun ComposeTestRule.launchCoffeeDrinksScreen( router: Router, repository: CoffeeDrinkRepository, mapper: CoffeeDrinkItemMapper ) { setContent { CoffeeDrinksScreen( router, repository, mapper ) } } @RunWith(JUnit4::class) class CoffeeDrinksScreenTest { @get:Rule val composeTestRule = createComposeRule() private val repository = ... private val mapper = ... @Before fun setUp() { composeTestRule.launchCoffeeDrinksScreen( repository, mapper ) } @Test fun shouldLaunchApp() { findByText("Coffee Drinks") .assertIsDisplayed() } @Test fun shouldLoadAmericano() { findBySubstring("Americano") .assertIsDisplayed() } }

Slide 60

Slide 60 text

fun ComposeTestRule.launchCoffeeDrinksScreen( router: Router, repository: CoffeeDrinkRepository, mapper: CoffeeDrinkItemMapper ) { setContent { CoffeeDrinksScreen( router, repository, mapper ) } } @RunWith(JUnit4::class) class CoffeeDrinksScreenTest { @get:Rule val composeTestRule = createComposeRule() private val repository = ... private val mapper = ... @Before fun setUp() { composeTestRule.launchCoffeeDrinksScreen( router, repository, mapper ) } @Test fun shouldLaunchApp() { findByText("Coffee Drinks") .assertIsDisplayed() } @Test fun shouldLoadAmericano() { findBySubstring("Americano") .assertIsDisplayed() } }

Slide 61

Slide 61 text

Notes

Slide 62

Slide 62 text

Interoperability Right To Left Resources Text( stringResource(R.string.name) ) Icon( painter = ImagePainter( imageResource( R.drawable.ic_arrow_back ) ) )

Slide 63

Slide 63 text

Resources Right To Left Interoperability

Slide 64

Slide 64 text

Resources Right To Left Interoperability @Composable @GenerateView fun CustomButton( title: String, icon: Int, ) { ... } https://www.youtube.com/watch?v=VsStyq4Lzxo

Slide 65

Slide 65 text

Web Map Animations

Slide 66

Slide 66 text

Web Map Animations Developer Preview

Slide 67

Slide 67 text

#ExploreMore Jetpack Compose alexzh.com @AlexZhukovich