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

Navigating with Compose in multi-module code

Adit Lal
November 14, 2022

Navigating with Compose in multi-module code

This talk would be targeting a vital and a pressing matter - how does one navigate to
composable screens using Android Navigation for compose in a multi-modular code architecture, where feature screens are written in feature modules. Problems faced and how to overcome them. We would be discussing some key learning points to remember while navigating between screens

Adit Lal

November 14, 2022
Tweet

More Decks by Adit Lal

Other Decks in Programming

Transcript

  1. Navigating with Jetpack Compose in a multi-modular codebase Google Developers

    Adit Lal 
 Individual Consultant
  2. Large amount of mobile apps need some form of navigation

    When working with compostable , is it possible to navigate between them? In Compose world - fragment free , preferably single Activity. Navigating in apps
  3. Jetpack Navigation 2019 Navigation Graph Navigation Intent 2008 Activity based

    navigation Fragment Manager 2011 Fragment based navigation
  4. Jetpack Compose 2021

  5. NavHost( navController = navController, startDestination = Screen.List.route ){ composable(“Details”) {

    DetailsScreen(...) } composable(“Search”) { SearchScreen(...) } composable(“List”) { ListScreen(...) } } NavHost Destination Destination Destination Compose + Navigation
  6. NavHost( navController = navController, startDestination = Screen.List.route ){ composable(“Details”) {

    DetailsScreen(...) } composable(“Search”) { SearchScreen(...) } composable(“List”) { ListScreen(...) } } NavHost Destination Destination Destination Compose + Navigation
  7. NavHost( navController = navController, startDestination = Screen.List.route ){ composable(“Details”) {

    DetailsScreen(...) } composable(“Search”) { SearchScreen(...) } composable(“List”) { ListScreen(...) } } NavHost Destination Destination Destination Compose + Navigation
  8. Require a NavHostController reference in Composable functions

  9. Difficult to test navigation logic

  10. Adds friction to Compose Migration

  11. Coupled to the Navigation Dependencies

  12. Multi-Modular Navigation ?

  13. How to Navigate to Composables across di ff erent features

    modules without depending on a NavHostController reference
  14. How to decouple these modules from Compose Navigation and handle

    navigation in a centralised location through View Models
  15. How to provide View Models to these Composables using Hilt

    Navigation Compose without having each feature module rely on that dependency
  16. How to simply our approach to testing by optimising our

    Navigation logic
  17. Hilt Navigation Compose Activity Compose Nav Controller App module

  18. Navigation Manager Navigation Commands Hilt Navigation Compose Activity Compose Nav

    Controller uses uses uses observes declares App module Nav module
  19. Navigation Manager Navigation Commands Hilt Navigation Compose Activity ViewModel Compose

    Nav Controller Composable provides uses uses uses uses uses triggers observes declares navigates to App module Nav module Feature module
  20. @Composable private fun StarWarsPeopleList( viewState: PeopleListState )

  21. @Composable private fun StarWarsPeopleList( viewState: PeopleListState )

  22. @Composable private fun StarWarsPeopleList( viewState: PeopleListState )

  23. @HiltViewModel class PeopleListViewModel @Inject constructor( private val savedStateHandle: SavedStateHandle, ...

    ) : ViewModel() { val state: LiveData<PeopleListState> ... }
  24. @HiltViewModel class PeopleListViewModel @Inject constructor( private val savedStateHandle: SavedStateHandle, ...

    ) : ViewModel() { val state: LiveData<PeopleListState> ... }
  25. @HiltViewModel class PeopleListViewModel @Inject constructor( private val savedStateHandle: SavedStateHandle, ...

    ) : ViewModel() { val state: LiveData<PeopleListState> ... }
  26. @HiltViewModel class PeopleListViewModel @Inject constructor( private val savedStateHandle: SavedStateHandle, ...

    ) : ViewModel() { val state: LiveData<PeopleListState> ... }
  27. @Composable fun StarWarsPeopleList( viewModel: PeopleListViewModel ) { val state by

    viewModel.uiState.observeAsState() StarWarsPeopleList(state) }
  28. @Composable fun StarWarsPeopleList( viewModel: PeopleListViewModel ) { val state by

    viewModel.uiState.observeAsState() StarWarsPeopleList(state) }
  29. @Composable fun StarWarsPeopleList( viewModel: PeopleListViewModel ) { val state by

    viewModel.uiState.observeAsState() StarWarsPeopleList(state) }
  30. @Composable fun StarWarsPeopleList( viewModel: PeopleListViewModel ) { val state by

    viewModel.uiState.observeAsState() StarWarsPeopleList(state) }
  31. @Composable fun Details( viewModel: DetailsViewModel ) { val state by

    viewModel.uiState.observeAsState() DetailsContent(state) } @HiltViewModel class DetailsViewModel @Inject constructor( private val savedStateHandle: SavedStateHandle, ... ) : ViewModel()
  32. @Composable fun Details( viewModel: DetailsViewModel ) { val state by

    viewModel.uiState.observeAsState() DetailsContent(state) } @HiltViewModel class DetailsViewModel @Inject constructor( private val savedStateHandle: SavedStateHandle, ... ) : ViewModel()
  33. interface NavigationCommand { val arguments: List<NamedNavArgument> val destination: String }

  34. interface NavigationCommand { val arguments: List<NamedNavArgument> val destination: String }

  35. interface NavigationCommand { val arguments: List<NamedNavArgument> val destination: String }

  36. object NavigationDirections { val list = object : NavigationCommand {

    override val arguments = emptyList<NamedNavArgument>() override val destination = "peoplelist" } val details = object : NavigationCommand { override val arguments = emptyList<NamedNavArgument>() override val destination = "details" } }
  37. object NavigationDirections { val list = object : NavigationCommand {

    override val arguments = emptyList<NamedNavArgument>() override val destination = "peoplelist" } val details = object : NavigationCommand { override val arguments = emptyList<NamedNavArgument>() override val destination = "details" } }
  38. object DetailsNavigation { private val KEY_PEOPLE_ID = "peopleId" val route

    = "details/{$KEY_PEOPLE_ID}" val arguments = listOf( navArgument(KEY_PEOPLE_ID) { type = NavType.StringType } ) fun details( peopleId: String? = null ) = object : NavigationCommand { override val arguments = arguments override val destination = "details/$KEY_PEOPLE_ID" } }
  39. object DetailsNavigation { private val KEY_PEOPLE_ID = "peopleId" val route

    = "details/{$KEY_PEOPLE_ID}" val arguments = listOf( navArgument(KEY_PEOPLE_ID) { type = NavType.StringType } ) fun details( peopleId: String? = null ) = object : NavigationCommand { override val arguments = arguments override val destination = "details/$KEY_PEOPLE_ID" } }
  40. Se tt ing up 
 Navigation Graph

  41. Navigation Manager Navigation Commands Hilt Navigation Compose Activity ViewModel Compose

    Nav Controller Composable provides uses uses uses uses uses triggers observes declares navigates to App module Nav module Feature module
  42. val navController = rememberNavController()

  43. NavHost( navController, startDestination = NavigationDirections.Authentication.destination ) { }

  44. Se tt ing up 
 Navigation Destinations

  45. Navigation Manager Navigation Commands Hilt Navigation Compose Activity ViewModel Compose

    Nav Controller Composable provides uses uses uses uses uses triggers observes declares navigates to App module Nav module Feature module
  46. composable(NavigationDirections.List.destination) { StarWarsPeopleList() }

  47. composable(NavigationDirections.Details.destination) { Details() }

  48. Providing ViewModels 
 to Composables

  49. Navigation Manager Navigation Commands Hilt Navigation Compose Activity ViewModel Compose

    Nav Controller Composable provides uses uses uses uses uses triggers observes declares navigates to App module Nav module Feature module
  50. androidx.hilt:hilt-navigation-compose

  51. StarWarsPeopleList( navController.hiltNavGraphViewModel(route = NavigationDirections.List.destination) )

  52. StarWarsPeopleList( hiltNavGraphViewModel() 
 )

  53. Handling Navigation 
 Events

  54. Navigation Manager Navigation Commands Hilt Navigation Compose Activity ViewModel Compose

    Nav Controller Composable provides uses uses uses uses uses triggers observes declares navigates to App module Nav module Feature module
  55. A component that is used to output the previously de

    fi ned NavigationCommand events, allowing an external class to observe these events| 
 A function that can be used to trigger these NavigationCommand events, allowing the observer of the above component to handle them Handling Navigation Events
  56. class NavigationManager { var commands = MutableStateFlow(Default) fun navigate( directions:

    NavigationCommand ) { commands.value = directions } }
  57. class NavigationManager { var commands = MutableStateFlow(Default) fun navigate( directions:

    NavigationCommand ) { commands.value = directions } }
  58. class NavigationManager { var commands = MutableStateFlow(Default) fun navigate( directions:

    NavigationCommand ) { commands.value = directions } }
  59. @Module @InstallIn(SingletonComponent::class) class AppModule { @Singleton @Provides fun providesNavigationManager() =

    NavigationManager() }
  60. @Module @InstallIn(SingletonComponent::class) class AppModule { @Singleton @Provides fun providesNavigationManager() =

    NavigationManager() }
  61. @Inject lateinit var navigationManager: NavigationManager navigationManager.commands.collectAsState().value.also { command -> if

    (command.destination.isNotEmpty()) navController.navigate(command.destination) }
  62. @Inject lateinit var navigationManager: NavigationManager navigationManager.commands.collectAsState().value.also { command -> if

    (command.destination.isNotEmpty()) navController.navigate(command.destination) }
  63. Triggering Navigation 
 Events

  64. Navigation Manager Navigation Commands Hilt Navigation Compose Activity ViewModel Compose

    Nav Controller Composable provides uses uses uses uses uses triggers observes declares navigates to App module Nav module Feature module
  65. @HiltViewModel class PeopleListViewModel @Inject constructor( private val savedStateHandle: SavedStateHandle, private

    val results: Results, private val sharedPrefs: Preferences, private val navigationManager: NavigationManager )
  66. @HiltViewModel class PeopleListViewModel @Inject constructor( private val savedStateHandle: SavedStateHandle, private

    val results: Results, private val sharedPrefs: Preferences, private val navigationManager: NavigationManager )
  67. navigationManager.navigate(NavigationDirections.Details)

  68. verify(mockNavigationManager).navigate(NavigationDirections.Details) Testing FTW 🎉

  69. Recap 
 Navigation Source : 
 www.joebirch.co

  70. We’ve removed the need to pass around a NavHostController to

    our composables Source : 
 www.joebirch.co
  71. Hilt ViewModels in Composables but w/o any direct deps in

    feature modules Source : 
 www.joebirch.co
  72. We’ve centralised our navigation logic and created a contract for

    the things which can be triggered Source : 
 www.joebirch.co
  73. Check out https://github.com/aldefy/Andromeda 
 https://bit.ly/3Nic0JF - Sample catalog app 


    
 Andromeda is an open-source Jetpack Compose design system. A collection of guidelines and components can be used to create amazing compose app user experiences. Foundations introduce Andromeda tokens and principles while Components provide the bolts and nuts that make Andromeda Compose wrapped apps tick.
  74. Thats all folks! https://cal.com/adit/30min 🎯@aditlal 🔗aditlal.dev