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. @Composable fun MovieMetadata( title: String, genres: List<String>, releaseDate: String, runtime:

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

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

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

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

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

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

    Int ) { Column { Text(...) Row(modifier = Modifier.padding(top = 5.dp)) { ... } } }
  8. @Composable fun MovieMetadata( title: String, genres: List<String>, 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) ) } }
  9. @Composable fun MovieMetadata( title: String, genres: List<String>, 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) ) } }
  10. @Composable fun MovieMetadata( title: String, genres: List<String>, 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) ) } } }
  11. @Composable fun MovieMetadata( title: String, genres: List<String>, releaseDate: String, runtime:

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

    Int, modifier: Modifier = Modifier ) { Column(modifier = modifier) { Text(...) Row(modifier = Modifier.padding(top = 5.dp)) { ... } CompositionLocalProvider( LocalContentAlpha provides ContentAlpha.medium ) { ... } } }
  13. @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 ) }
  14. @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 ) } }
  15. @Composable fun MovieItem( movie: Movie, modifier: Modifier = Modifier, onClick:

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

    (movie: Movie) -> Unit ) { Box(modifier = modifier) { Box( modifier = Modifier .width(130.dp) .aspectRatio(0.7f) ) { ... } } }
  17. @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) ) { ... } } }
  18. @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) ) { ... } } }
  19. @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) ) { ... } } }
  20. @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) ) { ... } } }
  21. @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) ) { ... } } }
  22. 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) ) { ... } } }
  23. 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) ) { ... } } }
  24. 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) ) { ... } }
  25. @Composable fun MovieList( title: String, movies: List<Movie>, 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 ) } } }
  26. @Composable fun MovieList( title: String, movies: List<Movie>, 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 ) } } }
  27. @Composable fun MovieList( title: String, movies: List<Movie>, 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 ) } } } }
  28. @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 )
  29. <?xml version="1.0" encoding="utf-8"?> <com.google.android.material.card.MaterialCardView 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"> <androidx.constraintlayout.widget.ConstraintLayout

    android:layout_width="match_parent" android:layout_height="match_parent"> <ImageView android:id="@+id/picture" 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" /> <androidx.constraintlayout.widget.Guideline android:id="@+id/guideline3" android:layout_width="wrap_content" android:layout_height="wrap_content" android:orientation="vertical" app:layout_constraintGuide_begin="100dp" />
  30. <LinearLayout 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"> <TextView

    android:id="@+id/name" android:layout_width="wrap_content" android:layout_height="wrap_content" android:textAppearance="@style/TextAppearance.Decathlon.Subtitle2" android:textColor="@color/colorOnSurface" /> <TextView android:id="@+id/type" android:layout_width="wrap_content" android:layout_height="wrap_content" android:textAppearance="@style/TextAppearance.Decathlon.Subtitle2" android:textColor="@color/grey" /> </LinearLayout>
  31. data class Activity( @DrawableRes val picture: Int, val displayName: String,

    val type: String, val price: Int ) class ActivityListAdapter(val items: MutableList<Activity>) : RecyclerView.Adapter<ActivityListAdapter.ActivityViewHolder>() { val onClick: LiveData<Activity> get() = this._onClick private val _onClick: MutableLiveData<Activity> by lazy { return@lazy MutableLiveData<Activity>() }
  32. 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<Activity>) { this.items.clear() this.items.addAll(items) notifyDataSetChanged() }
  33. 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) } } }
  34. 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, )
  35. 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 )
  36. 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 ) )
  37. @Composable fun MaterialTheme( colors: Colors = MaterialTheme.colors, typography: Typography =

    MaterialTheme.typography, shapes: Shapes = MaterialTheme.shapes, content: @Composable () -> Unit )
  38. @Composable fun ExploringMoviesTheme( isDarkMode: Boolean = isSystemInDarkTheme(), content: @Composable() ()

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

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

    -> Unit ) { MaterialTheme( colors = if (isDarkMode) DarkColorPalette else LightColorPalette, typography = typography, shapes = shapes, content = content ) }
  41. @Composable fun MovieList( title: String, movies: List<Movie>, 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 ) } } }
  42. @Composable fun MovieList( title: String, movies: List<Movie>, 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 ) } } }
  43. setContent { val isSystemDark = isSystemInDarkTheme() val isDarkModeState = remember

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

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

    { mutableStateOf(isSystemDark) } ExploringMoviesTheme(isDarkMode = isDarkModeState.value) { App( isDarkModeActive = isDarkModeState.value, switchDarkMode = { isDarkModeState.value = !isDarkModeState.value } ) } }
  46. class MovieViewModel : ViewModel() { fun getPopulars() = flow {

    ... } fun getDailyTrending() = flow { ... } fun getUpComing() = flow { ... } fun getMovieDetail(movieId: Int) = flow { ... } }
  47. @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() }
  48. @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 ) }
  49. @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 ) }
  50. @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 ) }
  51. 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
  52. 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
  53. 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
  54. UX UX UX BUSINESS LOGIC AND CORE ACTUAL ACTUAL ACTUAL

    COMPOSE MULTIPLATFORM COMPOSE WEB
  55. WHY COMPOSE WEB? ▸ Compose Canvas rendering not compatible (yet)

    with web technologies ▸ Compose Web emit DOM UI for NPM librairies compatibility and SEO
  56. 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) } } } }
  57. @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
  58. @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
  59. 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
  60. object Fonts { val roboto: FontFamily @Composable get() = ...

    } object Typographies { val roboto: Typography @Composable get() = Typography(defaultFontFamily = Fonts.roboto) } SOURCE SET commonMain
  61. 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
  62. @Composable expect fun RemoteImage( url: String, contentDescription: String?, modifier: Modifier

    = Modifier, contentScale: ContentScale = ContentScale.Fit ) SOURCE SET commonMain
  63. import org.jetbrains.compose.compose plugins { ... } android { ... }

    kotlin { android() jvm("desktop") sourceSets { named("commonMain") { ... } named("androidMain") { dependencies { implementation(Dependencies.Accompanist.coil) } } } }
  64. @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
  65. import org.jetbrains.compose.compose plugins { ... } android { ... }

    kotlin { android() jvm("desktop") sourceSets { named("commonMain") { ... } named("androidMain") { ... } named("desktopMain") { dependencies { implementation(Dependencies.okhttp) } } } }
  66. @Composable actual fun CrossRemoteImage( url: String, contentDescription: String?, modifier: Modifier,

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

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

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

    contentScale: ContentScale ) { val image = remember { mutableStateOf<ImageBitmap?>(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
  70. 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
  71. 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/