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

Let’s build an Android UI with Jetpack Compose

Let’s build an Android UI with Jetpack Compose

The Android UI uses a similar approach for the past decade. Recently, developers use DSL, functional programming more often than a few years ago. The Android UI Toolkit team proposed a new way of creating UI for Android applications.

This presentation covers the following topics:
- Getting Started with Jetpack Compose
- Overview of the layouts available in Jetpack Compose
- Build a set of screens with Jetpack Compose
- Introduction to Theme and Typography in Jetpack Compose
- Introduction to Testing with Jetpack Compose testing library

Alex Zhukovich

April 20, 2020
Tweet

More Decks by Alex Zhukovich

Other Decks in Technology

Transcript

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

    View Slide

  2. View Slide

  3. View framework

    View Slide

  4. View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  8. Jetpack Compose

    View Slide

  9. Unbundled
    from Android
    Interoperability with
    View framework
    Android Studio
    4.1+

    View Slide

  10. UI
    Runtime Plugin

    View Slide

  11. @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)
    }
    }
    }
    }

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  16. 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() { ... }
    }

    View Slide

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

    View Slide

  18. @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 = "-")
    }
    }
    }

    View Slide

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

    View Slide

  20. Layouts

    View Slide

  21. Row Column ConstraintLayout
    Stack

    View Slide

  22. 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)
    )
    }

    View Slide

  23. 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)
    )
    }

    View Slide

  24. 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)
    )
    }

    View Slide

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

    View Slide

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

    View Slide

  27. https://github.com/AlexZhukovich/CoffeeDrinksWithJetpackCompose

    View Slide

  28. View Slide

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

    View Slide

  30. @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
    )
    )
    }
    }

    View Slide

  31. @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()
    )
    }
    ...
    }
    }

    View Slide

  32. @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
    )
    }
    }
    }
    }

    View Slide

  33. @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(...)
    }
    }
    }
    }

    View Slide

  34. @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(...)
    }
    }
    }
    }

    View Slide

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

    View Slide

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

    View Slide

  37. @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
    )
    }

    View Slide

  38. @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
    )
    }
    }
    )
    }

    View Slide

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

    View Slide

  40. 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)
    )
    ...
    }
    }

    View Slide

  41. 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)
    )
    ...
    }
    }

    View Slide

  42. 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)
    )
    ...
    }
    }

    View Slide

  43. 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
    )
    }
    }

    View Slide

  44. 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
    )
    }
    }

    View Slide

  45. 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
    )
    }
    }

    View Slide

  46. Routing

    View Slide

  47. Activity/Fragment
    State
    Clickable(onClick = {
    startActivity(
    Intent(
    [email protected],
    TestActivity::class.java
    )
    )
    }

    View Slide

  48. 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, ...)
    ...
    }
    }
    }
    }

    View Slide

  49. 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, ...)
    ...
    }
    }
    }
    }

    View Slide

  50. 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, ...)
    ...
    }
    }
    }
    }

    View Slide

  51. Theme

    View Slide

  52. View Slide

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

    View Slide

  54. @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
    ),
    ...
    )

    View Slide

  55. Typography

    View Slide

  56. 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
    ),
    ...
    )

    View Slide

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

    View Slide

  58. Testing

    View Slide

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

    View Slide

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

    View Slide

  61. Notes

    View Slide

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

    View Slide

  63. Resources
    Right To Left
    Interoperability

    View Slide

  64. Resources
    Right To Left
    Interoperability
    @Composable
    @GenerateView
    fun CustomButton(
    title: String,
    icon: Int,
    ) {
    ...
    }
    android:id="@+id/button"
    ...
    app:title="@string/app_name"
    app:icon="@drawable/button_icon"
    />
    https://www.youtube.com/watch?v=VsStyq4Lzxo

    View Slide

  65. Web
    Map
    Animations

    View Slide

  66. Web
    Map
    Animations
    Developer Preview

    View Slide

  67. #ExploreMore
    Jetpack Compose
    alexzh.com @AlexZhukovich

    View Slide