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

Discovering Jetpack Compose

Gerard
February 28, 2020

Discovering Jetpack Compose

Jetpack Compose will be the new declarative UI to build Android apps. Based on this new paradigm, let's see how we can use it with some simple samples!

Gerard

February 28, 2020
Tweet

More Decks by Gerard

Other Decks in Technology

Transcript

  1. @GerardPaligot
    DISCOVERING
    JETPACK COMPOSE

    View full-size slide

  2. WHAT’S JETPACK COMPOSE?
    100% Kotlin
    Compiler Open Source
    Declarative UI toolkit
    Android Team

    View full-size slide

  3. THINK DIFFERENT.

    View full-size slide

  4. MovieMetadata
    Rating
    TopAppBar
    Poster
    PosterNoted
    MovieItem

    View full-size slide

  5. App
    MovieHome
    Home
    Section
    Home
    Section
    Home
    Section
    Data
    Events

    View full-size slide

  6. App
    MovieHome
    Home
    Section
    Home
    Section
    Home
    Section
    Data
    Events

    View full-size slide

  7. PLACING ELEMENTS

    View full-size slide

  8. @Composable
    fun MovieMetadata(
    title: String,
    genres: List,
    releaseDate: String,
    runtime: Int
    ) {
    }

    View full-size slide

  9. @Composable
    fun MovieMetadata(
    title: String,
    genres: List,
    releaseDate: String,
    runtime: Int
    ) {
    Text(
    text = title
    )
    }

    View full-size slide

  10. @Composable
    fun MovieMetadata(
    title: String,
    genres: List,
    releaseDate: String,
    runtime: Int
    ) {
    Text(
    text = title,
    style = MaterialTheme.typography.h6
    )
    }

    View full-size slide

  11. @Composable
    fun MovieMetadata(
    title: String,
    genres: List,
    releaseDate: String,
    runtime: Int
    ) {
    Text(
    text = title,
    style = MaterialTheme.typography.h6,
    maxLines = 2,
    overflow = TextOverflow.Ellipsis
    )
    }

    View full-size slide

  12. @Composable
    fun MovieMetadata(
    title: String,
    genres: List,
    releaseDate: String,
    runtime: Int
    ) {
    Column {
    Text(
    text = title,
    style = MaterialTheme.typography.h6,
    maxLines = 2,
    overflow = TextOverflow.Ellipsis
    )
    }
    }

    View full-size slide

  13. @Composable
    fun MovieMetadata(
    title: String,
    genres: List,
    releaseDate: String,
    runtime: Int
    ) {
    Column {
    Text(...)
    }
    }

    View full-size slide

  14. @Composable
    fun MovieMetadata(
    title: String,
    genres: List,
    releaseDate: String,
    runtime: Int
    ) {
    Column {
    Text(...)
    genres.forEach {
    Tag(text = it)
    }
    }
    }

    View full-size slide

  15. @Composable
    fun MovieMetadata(
    title: String,
    genres: List,
    releaseDate: String,
    runtime: Int
    ) {
    Column {
    Text(...)
    Row {
    genres.forEach {
    Tag(text = it)
    }
    }
    }
    }

    View full-size slide

  16. @Composable
    fun MovieMetadata(
    title: String,
    genres: List,
    releaseDate: String,
    runtime: Int
    ) {
    Column {
    Text(...)
    Row(modifier = Modifier.padding(top = 5.dp)) {
    genres.forEach {
    Tag(text = it)
    }
    }
    }
    }

    View full-size slide

  17. @Composable
    fun MovieMetadata(
    title: String,
    genres: List,
    releaseDate: String,
    runtime: Int
    ) {
    Column {
    Text(...)
    Row(modifier = Modifier.padding(top = 5.dp)) {
    ...
    }
    }
    }

    View full-size slide

  18. @Composable
    fun MovieMetadata(
    title: String,
    genres: List,
    releaseDate: String,
    runtime: Int
    ) {
    Column {
    Text(...)
    Row(modifier = Modifier.padding(top = 5.dp)) {
    ...
    }
    val year = releaseDate.getCalendar().get(Calendar.YEAR)
    val time = "${runtime / 60}h ${runtime % 60}min"
    Text(
    text ="$year - $time",
    modifier = Modifier.padding(top = 5.dp)
    )
    }
    }

    View full-size slide

  19. @Composable
    fun MovieMetadata(
    title: String,
    genres: List,
    releaseDate: String,
    runtime: Int
    ) {
    Column {
    Text(...)
    Row(modifier = Modifier.padding(top = 5.dp)) {
    ...
    }
    val year = releaseDate.getCalendar().get(Calendar.YEAR)
    val time = "${runtime / 60}h ${runtime % 60}min"
    Text(
    text = if (runtime != 0) "$year - $time" else "$year",
    modifier = Modifier.padding(top = 5.dp)
    )
    }
    }

    View full-size slide

  20. @Composable
    fun MovieMetadata(
    title: String,
    genres: List,
    releaseDate: String,
    runtime: Int
    ) {
    Column {
    Text(...)
    Row(modifier = Modifier.padding(top = 5.dp)) {
    ...
    }
    CompositionLocalProvider(
    LocalContentAlpha provides ContentAlpha.medium
    ) {
    val year = releaseDate.getCalendar().get(Calendar.YEAR)
    val time = "${runtime / 60}h ${runtime % 60}min"
    Text(
    text = if (runtime != 0) "$year - $time" else "$year",
    modifier = Modifier.padding(top = 5.dp)
    )
    }
    }
    }

    View full-size slide

  21. @Composable
    fun MovieMetadata(
    title: String,
    genres: List,
    releaseDate: String,
    runtime: Int
    ) {
    Column {
    Text(...)
    Row(modifier = Modifier.padding(top = 5.dp)) {
    ...
    }
    CompositionLocalProvider(
    LocalContentAlpha provides ContentAlpha.medium
    ) {
    ...
    }
    }
    }

    View full-size slide

  22. @Composable
    fun MovieMetadata(
    title: String,
    genres: List,
    releaseDate: String,
    runtime: Int,
    modifier: Modifier = Modifier
    ) {
    Column(modifier = modifier) {
    Text(...)
    Row(modifier = Modifier.padding(top = 5.dp)) {
    ...
    }
    CompositionLocalProvider(
    LocalContentAlpha provides ContentAlpha.medium
    ) {
    ...
    }
    }
    }

    View full-size slide

  23. LET’S CODE A LIST

    View full-size slide

  24. @Composable
    fun PosterNoted(
    posterUrl: String,
    voteAverage: Int,
    ratingSize: Dp,
    ratingAlignment: Alignment,
    onClick: () -> Unit = {}
    )

    View full-size slide

  25. @Composable
    fun MovieItem(
    movie: Movie,
    modifier: Modifier = Modifier,
    onClick: (movie: Movie) -> Unit
    ) {
    }

    View full-size slide

  26. @Composable
    fun MovieItem(
    movie: Movie,
    modifier: Modifier = Modifier,
    onClick: (movie: Movie) -> Unit
    ) {
    PosterNoted(
    posterUrl = movie.pictureUrl,
    voteAverage = movie.percentage,
    ratingSize = 50.dp,
    ratingAlignment = Alignment.TopEnd
    )
    }

    View full-size slide

  27. @Composable
    fun MovieItem(
    movie: Movie,
    modifier: Modifier = Modifier,
    onClick: (movie: Movie) -> Unit
    ) {
    Box(
    modifier = Modifier
    .width(130.dp)
    .aspectRatio(0.7f)
    ) {
    PosterNoted(
    posterUrl = movie.pictureUrl,
    voteAverage = movie.percentage,
    ratingSize = 50.dp,
    ratingAlignment = Alignment.TopEnd
    )
    }
    }

    View full-size slide

  28. @Composable
    fun MovieItem(
    movie: Movie,
    modifier: Modifier = Modifier,
    onClick: (movie: Movie) -> Unit
    ) {
    Box(
    modifier = Modifier
    .width(130.dp)
    .aspectRatio(0.7f)
    ) {

    }
    }

    View full-size slide

  29. @Composable
    fun MovieItem(
    movie: Movie,
    modifier: Modifier = Modifier,
    onClick: (movie: Movie) -> Unit
    ) {
    Box(modifier = modifier) {
    Box(
    modifier = Modifier
    .width(130.dp)
    .aspectRatio(0.7f)
    ) {
    ...
    }
    }
    }

    View full-size slide

  30. @Composable
    fun MovieItem(
    movie: Movie,
    modifier: Modifier = Modifier,
    onClick: (movie: Movie) -> Unit
    ) {
    val posterWidth = 130
    val height = (posterWidth / 0.7f).roundToInt()
    Box(modifier = modifier) {
    Surface(
    modifier = Modifier
    .fillMaxWidth()
    .preferredHeight((height - 30).dp)
    ) {
    }
    Box(
    modifier = Modifier
    .width(posterWidth.dp)
    .aspectRatio(0.7f)
    ) {
    ...
    }
    }
    }

    View full-size slide

  31. @Composable
    fun MovieItem(
    movie: Movie,
    modifier: Modifier = Modifier,
    onClick: (movie: Movie) -> Unit
    ) {
    val posterWidth = 130
    val height = (posterWidth / 0.7f).roundToInt()
    Box(modifier = modifier) {
    Surface(
    modifier = Modifier
    .padding(top = 60.dp)
    .fillMaxWidth()
    .preferredHeight((height - 30).dp)
    ) {
    }
    Box(
    modifier = Modifier
    .width(posterWidth.dp)
    .aspectRatio(0.7f)
    ) {
    ...
    }
    }
    }

    View full-size slide

  32. @Composable
    fun MovieItem(
    movie: Movie,
    modifier: Modifier = Modifier,
    onClick: (movie: Movie) -> Unit
    ) {
    val posterWidth = 130
    val height = (posterWidth / 0.7f).roundToInt()
    Box(modifier = modifier) {
    Surface(
    modifier = Modifier
    .padding(top = 60.dp)
    .fillMaxWidth()
    .preferredHeight((height - 30).dp)
    .clickable(onClick = { onClick(movie) })
    ) {
    }
    Box(
    modifier = Modifier
    .width(posterWidth.dp)
    .aspectRatio(0.7f)
    ) {
    ...
    }
    }
    }

    View full-size slide

  33. @Composable
    fun MovieItem(
    movie: Movie,
    modifier: Modifier = Modifier,
    onClick: (movie: Movie) -> Unit
    ) {
    val posterWidth = 130
    val height = (posterWidth / 0.7f).roundToInt()
    Box(modifier = modifier) {
    Surface(
    modifier = Modifier
    .padding(top = 60.dp)
    .fillMaxWidth()
    .preferredHeight((height - 30).dp)
    .clickable(onClick = { onClick(movie) }),
    shape = RoundedCornerShape(8.dp)
    ) {
    }
    Box(
    modifier = Modifier
    .width(posterWidth.dp)
    .aspectRatio(0.7f)
    ) {
    ...
    }
    }
    }

    View full-size slide

  34. @Composable
    fun MovieItem(
    movie: Movie,
    modifier: Modifier = Modifier,
    onClick: (movie: Movie) -> Unit
    ) {
    val posterWidth = 130
    val height = (posterWidth / 0.7f).roundToInt()
    Box(modifier = modifier) {
    Surface(
    modifier = Modifier
    .padding(top = 60.dp)
    .fillMaxWidth()
    .preferredHeight((height - 30).dp)
    .clickable(onClick = { onClick(movie) }),
    shape = RoundedCornerShape(8.dp),
    elevation = 5.dp
    ) {
    }
    Box(
    modifier = Modifier
    .width(posterWidth.dp)
    .aspectRatio(0.7f)
    ) {
    ...
    }
    }
    }

    View full-size slide

  35. val posterWidth = 130
    val height = (posterWidth / 0.7f).roundToInt()
    Box(modifier = modifier) {
    Surface(
    modifier = Modifier
    .padding(top = 60.dp)
    .fillMaxWidth()
    .preferredHeight((height - 30).dp)
    .clickable(onClick = { onClick(movie) }),
    shape = RoundedCornerShape(8.dp),
    elevation = 5.dp
    ) {
    }
    Box(
    modifier = Modifier
    .width(posterWidth.dp)
    .aspectRatio(0.7f)
    ) {
    ...
    }
    }
    }

    View full-size slide

  36. val posterWidth = 130
    val height = (posterWidth / 0.7f).roundToInt()
    Box(modifier = modifier) {
    Surface(
    modifier = Modifier
    .padding(top = 60.dp)
    .fillMaxWidth()
    .preferredHeight((height - 30).dp)
    .clickable(onClick = { onClick(movie) }),
    shape = RoundedCornerShape(8.dp),
    elevation = 5.dp
    ) {
    }
    Box(
    modifier = Modifier
    .padding(start = 30.dp)
    .width(posterWidth.dp)
    .aspectRatio(0.7f)
    ) {
    ...
    }
    }
    }

    View full-size slide

  37. val posterWidth = 130
    val height = (posterWidth / 0.7f).roundToInt()
    Box(modifier = modifier) {
    Surface(
    modifier = Modifier
    .padding(top = 60.dp)
    .fillMaxWidth()
    .preferredHeight((height - 30).dp)
    .clickable(onClick = { onClick(movie) }),
    shape = RoundedCornerShape(8.dp),
    elevation = 5.dp
    ) {
    MovieMetadata(
    title = movie.title, genres = movie.genres,
    releaseDate = movie.releaseDate, runtime = movie.runtime,
    modifier = Modifier.padding(start = (posterWidth + 15).dp)
    .align(alignment = Alignment.CenterStart)
    )
    }
    Box(
    modifier = Modifier
    .padding(start = 30.dp)
    .width(posterWidth.dp)
    .aspectRatio(0.7f)
    ) {
    ...
    }
    }

    View full-size slide

  38. @Composable
    fun MovieList(
    title: String,
    movies: List,
    onClick: (movie: Movie) -> Unit
    ) {
    }

    View full-size slide

  39. @Composable
    fun MovieList(
    title: String,
    movies: List,
    onClick: (movie: Movie) -> Unit
    ) {
    val state = rememberScrollState()
    Column(modifier = Modifier.verticalScroll(state = state)) {
    movies.forEach {
    MovieItem(
    movie = it,
    modifier = Modifier
    .padding(start = 10.dp, top = 10.dp, end = 10.dp),
    onClick = onClick
    )
    }
    }
    }

    View full-size slide

  40. @Composable
    fun MovieList(
    title: String,
    movies: List,
    onClick: (movie: Movie) -> Unit
    ) {
    LazyColumn {
    items(movies) { movie ->
    MovieItem(
    movie = movie,
    modifier = Modifier
    .padding(start = 10.dp, top = 10.dp, end = 10.dp),
    onClick = onClick
    )
    }
    }
    }

    View full-size slide

  41. @Composable
    fun MovieList(
    title: String,
    movies: List,
    onClick: (movie: Movie) -> Unit
    ) {
    MovieScaffold(title = title) {
    LazyColumn {
    items(movies) { movie ->
    MovieItem(
    movie = it,
    modifier = Modifier
    .padding(start = 10.dp, top = 10.dp, end = 10.dp),
    onClick = onClick
    )
    }
    }
    }
    }

    View full-size slide

  42. @Composable
    fun Scaffold(
    modifier: Modifier = Modifier,
    scaffoldState: ScaffoldState = rememberScaffoldState(),
    topBar: @Composable () -> Unit = emptyContent(),
    bottomBar: @Composable () -> Unit = emptyContent(),
    snackbarHost: @Composable (SnackbarHostState) -> Unit = { SnackbarHost(it) },
    floatingActionButton: @Composable () -> Unit = emptyContent(),
    floatingActionButtonPosition: FabPosition = FabPosition.End,
    isFloatingActionButtonDocked: Boolean = false,
    drawerContent: @Composable (ColumnScope.() -> Unit)? = null,
    drawerGesturesEnabled: Boolean = true,
    drawerShape: Shape = MaterialTheme.shapes.large,
    drawerElevation: Dp = DrawerConstants.DefaultElevation,
    drawerBackgroundColor: Color = MaterialTheme.colors.surface,
    drawerContentColor: Color = contentColorFor(drawerBackgroundColor),
    drawerScrimColor: Color = DrawerConstants.defaultScrimColor,
    backgroundColor: Color = MaterialTheme.colors.background,
    contentColor: Color = contentColorFor(backgroundColor),
    bodyContent: @Composable (PaddingValues) -> Unit
    )

    View full-size slide

  43. LET’S CODE A LIST
    WITH ACTUAL FRAMEWORK UI

    View full-size slide


  44. xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/list"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical" />

    View full-size slide


  45. xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent" android:layout_height="80dp">
    android:layout_width="match_parent" android:layout_height="match_parent">
    android:layout_width="50dp" android:layout_height="50dp" android:layout_marginStart="20dp"
    app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="parent" tools:srcCompat="@tools:sample/avatars" />
    android:layout_width="wrap_content" android:layout_height="wrap_content"
    android:orientation="vertical" app:layout_constraintGuide_begin="100dp" />

    View full-size slide

  46. android:layout_width="0dp" android:layout_height="wrap_content"
    android:layout_marginEnd="20dp" android:orientation="vertical"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintEnd_toStartOf="@+id/price"
    app:layout_constraintStart_toStartOf="@+id/guideline3"
    app:layout_constraintTop_toTopOf="parent">
    android:layout_width="wrap_content" android:layout_height="wrap_content"
    android:textAppearance="@style/TextAppearance.Decathlon.Subtitle2"
    android:textColor="@color/colorOnSurface" />
    android:layout_width="wrap_content" android:layout_height="wrap_content"
    android:textAppearance="@style/TextAppearance.Decathlon.Subtitle2"
    android:textColor="@color/grey" />

    View full-size slide

  47. android:id="@+id/price"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_marginEnd="20dp"
    android:textAppearance="@style/TextAppearance.Decathlon.Subtitle2"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintTop_toTopOf="parent"
    tools:text="50$" />


    View full-size slide

  48. data class Activity(
    @DrawableRes val picture: Int,
    val displayName: String,
    val type: String,
    val price: Int
    )
    class ActivityListAdapter(val items: MutableList) :
    RecyclerView.Adapter() {
    val onClick: LiveData
    get() = this._onClick
    private val _onClick: MutableLiveData by lazy {
    return@lazy MutableLiveData()
    }

    View full-size slide

  49. override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ActivityViewHolder =
    ActivityViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.item_activity, parent, false))
    override fun getItemCount(): Int = items.size
    override fun onBindViewHolder(holder: ActivityViewHolder, position: Int) {
    val activity = items[position]
    holder.bind(activity, View.OnClickListener {
    _onClick.value = activity
    })
    }
    fun update(items: List) {
    this.items.clear()
    this.items.addAll(items)
    notifyDataSetChanged()
    }

    View full-size slide

  50. class ActivityViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
    fun bind(activity: Activity, onClickListener: View.OnClickListener) {
    itemView.setOnClickListener(onClickListener)
    itemView.picture.setImageDrawable(ContextCompat.getDrawable(itemView.context,
    activity.picture))
    itemView.name.text = activity.displayName
    itemView.type.text = activity.type
    val color = if (activity.price > 0)
    ContextCompat.getColor(itemView.context, R.color.green)
    else
    ContextCompat.getColor(itemView.context, R.color.red)
    itemView.price.setTextColor(color)
    }
    }
    }

    View full-size slide

  51. val orange800 = Color(255, 150, 29)
    val orange900 = Color(255, 118, 26)
    val blue400 = Color(26, 163, 255)
    private val LightColorPalette = lightColors(
    primary = orange900,
    primaryVariant = orange800,
    secondary = blue400,
    background = Color.White,
    surface = Color.White,
    onPrimary = Color.White,
    onSecondary = Color.Black,
    onBackground = Color.Black,
    onSurface = Color.Black,
    )

    View full-size slide

  52. val orange400 = Color(255, 205, 56)
    val blue800 = Color(0, 98, 203)
    val blue900 = Color(0, 68, 171)
    val grayDark = Color(45, 47, 49)
    private val DarkColorPalette = darkColors(
    primary = blue900,
    primaryVariant = blue800,
    secondary = orange400,
    background = grayDark,
    surface = grayDark,
    onPrimary = Color.White,
    onSecondary = Color.Black,
    onBackground = Color.White,
    onSurface = Color.White
    )

    View full-size slide

  53. val typography = Typography(
    h5 = TextStyle(
    fontFamily = FontFamily.Default,
    fontWeight = FontWeight.W700,
    fontSize = 24.sp
    ),
    subtitle1 = TextStyle(
    fontFamily = FontFamily.Default,
    fontWeight = FontWeight.W700,
    fontSize = 16.sp
    ),
    body1 = TextStyle(
    fontFamily = FontFamily.Default,
    fontWeight = FontWeight.Normal,
    fontSize = 16.sp
    ),
    caption = TextStyle(
    fontFamily = FontFamily.Default,
    fontWeight = FontWeight.W400,
    fontSize = 10.sp
    )
    )

    View full-size slide

  54. val shapes = Shapes(
    small = RoundedCornerShape(2.dp),
    medium = RoundedCornerShape(4.dp),
    large = RoundedCornerShape(8.dp)
    )

    View full-size slide

  55. @Composable
    fun MaterialTheme(
    colors: Colors = MaterialTheme.colors,
    typography: Typography = MaterialTheme.typography,
    shapes: Shapes = MaterialTheme.shapes,
    content: @Composable () -> Unit
    )

    View full-size slide

  56. @Composable
    fun ExploringMoviesTheme(
    isDarkMode: Boolean = isSystemInDarkTheme(),
    content: @Composable() () -> Unit
    ) {
    }

    View full-size slide

  57. @Composable
    fun ExploringMoviesTheme(
    isDarkMode: Boolean = isSystemInDarkTheme(),
    content: @Composable() () -> Unit
    ) {
    MaterialTheme(
    content = content
    )
    }

    View full-size slide

  58. @Composable
    fun ExploringMoviesTheme(
    isDarkMode: Boolean = isSystemInDarkTheme(),
    content: @Composable() () -> Unit
    ) {
    MaterialTheme(
    colors =
    if (isDarkMode) DarkColorPalette else LightColorPalette,
    content = content
    )
    }

    View full-size slide

  59. @Composable
    fun ExploringMoviesTheme(
    isDarkMode: Boolean = isSystemInDarkTheme(),
    content: @Composable() () -> Unit
    ) {
    MaterialTheme(
    colors =
    if (isDarkMode) DarkColorPalette else LightColorPalette,
    typography = typography,
    content = content
    )
    }

    View full-size slide

  60. @Composable
    fun ExploringMoviesTheme(
    isDarkMode: Boolean = isSystemInDarkTheme(),
    content: @Composable() () -> Unit
    ) {
    MaterialTheme(
    colors =
    if (isDarkMode) DarkColorPalette else LightColorPalette,
    typography = typography,
    shapes = shapes,
    content = content
    )
    }

    View full-size slide

  61. @Composable
    fun MovieList(
    title: String,
    movies: List,
    onClick: (movie: Movie) -> Unit
    ) {
    MovieScaffold(
    title = title
    ){
    LazyColumnFor(items = movies) {
    MovieItem(
    movie = it,
    modifier = Modifier
    .padding(start = 10.dp, top = 10.dp, end = 10.dp),
    onClick = onClick
    )
    }
    }
    }

    View full-size slide

  62. @Composable
    fun MovieList(
    title: String,
    movies: List,
    isDarkModeActive: Boolean,
    switchDarkMode: () -> Unit = {},
    onClick: (movie: Movie) -> Unit
    ) {
    MovieScaffold(
    title = title,
    isDarkModeActive = isDarkModeActive,
    switchDarkMode = switchDarkMode
    ){
    LazyColumnFor(items = movies) {
    MovieItem(
    movie = it,
    modifier = Modifier
    .padding(start = 10.dp, top = 10.dp, end = 10.dp),
    onClick = onClick
    )
    }
    }
    }

    View full-size slide

  63. setContent {
    val isSystemDark = isSystemInDarkTheme()
    }

    View full-size slide

  64. setContent {
    val isSystemDark = isSystemInDarkTheme()
    val isDarkModeState = remember { mutableStateOf(isSystemDark) }
    }

    View full-size slide

  65. setContent {
    val isSystemDark = isSystemInDarkTheme()
    val isDarkModeState = remember { mutableStateOf(isSystemDark) }
    ExploringMoviesTheme(isDarkMode = isDarkModeState.value) {
    }
    }

    View full-size slide

  66. setContent {
    val isSystemDark = isSystemInDarkTheme()
    val isDarkModeState = remember { mutableStateOf(isSystemDark) }
    ExploringMoviesTheme(isDarkMode = isDarkModeState.value) {
    App(
    isDarkModeActive = isDarkModeState.value
    )
    }
    }

    View full-size slide

  67. setContent {
    val isSystemDark = isSystemInDarkTheme()
    val isDarkModeState = remember { mutableStateOf(isSystemDark) }
    ExploringMoviesTheme(isDarkMode = isDarkModeState.value) {
    App(
    isDarkModeActive = isDarkModeState.value,
    switchDarkMode = {
    isDarkModeState.value = !isDarkModeState.value
    }
    )
    }
    }

    View full-size slide

  68. OBSERVING DATA WITH
    VIEWMODEL

    View full-size slide

  69. class MovieViewModel : ViewModel() {
    fun getPopulars() = flow {
    ...
    }
    fun getDailyTrending() = flow {
    ...
    }
    fun getUpComing() = flow {
    ...
    }
    fun getMovieDetail(movieId: Int) = flow {
    ...
    }
    }

    View full-size slide

  70. ▸ LiveData.observeAsState()
    ▸ Flow.collectAsState()
    ▸ Observable.subscribeAsState()

    View full-size slide

  71. @Composable
    fun MyExample() {
    // Returns the same instance as long as the activity is alive,
    // just as if you grabbed the instance from an Activity or
    // Fragment
    val viewModel: MovieViewModel = viewModel()
    }
    @Composable
    fun MyExample2() {
    // Same instance as in MyExample
    val viewModel: MovieViewModel = viewModel()
    }

    View full-size slide

  72. @Composable
    fun MovieListViewModel(
    movieSection: MovieSection,
    isDarkModeActive: Boolean,
    switchDarkMode: () -> Unit = {},
    onClick: (movie: Movie) -> Unit
    ) {
    MovieList(
    title = title,
    movies = movies.value,
    isDarkModeActive = isDarkModeActive,
    switchDarkMode = switchDarkMode,
    onClick = onClick
    )
    }

    View full-size slide

  73. @Composable
    fun MovieListViewModel(
    movieSection: MovieSection,
    isDarkModeActive: Boolean,
    switchDarkMode: () -> Unit = {},
    onClick: (movie: Movie) -> Unit
    ) {
    val viewModel: MovieViewModel = viewModel()
    MovieList(
    title = title,
    movies = movies.value,
    isDarkModeActive = isDarkModeActive,
    switchDarkMode = switchDarkMode,
    onClick = onClick
    )
    }

    View full-size slide

  74. @Composable
    fun MovieListViewModel(
    movieSection: MovieSection,
    isDarkModeActive: Boolean,
    switchDarkMode: () -> Unit = {},
    onClick: (movie: Movie) -> Unit
    ) {
    val viewModel: MovieViewModel = viewModel()
    val movies = when (movieSection) {
    MovieSection.UPCOMING -> viewModel.getUpComing()
    .collectAsState(initial = emptyList())
    MovieSection.POPULAR -> viewModel.getPopulars()
    .collectAsState(initial = emptyList())
    MovieSection.TRENDING -> viewModel.getDailyTrending()
    .collectAsState(initial = emptyList())
    }
    MovieList(
    movies = movies.value,
    isDarkModeActive = isDarkModeActive,
    switchDarkMode = switchDarkMode,
    onClick = onClick
    )
    }

    View full-size slide

  75. @GerardPaligot
    DISCOVERING
    JETPACK COMPOSE

    View full-size slide

  76. @GerardPaligot
    DISCOVERING
    COMPOSE MULTIPLATFORM

    View full-size slide

  77. UI/UX UI/UX UI/UX
    BUSINESS LOGIC AND CORE

    View full-size slide

  78. UI/UX UI/UX UI/UX
    BUSINESS LOGIC AND CORE
    ACTUAL ACTUAL ACTUAL

    View full-size slide

  79. BUSINESS LOGIC AND CORE
    ACTUAL ACTUAL ACTUAL
    COMPOSE MULTIPLATFORM

    View full-size slide

  80. BUSINESS LOGIC AND CORE
    ACTUAL ACTUAL ACTUAL
    COMPOSE MULTIPLATFORM

    View full-size slide

  81. UX UX UX
    BUSINESS LOGIC AND CORE
    ACTUAL ACTUAL ACTUAL
    COMPOSE MULTIPLATFORM

    View full-size slide

  82. COMPILER
    RUNTIME
    UI
    FOUNDATION-LAYOUT
    ANIMATION
    FOUNDATION
    MATERIAL 3
    Transf m @C posable functi s Pro amming model, state
    management and runtime f
    the c pose c pil plugin
    Fundamental c p ents
    Basics lay ts like B , R
    and C umn
    C p ents animati s
    Basics c p ents and int acti s
    Implementati of Mat ial Design
    specificati s
    Implementati of Mat ial Y

    View full-size slide

  83. COMPILER
    RUNTIME
    UI
    FOUNDATION-LAYOUT
    ANIMATION
    FOUNDATION
    MATERIAL 3
    Transf m @C posable functi s Pro amming model, state
    management and runtime f
    the c pose c pil plugin

    View full-size slide

  84. COMPILER
    RUNTIME
    UI
    FOUNDATION-LAYOUT
    ANIMATION
    FOUNDATION
    MATERIAL 3
    Transf m @C posable functi s Pro amming model, state
    management and runtime f
    the c pose c pil plugin
    WEB-CORE-RUNTIME
    WEB-CORE
    WIDGETS
    Runtime dedicated to web apps
    to gen ate DOM tags
    Basics native web c p ents
    Modifi s and lay ts

    View full-size slide

  85. UX UX UX
    BUSINESS LOGIC AND CORE
    ACTUAL ACTUAL ACTUAL
    COMPOSE MULTIPLATFORM

    View full-size slide

  86. UX UX UX
    BUSINESS LOGIC AND CORE
    ACTUAL ACTUAL ACTUAL
    COMPOSE MULTIPLATFORM COMPOSE WEB

    View full-size slide

  87. WHY COMPOSE WEB?
    ▸ Compose Canvas rendering not compatible (yet) with web technologies
    ▸ Compose Web emit DOM UI for NPM librairies compatibility and SEO

    View full-size slide

  88. :android-app :desktop-app
    :data

    View full-size slide

  89. :android-app :desktop-app
    :data

    View full-size slide

  90. :android-app :desktop-app
    :data :components
    :theming

    View full-size slide

  91. :android-app :desktop-app
    :data :components
    :theming

    View full-size slide

  92. plugins {
    id("com.android.library")
    }
    android {
    ...
    }

    View full-size slide

  93. plugins {
    id("com.android.library")
    kotlin("multiplatform")
    id("org.jetbrains.compose")
    }
    android {
    ...
    }

    View full-size slide

  94. plugins {
    id("com.android.library")
    kotlin("multiplatform")
    id("org.jetbrains.compose")
    }
    android {
    ...
    }
    kotlin {
    android()
    jvm("desktop")
    }

    View full-size slide

  95. import org.jetbrains.compose.compose
    plugins {
    id("com.android.library")
    kotlin("multiplatform")
    id("org.jetbrains.compose")
    }
    android {
    ...
    }
    kotlin {
    android()
    jvm("desktop")
    sourceSets {
    named("commonMain") {
    dependencies {
    implementation(compose.runtime)
    implementation(compose.foundation)
    implementation(compose.material)
    }
    }
    }
    }

    View full-size slide

  96. SOURCE SET
    desktopMain
    SOURCE SET
    androidMain
    SOURCE SET
    commonMain

    View full-size slide

  97. @Composable
    expect fun Font(
    fontName: String,
    weight: FontWeight,
    style: FontStyle
    ): Font
    SOURCE SET
    commonMain

    View full-size slide

  98. @Composable
    actual fun Font(
    fontName: String,
    weight: FontWeight,
    style: FontStyle
    ): Font {
    val context = LocalContext.current
    val id = context.resources
    .getIdentifier(fontName, "font", context.packageName)
    return Font(id, weight, style)
    }
    SOURCE SET
    androidMain

    View full-size slide

  99. @Composable
    actual fun Font(
    fontName: String,
    weight: FontWeight,
    style: FontStyle
    ): Font {
    return androidx.compose.ui.text.platform.Font(
    "font/${fontName}.ttf",
    weight,
    style
    )
    }
    SOURCE SET
    desktopMain

    View full-size slide

  100. object Fonts {
    val roboto: FontFamily
    @Composable
    get() = FontFamily(
    Font(
    "roboto_bold",
    FontWeight.Bold,
    FontStyle.Normal
    ),
    Font(
    "roboto_italic",
    FontWeight.Normal,
    FontStyle.Italic
    ),
    Font(
    "roboto_regular",
    FontWeight.Normal,
    FontStyle.Normal
    )
    )
    }
    SOURCE SET
    commonMain

    View full-size slide

  101. object Fonts {
    val roboto: FontFamily
    @Composable
    get() = ...
    }
    SOURCE SET
    commonMain

    View full-size slide

  102. object Fonts {
    val roboto: FontFamily
    @Composable
    get() = ...
    }
    object Typographies {
    val roboto: Typography
    @Composable
    get() = Typography(defaultFontFamily = Fonts.roboto)
    }
    SOURCE SET
    commonMain

    View full-size slide

  103. object Fonts {
    val roboto: FontFamily
    @Composable
    get() = ...
    }
    object Typographies {
    val roboto: Typography
    @Composable
    get() = Typography(defaultFontFamily = Fonts.roboto)
    }
    @Composable
    fun ExploringMoviesTheme(
    isDarkMode: Boolean = isSystemInDarkTheme(),
    content: @Composable () -> Unit
    ) {
    MaterialTheme(
    colors =
    if (isDarkMode) DarkColorPalette else LightColorPalette,
    typography = Typographies.roboto,
    shapes = shapes,
    content = content
    )
    }
    SOURCE SET
    commonMain

    View full-size slide

  104. :android-app :desktop-app
    :data :components
    :theming

    View full-size slide

  105. @Composable
    expect fun RemoteImage(
    url: String,
    contentDescription: String?,
    modifier: Modifier = Modifier,
    contentScale: ContentScale = ContentScale.Fit
    )
    SOURCE SET
    commonMain

    View full-size slide

  106. import org.jetbrains.compose.compose
    plugins {
    ...
    }
    android {
    ...
    }
    kotlin {
    android()
    jvm("desktop")
    sourceSets {
    named("commonMain") {
    ...
    }
    named("androidMain") {
    dependencies {
    implementation(Dependencies.Accompanist.coil)
    }
    }
    }
    }

    View full-size slide

  107. @Composable
    actual fun CrossRemoteImage(
    url: String,
    contentDescription: String?,
    modifier: Modifier,
    contentScale: ContentScale
    ) {
    Image(
    painter = rememberAsyncImagePainter(model = url),
    modifier = modifier,
    contentDescription = contentDescription,
    contentScale = ContentScale.Crop,
    )
    }
    SOURCE SET
    androidMain

    View full-size slide

  108. import org.jetbrains.compose.compose
    plugins {
    ...
    }
    android {
    ...
    }
    kotlin {
    android()
    jvm("desktop")
    sourceSets {
    named("commonMain") {
    ...
    }
    named("androidMain") {
    ...
    }
    named("desktopMain") {
    dependencies {
    implementation(Dependencies.okhttp)
    }
    }
    }
    }

    View full-size slide

  109. @Composable
    actual fun CrossRemoteImage(
    url: String,
    contentDescription: String?,
    modifier: Modifier,
    contentScale: ContentScale
    ) {
    val image = remember { mutableStateOf(null) }
    if (image.value != null) {
    Image(
    bitmap = image.value!!,
    contentDescription = contentDescription,
    modifier = modifier,
    contentScale = contentScale
    )
    }
    }
    SOURCE SET
    desktopMain

    View full-size slide

  110. @Composable
    actual fun CrossRemoteImage(
    url: String,
    contentDescription: String?,
    modifier: Modifier,
    contentScale: ContentScale
    ) {
    val image = remember { mutableStateOf(null) }
    LaunchedEffect(url) {
    }
    if (image.value != null) {
    Image(
    bitmap = image.value!!,
    contentDescription = contentDescription,
    modifier = modifier,
    contentScale = contentScale
    )
    }
    }
    SOURCE SET
    desktopMain

    View full-size slide

  111. @Composable
    actual fun CrossRemoteImage(
    url: String,
    contentDescription: String?,
    modifier: Modifier,
    contentScale: ContentScale
    ) {
    val image = remember { mutableStateOf(null) }
    LaunchedEffect(url) {
    ImageLoader.load(url)?.let {
    }
    }
    if (image.value != null) {
    Image(
    bitmap = image.value!!,
    contentDescription = contentDescription,
    modifier = modifier,
    contentScale = contentScale
    )
    }
    }
    SOURCE SET
    desktopMain

    View full-size slide

  112. @Composable
    actual fun CrossRemoteImage(
    url: String,
    contentDescription: String?,
    modifier: Modifier,
    contentScale: ContentScale
    ) {
    val image = remember { mutableStateOf(null) }
    LaunchedEffect(url) {
    ImageLoader.load(url)?.let {
    image.value = org.jetbrains.skia.Image.makeFromEncoded(it)
    .toComposeImageBitmap()
    }
    }
    if (image.value != null) {
    Image(
    bitmap = image.value!!,
    contentDescription = contentDescription,
    modifier = modifier,
    contentScale = contentScale
    )
    }
    }
    SOURCE SET
    desktopMain

    View full-size slide

  113. WHY SHOULD CARE
    ABOUT COMPOSE?

    View full-size slide

  114. WHY SHOULD CARE ABOUT COMPOSE?
    ▸ Android UI framework never
    changed since 2008
    ▸ Need to wait very long time before
    bug fixes deployed in production
    ▸ Need to worry about keeping the
    View hierarchy as flat as possible
    ▸ Unbundled from OS
    ▸ Exit View and Fragment
    ▸ Clarify state ownership
    and event handling
    ▸ Writing less code

    View full-size slide

  115. slack.kotlinlang.org
    #compose
    Open Source Project
    github.com/androidx/androidx

    View full-size slide

  116. REFERENCES
    ▸ Discovering Movies, GitHub Repository of Gerard Paligot
    https://github.com/GerardPaligot/discovering-movies
    ▸ Official documentation about Jetpack Compose, Developer Android Website
    https://developer.android.com/jetpack/compose/documentation
    ▸ Pathways Compose
    https://developer.android.com/courses/pathways/compose
    ▸ Compose Academy
    https://compose.academy/
    ▸ Samples Compose app by community
    https://jetpackcompose.app/

    View full-size slide

  117. THANK YOU!
    @GerardPaligot

    View full-size slide