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

Droidcon Singapore - From XML To Compose - Ahme...

Droidcon Singapore - From XML To Compose - Ahmed Tikiwa

My journey of migrating my large Android app to Jetpack Compose has been ongoing over the past year, gradually adding various Compose-related features over time. This talk is a continuation of that journey primarily focusing on the addition of Jetpack Compose Navigation using Compose Destinations and the addition of Material Design 3.

Ahmed Tikiwa

October 13, 2022
Tweet

More Decks by Ahmed Tikiwa

Other Decks in Programming

Transcript

  1. Ahmed Tikiwa GDE Android @ahmed_tikiwa My journey of transforming an

    existing large app to Jetpack Compose From xml to compose
  2. Agenda What this talk will cover • What is Jetpack

    Compose • How I learned it • The migration approach • About the app • Adding Compose Navigation • Adding Material Design 3 • Before and after’s
  3. “Our theming layer is vastly more intuitive and legible. We’ve

    been able to accomplish within a single Kotlin file what otherwise extended across multiple XML files that were responsible for attribute definitions and assignments via multiple layered theme overlays.” Twitter
  4. “It’s much easier to trace through code when it’s all

    written in the same language [Kotlin] and often the same file, rather than jumping back and forth between Kotlin and XML” Monzo
  5. • Bottom-up approach ◦ Smaller UI elements e.g Button or

    TextView • Top-down approach ◦ Fragments or view containers The migration approach
  6. • All screens are Compose ◦ Use of ComposeView inside

    Fragments (inter-op) • Bottom navigation and Toolbar navigation view-based ◦ Jetpack Navigation used • Material 2 The Compose journey so far What has been migrated to Jetpack Compose to date
  7. by Rafael Costa (@raamcosta) Compose Destinations • KSP library •

    Processes annotations • Generates navigation-related code https://github.com/raamcosta/compose-destinations
  8. by Rafael Costa (@raamcosta) Compose Destinations • Typesafe navigation arguments

    • Simple but configurable navigation graphs setup • Navigating back with a result in a simple and type-safe way • Navigation animations through integration with Accompanist Navigation-Animation
  9. plugins { . . . id "com.google.devtools.ksp" version "$kotlin_version-1.0.6" }

    android { applicationVariants.all { variant -> kotlin.sourceSets { getByName(variant.name) { kotlin.srcDir("build/generated/ksp/${variant.name}/kotlin") } } } } Setting up app/build.gradle
  10. plugins { . . . id "com.google.devtools.ksp" version "$kotlin_version-1.0.6" }

    android { // Relates to https://github.com/google/ksp/issues/37 applicationVariants.all { variant -> kotlin.sourceSets { getByName(variant.name) { kotlin.srcDir("build/generated/ksp/${variant.name}/kotlin") } } } } Setting up app/build.gradle
  11. dependencies { . . . // Jetpack Compose Destinations implementation

    "io.github.raamcosta.compose-destinations:core:$compose_destinations_version” implementation "io.github.raamcosta.compose-destinations:ksp:$compose_destinations_version” // Accompanist navigation animation implementation "com.google.accompanist:accompanist-navigation-animation:$accompanist_version” } Setting up app/build.gradle
  12. @RootNavGraph(start = true) @Destination @Composable fun DashboardScreen( viewModel: DashboardViewModel =

    hiltViewModel(), navigator: DestinationsNavigator ) { ... Surface(modifier = Modifier.fillMaxSize()) {. . . } } The Main Screens app/ui/dashboard/DashboardScreen.kt
  13. @RootNavGraph(start = true) @Destination @Composable fun DashboardScreen( viewModel: DashboardViewModel =

    hiltViewModel(), navigator: DestinationsNavigator ) { ... Surface(modifier = Modifier.fillMaxSize()) {. . . } } The Main Screens app/ui/dashboard/DashboardScreen.kt
  14. @RootNavGraph(start = true) @Destination @Composable fun DashboardScreen( viewModel: DashboardViewModel =

    hiltViewModel(), navigator: DestinationsNavigator ) { ... Surface(modifier = Modifier.fillMaxSize()) {. . . } } The Main Screens app/ui/dashboard/DashboardScreen.kt
  15. @Destination(navArgsDelegate = ShowDetailArg::class) @Composable fun ShowDetailScreen( viewModel: ShowDetailViewModel = hiltViewModel(),

    showDetailArgs: ShowDetailArg?, navigator: DestinationsNavigator ) { Surface( modifier = Modifier .fillMaxSize() ) {. . . } } The Parameter-based Screens app/ui/detail/ShowDetailScreen.kt
  16. @Destination(navArgsDelegate = ShowDetailArg::class) @Composable fun ShowDetailScreen( viewModel: ShowDetailViewModel = hiltViewModel(),

    showDetailArgs: ShowDetailArg?, navigator: DestinationsNavigator ) { Surface( modifier = Modifier .fillMaxSize() ) {. . . } } The Parameter-based Screens app/ui/detail/ShowDetailScreen.kt package com.theupnextapp.domain data class ShowDetailArg( val source: String? = null, val showId: String?, val showTitle: String?, val showImageUrl: String?, val showBackgroundUrl: String?, val imdbID: String? = null, val isAuthorizedOnTrakt: Boolean? = false )
  17. @Destination(navArgsDelegate = ShowDetailArg::class) @Composable fun ShowDetailScreen( viewModel: ShowDetailViewModel = hiltViewModel(),

    showDetailArgs: ShowDetailArg?, navigator: DestinationsNavigator ) { Surface( modifier = Modifier .fillMaxSize() ) {. . . } } The Parameter-based Screens app/ui/detail/ShowDetailScreen.kt package com.theupnextapp.domain data class ShowDetailArg( val source: String? = null, val showId: String?, val showTitle: String?, val showImageUrl: String?, val showBackgroundUrl: String?, val imdbID: String? = null, val isAuthorizedOnTrakt: Boolean? = false )
  18. @Composable fun AppNavigation( navHostController: NavHostController, contentPadding: PaddingValues ) { val

    navHostEngine = rememberNavHostEngine() DestinationsNavHost( engine = navHostEngine, navGraph = NavGraphs.root, navController = navHostController, modifier = Modifier.padding(contentPadding) ) } The NavHost app/ui/navigation/AppNavigation.kt
  19. @ExperimentalMaterial3Api @Composable fun TopBar( navBackStackEntry: NavBackStackEntry?, onArrowClick: () -> Unit

    ) { val appBarIconState = rememberSaveable { mutableStateOf(true) } appBarIconState.value = isChildScreen(navBackStackEntry = navBackStackEntry) TopAppBar(. . .) } The TopBar app/ui/main/Topbar.kt
  20. @ExperimentalMaterial3Api @Composable fun TopBar( navBackStackEntry: NavBackStackEntry?, onArrowClick: () -> Unit

    ) { val appBarIconState = rememberSaveable { mutableStateOf(true) } appBarIconState.value = isChildScreen(navBackStackEntry = navBackStackEntry) TopAppBar(. . .) } The TopBar app/ui/main/Topbar.kt
  21. @ExperimentalMaterial3Api @Composable fun TopBar( navBackStackEntry: NavBackStackEntry?, onArrowClick: () -> Unit

    ) { val appBarIconState = rememberSaveable { mutableStateOf(true) } appBarIconState.value = isChildScreen(navBackStackEntry = navBackStackEntry) TopAppBar(. . .) } The TopBar app/ui/main/Topbar.kt
  22. @ExperimentalMaterial3Api @Composable fun TopBar( navBackStackEntry: NavBackStackEntry?, onArrowClick: () -> Unit

    ) { val appBarIconState = rememberSaveable { mutableStateOf(true) } appBarIconState.value = isChildScreen(navBackStackEntry = navBackStackEntry) TopAppBar(. . .) } The TopBar app/ui/main/Topbar.kt
  23. . . . TopAppBar( title = { AnimatedVisibility(visible = !appBarIconState.value)

    { Text( text = stringResource(id = R.string.app_name), style = MaterialTheme.typography.headlineSmall ) } }, navigationIcon = { NavigationIcon(appBarIconState = appBarIconState) { onArrowClick() } } ) The TopBar app/ui/main/Topbar.kt
  24. . . . TopAppBar( title = { AnimatedVisibility(visible = !appBarIconState.value)

    { Text( text = stringResource(id = R.string.app_name), style = MaterialTheme.typography.headlineSmall ) } }, navigationIcon = { NavigationIcon(appBarIconState = appBarIconState) { onArrowClick() } } ) The TopBar app/ui/main/Topbar.kt
  25. . . . TopAppBar( title = { AnimatedVisibility(visible = !appBarIconState.value)

    { Text( text = stringResource(id = R.string.app_name), style = MaterialTheme.typography.headlineSmall ) } }, navigationIcon = { NavigationIcon(appBarIconState = appBarIconState) { onArrowClick() } } ) The TopBar app/ui/main/Topbar.kt
  26. @Composable fun NavigationIcon( appBarIconState: State<Boolean>, onArrowClick: () -> Unit )

    { AnimatedVisibility(visible = appBarIconState.value) { IconButton(onClick = { onArrowClick() }) { Icon(Icons.Filled.ArrowBack, contentDescription = "Back arrow") } } } The TopBar app/ui/main/Topbar.kt
  27. @Composable fun NavigationIcon( appBarIconState: State<Boolean>, onArrowClick: () -> Unit )

    { AnimatedVisibility(visible = appBarIconState.value) { IconButton(onClick = { onArrowClick() }) { Icon(Icons.Filled.ArrowBack, contentDescription = "Back arrow") } } } The TopBar app/ui/main/Topbar.kt
  28. @Composable fun NavigationIcon( appBarIconState: State<Boolean>, onArrowClick: () -> Unit )

    { AnimatedVisibility(visible = appBarIconState.value) { IconButton(onClick = { onArrowClick() }) { Icon(Icons.Filled.ArrowBack, contentDescription = "Back arrow") } } } The TopBar app/ui/main/Topbar.kt
  29. import com.ramcosta.composedestinations.spec.Direction import com.theupnextapp.ui.destinations... enum class BottomBarDestination( val direction: Direction,

    val icon: ImageVector, @StringRes val label: Int ) { SearchScreen(SearchScreenDestination, Icons.Default.Search, R.string.bottom_nav_title_search), Dashboard(DashboardScreenDestination, Icons.Default.Home, R.string.bottom_nav_title_dashboard), Explore(ExploreScreenDestination, Icons.Filled.Explore, R.string.bottom_nav_title_explore), TraktAccount(TraktAccountScreenDestination(),Icons.Filled.AccountBox, R.string.bottom_nav_title_account ) } The BottomBar app/ui/main/Bottombar.kt
  30. import com.ramcosta.composedestinations.spec.Direction import com.theupnextapp.ui.destinations... enum class BottomBarDestination( val direction: Direction,

    val icon: ImageVector, @StringRes val label: Int ) { SearchScreen(SearchScreenDestination, Icons.Default.Search, R.string.bottom_nav_title_search), Dashboard(DashboardScreenDestination, Icons.Default.Home, R.string.bottom_nav_title_dashboard), Explore(ExploreScreenDestination, Icons.Filled.Explore, R.string.bottom_nav_title_explore), TraktAccount(TraktAccountScreenDestination(),Icons.Filled.AccountBox, R.string.bottom_nav_title_account ) } The BottomBar app/ui/main/Bottombar.kt
  31. import com.ramcosta.composedestinations.spec.Direction import com.theupnextapp.ui.destinations... enum class BottomBarDestination( val direction: Direction,

    val icon: ImageVector, @StringRes val label: Int ) { SearchScreen(SearchScreenDestination, Icons.Default.Search, R.string.bottom_nav_title_search), Dashboard(DashboardScreenDestination, Icons.Default.Home, R.string.bottom_nav_title_dashboard), Explore(ExploreScreenDestination, Icons.Filled.Explore, R.string.bottom_nav_title_explore), TraktAccount(TraktAccountScreenDestination(),Icons.Filled.AccountBox, R.string.bottom_nav_title_account ) } The BottomBar app/ui/main/Bottombar.kt
  32. @Composable fun BottomBar( currentDestination: Destination, onBottomBarItemClick: (Direction) -> Unit )

    { val bottomBarState = rememberSaveable { mutableStateOf(true) } bottomBarState.value = isMainScreen(destination = currentDestination) AnimatedVisibility( visible = bottomBarState.value, enter = slideInVertically(initialOffsetY = { it }), exit = slideOutVertically(targetOffsetY = { it }), content = {. . .} ) } The BottomBar app/ui/main/Bottombar.kt
  33. @Composable fun BottomBar( currentDestination: Destination, onBottomBarItemClick: (Direction) -> Unit )

    { val bottomBarState = rememberSaveable { mutableStateOf(true) } bottomBarState.value = isMainScreen(destination = currentDestination) AnimatedVisibility( visible = bottomBarState.value, enter = slideInVertically(initialOffsetY = { it }), exit = slideOutVertically(targetOffsetY = { it }), content = {. . .} ) } The BottomBar app/ui/main/Bottombar.kt
  34. @Composable fun BottomBar( currentDestination: Destination, onBottomBarItemClick: (Direction) -> Unit )

    { val bottomBarState = rememberSaveable { mutableStateOf(true) } bottomBarState.value = isMainScreen(destination = currentDestination) AnimatedVisibility( visible = bottomBarState.value, enter = slideInVertically(initialOffsetY = { it }), exit = slideOutVertically(targetOffsetY = { it }), content = {. . .} ) } The BottomBar app/ui/main/Bottombar.kt
  35. @Composable fun BottomBar( currentDestination: Destination, onBottomBarItemClick: (Direction) -> Unit )

    { val bottomBarState = rememberSaveable { mutableStateOf(true) } bottomBarState.value = isMainScreen(destination = currentDestination) AnimatedVisibility( visible = bottomBarState.value, enter = slideInVertically(initialOffsetY = { it }), exit = slideOutVertically(targetOffsetY = { it }), content = {. . .} ) } The BottomBar app/ui/main/Bottombar.kt
  36. @Composable fun BottomBar( currentDestination: Destination, onBottomBarItemClick: (Direction) -> Unit )

    { val bottomBarState = rememberSaveable { mutableStateOf(true) } bottomBarState.value = isMainScreen(destination = currentDestination) AnimatedVisibility( visible = bottomBarState.value, enter = slideInVertically(initialOffsetY = { it }), exit = slideOutVertically(targetOffsetY = { it }), content = {. . .} ) } The BottomBar app/ui/main/Bottombar.kt
  37. @Composable fun BottomBar(. . .) { ... AnimatedVisibility( ... content

    = { BottomAppBar { BottomBarDestination.values().forEach { destination -> NavigationBarItem( icon = { Icon(imageVector = destination.icon, contentDescription = stringResource(id = destination.label)) }, label = { Text(stringResource(id = destination.label)) }, selected = destination.direction.route.startsWith(currentDestination.baseRoute), onClick = { onBottomBarItemClick(destination.direction) } ... The BottomBar app/ui/main/Bottombar.kt
  38. @Composable fun BottomBar(. . .) { ... AnimatedVisibility( ... content

    = { BottomAppBar { BottomBarDestination.values().forEach { destination -> NavigationBarItem( icon = { Icon(imageVector = destination.icon, contentDescription = stringResource(id = destination.label)) }, label = { Text(stringResource(id = destination.label)) }, selected = destination.direction.route.startsWith(currentDestination.baseRoute), onClick = { onBottomBarItemClick(destination.direction) } ... The BottomBar app/ui/main/Bottombar.kt
  39. @Composable fun BottomBar(. . .) { ... AnimatedVisibility( ... content

    = { BottomAppBar { BottomBarDestination.values().forEach { destination -> NavigationBarItem( icon = { Icon(imageVector = destination.icon, contentDescription = stringResource(id = destination.label)) }, label = { Text(stringResource(id = destination.label)) }, selected = destination.direction.route.startsWith(currentDestination.baseRoute), onClick = { onBottomBarItemClick(destination.direction) } ... The BottomBar app/ui/main/Bottombar.kt
  40. @Composable fun BottomBar(. . .) { ... AnimatedVisibility( ... content

    = { BottomAppBar { BottomBarDestination.values().forEach { destination -> NavigationBarItem( icon = { Icon(imageVector = destination.icon, contentDescription = stringResource(id = destination.label)) }, label = { Text(stringResource(id = destination.label)) }, selected = destination.direction.route.startsWith(currentDestination.baseRoute), onClick = { onBottomBarItemClick(destination.direction) } ... The BottomBar app/ui/main/Bottombar.kt
  41. @Composable fun BottomBar(. . .) { ... AnimatedVisibility( ... content

    = { BottomAppBar { BottomBarDestination.values().forEach { destination -> NavigationBarItem( icon = { Icon(imageVector = destination.icon, contentDescription = stringResource(id = destination.label)) }, label = { Text(stringResource(id = destination.label)) }, selected = destination.direction.route.startsWith(currentDestination.baseRoute), onClick = { onBottomBarItemClick(destination.direction) } ... The BottomBar app/ui/main/Bottombar.kt
  42. @Composable fun BottomBar(. . .) { ... AnimatedVisibility( ... content

    = { BottomAppBar { BottomBarDestination.values().forEach { destination -> NavigationBarItem( icon = { Icon(imageVector = destination.icon, contentDescription = stringResource(id = destination.label)) }, label = { Text(stringResource(id = destination.label)) }, selected = destination.direction.route.startsWith(currentDestination.baseRoute), onClick = { onBottomBarItemClick(destination.direction) } ... The BottomBar app/ui/main/Bottombar.kt
  43. @Composable fun BottomBar(. . .) { ... AnimatedVisibility( ... content

    = { BottomAppBar { BottomBarDestination.values().forEach { destination -> NavigationBarItem( icon = { Icon(imageVector = destination.icon, contentDescription = stringResource(id = destination.label)) }, label = { Text(stringResource(id = destination.label)) }, selected = destination.direction.route.startsWith(currentDestination.baseRoute), onClick = { onBottomBarItemClick(destination.direction) } ... The BottomBar app/ui/main/Bottombar.kt
  44. @Composable fun MainScaffold( navHostController: NavHostController, topBar: @Composable (NavBackStackEntry?) -> Unit,

    bottomBar: @Composable (Destination) -> Unit, content: @Composable (PaddingValues) -> Unit ) { val currentBackStackEntryAsState by navHostController.currentBackStackEntryAsState() val destination = currentBackStackEntryAsState?.appDestination() ?: NavGraphs.root.startRoute.startAppDestination Scaffold( topBar = { topBar(currentBackStackEntryAsState) }, bottomBar = { bottomBar(destination) }, content = content ) } The Scaffold app/ui/main/MainScaffold.kt
  45. @Composable fun MainScaffold( navHostController: NavHostController, topBar: @Composable (NavBackStackEntry?) -> Unit,

    bottomBar: @Composable (Destination) -> Unit, content: @Composable (PaddingValues) -> Unit ) { val currentBackStackEntryAsState by navHostController.currentBackStackEntryAsState() val destination = currentBackStackEntryAsState?.appDestination() ?: NavGraphs.root.startRoute.startAppDestination Scaffold( topBar = { topBar(currentBackStackEntryAsState) }, bottomBar = { bottomBar(destination) }, content = content ) } The Scaffold app/ui/main/MainScaffold.kt
  46. @Composable fun MainScaffold( navHostController: NavHostController, topBar: @Composable (NavBackStackEntry?) -> Unit,

    bottomBar: @Composable (Destination) -> Unit, content: @Composable (PaddingValues) -> Unit ) { val currentBackStackEntryAsState by navHostController.currentBackStackEntryAsState() val destination = currentBackStackEntryAsState?.appDestination() ?: NavGraphs.root.startRoute.startAppDestination Scaffold( topBar = { topBar(currentBackStackEntryAsState) }, bottomBar = { bottomBar(destination) }, content = content ) } The Scaffold app/ui/main/MainScaffold.kt
  47. @Composable fun MainScaffold( navHostController: NavHostController, topBar: @Composable (NavBackStackEntry?) -> Unit,

    bottomBar: @Composable (Destination) -> Unit, content: @Composable (PaddingValues) -> Unit ) { val currentBackStackEntryAsState by navHostController.currentBackStackEntryAsState() val destination = currentBackStackEntryAsState?.appDestination() ?: NavGraphs.root.startRoute.startAppDestination Scaffold( topBar = { topBar(currentBackStackEntryAsState) }, bottomBar = { bottomBar(destination) }, content = content ) } The Scaffold app/ui/main/MainScaffold.kt
  48. @Composable fun MainScaffold( navHostController: NavHostController, topBar: @Composable (NavBackStackEntry?) -> Unit,

    bottomBar: @Composable (Destination) -> Unit, content: @Composable (PaddingValues) -> Unit ) { val currentBackStackEntryAsState by navHostController.currentBackStackEntryAsState() val destination = currentBackStackEntryAsState?.appDestination() ?: NavGraphs.root.startRoute.startAppDestination Scaffold( topBar = { topBar(currentBackStackEntryAsState) }, bottomBar = { bottomBar(destination) }, content = content ) } The Scaffold app/ui/main/MainScaffold.kt
  49. @Composable fun MainScaffold( navHostController: NavHostController, topBar: @Composable (NavBackStackEntry?) -> Unit,

    bottomBar: @Composable (Destination) -> Unit, content: @Composable (PaddingValues) -> Unit ) { val currentBackStackEntryAsState by navHostController.currentBackStackEntryAsState() val destination = currentBackStackEntryAsState?.appDestination() ?: NavGraphs.root.startRoute.startAppDestination Scaffold( topBar = { topBar(currentBackStackEntryAsState) }, bottomBar = { bottomBar(destination) }, content = content ) } The Scaffold app/ui/main/MainScaffold.kt
  50. @Composable fun MainScaffold( navHostController: NavHostController, topBar: @Composable (NavBackStackEntry?) -> Unit,

    bottomBar: @Composable (Destination) -> Unit, content: @Composable (PaddingValues) -> Unit ) { val currentBackStackEntryAsState by navHostController.currentBackStackEntryAsState() val destination = currentBackStackEntryAsState?.appDestination() ?: NavGraphs.root.startRoute.startAppDestination Scaffold( topBar = { topBar(currentBackStackEntryAsState) }, bottomBar = { bottomBar(destination) }, content = content ) } The Scaffold app/ui/main/MainScaffold.kt
  51. @Composable fun MainScreen( traktCodeState: MutableState<String?>, ) { val navController =

    rememberAnimatedNavController() ... MainScaffold( navHostController = navController, topBar = { navBackStackEntry -> TopBar(...) { navController.navigateUp() } }, bottomBar = { destination -> BottomBar(currentDestination = destination, onBottomBarItemClick = { navController.navigateTo(it) { launchSingleTop = true } } ) } ) { AppNavigation(navHostController = navController, contentPadding = it) } The MainScreen app/ui/main/MainScreen.kt
  52. @Composable fun MainScreen( traktCodeState: MutableState<String?>, ) { val navController =

    rememberAnimatedNavController() ... MainScaffold( navHostController = navController, topBar = { navBackStackEntry -> TopBar(...) { navController.navigateUp() } }, bottomBar = { destination -> BottomBar(currentDestination = destination, onBottomBarItemClick = { navController.navigateTo(it) { launchSingleTop = true } } ) } ) { AppNavigation(navHostController = navController, contentPadding = it) } The MainScreen app/ui/main/MainScreen.kt
  53. @Composable fun MainScreen( traktCodeState: MutableState<String?>, ) { val navController =

    rememberAnimatedNavController() ... MainScaffold( navHostController = navController, topBar = { navBackStackEntry -> TopBar(...) { navController.navigateUp() } }, bottomBar = { destination -> BottomBar(currentDestination = destination, onBottomBarItemClick = { navController.navigateTo(it) { launchSingleTop = true } } ) } ) { AppNavigation(navHostController = navController, contentPadding = it) } The MainScreen app/ui/main/MainScreen.kt
  54. @Composable fun MainScreen( traktCodeState: MutableState<String?>, ) { val navController =

    rememberAnimatedNavController() ... MainScaffold( navHostController = navController, topBar = { navBackStackEntry -> TopBar(...) { navController.navigateUp() } }, bottomBar = { destination -> BottomBar(currentDestination = destination, onBottomBarItemClick = { navController.navigateTo(it) { launchSingleTop = true } } ) } ) { AppNavigation(navHostController = navController, contentPadding = it) } The MainScreen app/ui/main/MainScreen.kt
  55. @Composable fun MainScreen( traktCodeState: MutableState<String?>, ) { val navController =

    rememberAnimatedNavController() ... MainScaffold( navHostController = navController, topBar = { navBackStackEntry -> TopBar(...) { navController.navigateUp() } }, bottomBar = { destination -> BottomBar(currentDestination = destination, onBottomBarItemClick = { navController.navigateTo(it) { launchSingleTop = true } } ) } ) { AppNavigation(navHostController = navController, contentPadding = it) } The MainScreen app/ui/main/MainScreen.kt
  56. @Composable fun MainScreen( traktCodeState: MutableState<String?>, ) { val navController =

    rememberAnimatedNavController() ... MainScaffold( navHostController = navController, topBar = { navBackStackEntry -> TopBar(...) { navController.navigateUp() } }, bottomBar = { destination -> BottomBar(currentDestination = destination, onBottomBarItemClick = { navController.navigateTo(it) { launchSingleTop = true } } ) } ) { AppNavigation(navHostController = navController, contentPadding = it) } The MainScreen app/ui/main/MainScreen.kt
  57. @Composable fun MainScreen( traktCodeState: MutableState<String?>, ) { val navController =

    rememberAnimatedNavController() ... MainScaffold( navHostController = navController, topBar = { navBackStackEntry -> TopBar(...) { navController.navigateUp() } }, bottomBar = { destination -> BottomBar(currentDestination = destination, onBottomBarItemClick = { navController.navigateTo(it) { launchSingleTop = true } } ) } ) { AppNavigation(navHostController = navController, contentPadding = it) } The MainScreen app/ui/main/MainScreen.kt
  58. @Composable fun MainScreen( traktCodeState: MutableState<String?>, ) { val navController =

    rememberAnimatedNavController() ... MainScaffold( navHostController = navController, topBar = { navBackStackEntry -> TopBar(...) { navController.navigateUp() } }, bottomBar = { destination -> BottomBar(currentDestination = destination, onBottomBarItemClick = { navController.navigateTo(it) { launchSingleTop = true } } ) } ) { AppNavigation(navHostController = navController, contentPadding = it) } The MainScreen app/ui/main/MainScreen.kt
  59. override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { val dataString:

    MutableState<String?> = rememberSaveable { mutableStateOf("") } DisposableEffect(Unit) { val listener = Consumer<Intent> { intent -> val code = intent.data?.getQueryParameter("code") dataString.value = code } addOnNewIntentListener(listener) onDispose { removeOnNewIntentListener(listener) } } UpnextTheme { MainScreen(dataString) } } The MainActivity app/MainActivity.kt
  60. override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { val dataString:

    MutableState<String?> = rememberSaveable { mutableStateOf("") } DisposableEffect(Unit) { val listener = Consumer<Intent> { intent -> val code = intent.data?.getQueryParameter("code") dataString.value = code } addOnNewIntentListener(listener) onDispose { removeOnNewIntentListener(listener) } } UpnextTheme { MainScreen(dataString) } } The MainScreen app/MainActivity.kt
  61. override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { val dataString:

    MutableState<String?> = rememberSaveable { mutableStateOf("") } DisposableEffect(Unit) { val listener = Consumer<Intent> { intent -> val code = intent.data?.getQueryParameter("code") dataString.value = code } addOnNewIntentListener(listener) onDispose { removeOnNewIntentListener(listener) } } UpnextTheme { MainScreen(dataString) } } The MainScreen app/MainActivity.kt
  62. override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { val dataString:

    MutableState<String?> = rememberSaveable { mutableStateOf("") } DisposableEffect(Unit) { val listener = Consumer<Intent> { intent -> val code = intent.data?.getQueryParameter("code") dataString.value = code } addOnNewIntentListener(listener) onDispose { removeOnNewIntentListener(listener) } } UpnextTheme { MainScreen(dataString) } } The MainScreen app/MainActivity.kt
  63. override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { val dataString:

    MutableState<String?> = rememberSaveable { mutableStateOf("") } DisposableEffect(Unit) { val listener = Consumer<Intent> { intent -> val code = intent.data?.getQueryParameter("code") dataString.value = code } addOnNewIntentListener(listener) onDispose { removeOnNewIntentListener(listener) } } UpnextTheme { MainScreen(dataString) } } The MainScreen app/MainActivity.kt
  64. dependencies { . . . // Compose Material Design implementation

    "androidx.compose.material3:material3:$compose_material_version” implementation "androidx.compose.material3:material3-window-size-class:$compose_material_version” implementation "com.google.android.material:compose-theme-adapter:$compose_theme_adapter_version” } Setting up app/build.gradle
  65. package com.theupnextapp.ui.theme import androidx.compose.ui.graphics.Color val Teal500 = Color(0xFF009688) val Purple500

    = Color(0xFF6200EE) val Teal600 = Color(0xFF00897B) val Teal200 = Color(0xFF80CBC4) val Purple200 = Color(0xFFBB86FC) val Teal700 = Color(0xFF00796B) Defining the colors for the theme app/ui/theme/color/Color.kt
  66. package com.theupnextapp.ui.theme import androidx.compose.ui.graphics.Color import androidx.compose.material3.Typography import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontFamily

    import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.sp val Typography = Typography( bodyLarge = TextStyle( fontFamily = FontFamily.Default, fontWeight = FontWeight.Normal, fontSize = 16.sp, lineHeight = 24.sp, letterSpacing = 0.5.sp ) ) Defining the typography for the theme app/ui/theme/color/Type.kt
  67. An overview of the screens Upnext: TV Series Manager Compose

    AppBar Compose BottomBar @Composable ShowsRow() & uses LazyRow()
  68. An overview of the screens Upnext: TV Series Manager Compose

    AppBar Compose BottomBar @Composable TrendingShowsRow() & uses LazyRow() @Composable PopularShowsRow() & uses LazyRow()
  69. An overview of the screens Upnext: TV Series Manager Compose

    AppBar @Composable ShowCastList() @Composable BackdropAndTitle() @Composable PosterAndMetadata()
  70. An overview of the screens Upnext: TV Series Manager Compose

    AppBar @Composable ShowDetailButtons() @Composable ShowCastList() @Composable NextEpisode() @Composable PreviousEpisode()
  71. An overview of the screens Upnext: TV Series Manager Compose

    AppBar @Composable TraktRatingSummary() @Composable NextEpisode() @Composable PreviousEpisode()
  72. An overview of the screens Upnext: TV Series Manager Compose

    AppBar @Composable SectionHeadingText() @Composable ShowSeasonsCard() @Composable ShowSeasons() uses LazyColumn()
  73. An overview of the screens Upnext: TV Series Manager Compose

    AppBar @Composable ShowSeasonEpisodes() uses LazyColumn() @Composable ShowSeasonEpisodeCard() @Composable SectionHeadingText()
  74. An overview of the screens Upnext: TV Series Manager Compose

    AppBar Composable BottomBar @Composable FavoritesList() uses LazyVerticalGrid() @Composable ListPosterCard() @Composable SectionHeadingText()
  75. • Official Compose Documentation https://developer.android.com/jetpack/compose • Official Compose course https://developer.android.com/courses/pathways/compose

    • Android on YouTube https://www.youtube.com/c/AndroidDevelopers • State Hoisting https://developer.android.com/jetpack/compose/state#state-hoisting Resources
  76. • UpNext TV Series Manager code is available as an

    open-source project and part of the Google Developers Dev Library https://devlibrary.withgoogle.com/products/android/repos/akitikkx-upnext Click on the “View on Github” to see the code. Contributions welcome from the community! Please read my Readme and contribution Guidelines for more information Resources