$30 off During Our Annual Pro Sale. View Details »

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

    View Slide

  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

    View Slide

  3. Jetpack Navigation
    2019
    Navigation
    Graph
    Navigation
    Intent
    2008
    Activity based
    navigation
    Fragment Manager
    2011
    Fragment based
    navigation

    View Slide

  4. Jetpack Compose
    2021

    View Slide

  5. NavHost(


    navController = navController,


    startDestination = Screen.List.route


    ){


    composable(“Details”) { DetailsScreen(...) }


    composable(“Search”) { SearchScreen(...) }


    composable(“List”) { ListScreen(...) }


    }
    NavHost
    Destination Destination Destination
    Compose + Navigation

    View Slide

  6. NavHost(


    navController = navController,


    startDestination = Screen.List.route


    ){


    composable(“Details”) { DetailsScreen(...) }


    composable(“Search”) { SearchScreen(...) }


    composable(“List”) { ListScreen(...) }


    }
    NavHost
    Destination Destination Destination
    Compose + Navigation

    View Slide

  7. NavHost(


    navController = navController,


    startDestination = Screen.List.route


    ){


    composable(“Details”) { DetailsScreen(...) }


    composable(“Search”) { SearchScreen(...) }


    composable(“List”) { ListScreen(...) }


    }
    NavHost
    Destination Destination Destination
    Compose + Navigation

    View Slide

  8. Require a NavHostController
    reference in Composable functions

    View Slide

  9. Difficult to test navigation logic

    View Slide

  10. Adds friction to Compose Migration

    View Slide

  11. Coupled to the Navigation
    Dependencies

    View Slide

  12. Multi-Modular
    Navigation


    ?

    View Slide

  13. How to Navigate to Composables across di
    ff
    erent features modules
    without depending on a NavHostController reference

    View Slide

  14. How to decouple these modules from Compose Navigation and handle
    navigation in a centralised location through View Models

    View Slide

  15. How to provide View Models to these Composables using Hilt
    Navigation Compose without having each feature module rely on that
    dependency

    View Slide

  16. How to simply our approach to testing by optimising our Navigation
    logic

    View Slide

  17. Hilt Navigation
    Compose
    Activity
    Compose Nav
    Controller
    App module

    View Slide

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

    View Slide

  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

    View Slide

  20. @Composable


    private fun StarWarsPeopleList(


    viewState: PeopleListState


    )

    View Slide

  21. @Composable


    private fun StarWarsPeopleList(


    viewState: PeopleListState


    )

    View Slide

  22. @Composable


    private fun StarWarsPeopleList(


    viewState: PeopleListState


    )

    View Slide

  23. @HiltViewModel


    class PeopleListViewModel @Inject constructor(


    private val savedStateHandle: SavedStateHandle,


    ...


    ) : ViewModel() {


    val state: LiveData ...


    }

    View Slide

  24. @HiltViewModel


    class PeopleListViewModel @Inject constructor(


    private val savedStateHandle: SavedStateHandle,


    ...


    ) : ViewModel() {


    val state: LiveData ...


    }

    View Slide

  25. @HiltViewModel


    class PeopleListViewModel @Inject constructor(


    private val savedStateHandle: SavedStateHandle,


    ...


    ) : ViewModel() {


    val state: LiveData ...


    }

    View Slide

  26. @HiltViewModel


    class PeopleListViewModel @Inject constructor(


    private val savedStateHandle: SavedStateHandle,


    ...


    ) : ViewModel() {


    val state: LiveData ...


    }

    View Slide

  27. @Composable


    fun StarWarsPeopleList(


    viewModel: PeopleListViewModel


    ) {


    val state by viewModel.uiState.observeAsState()


    StarWarsPeopleList(state)


    }

    View Slide

  28. @Composable


    fun StarWarsPeopleList(


    viewModel: PeopleListViewModel


    ) {


    val state by viewModel.uiState.observeAsState()


    StarWarsPeopleList(state)


    }

    View Slide

  29. @Composable


    fun StarWarsPeopleList(


    viewModel: PeopleListViewModel


    ) {


    val state by viewModel.uiState.observeAsState()


    StarWarsPeopleList(state)


    }

    View Slide

  30. @Composable


    fun StarWarsPeopleList(


    viewModel: PeopleListViewModel


    ) {


    val state by viewModel.uiState.observeAsState()


    StarWarsPeopleList(state)


    }

    View Slide

  31. @Composable


    fun Details(


    viewModel: DetailsViewModel


    ) {


    val state by viewModel.uiState.observeAsState()


    DetailsContent(state)


    }


    @HiltViewModel


    class DetailsViewModel @Inject constructor(


    private val savedStateHandle: SavedStateHandle,


    ...


    ) : ViewModel()

    View Slide

  32. @Composable


    fun Details(


    viewModel: DetailsViewModel


    ) {


    val state by viewModel.uiState.observeAsState()


    DetailsContent(state)


    }


    @HiltViewModel


    class DetailsViewModel @Inject constructor(


    private val savedStateHandle: SavedStateHandle,


    ...


    ) : ViewModel()

    View Slide

  33. interface NavigationCommand {


    val arguments: List


    val destination: String


    }

    View Slide

  34. interface NavigationCommand {


    val arguments: List


    val destination: String


    }

    View Slide

  35. interface NavigationCommand {


    val arguments: List


    val destination: String


    }

    View Slide

  36. object NavigationDirections {


    val list = object : NavigationCommand {


    override val arguments = emptyList()


    override val destination = "peoplelist"


    }


    val details = object : NavigationCommand {


    override val arguments = emptyList()


    override val destination = "details"


    }


    }

    View Slide

  37. object NavigationDirections {


    val list = object : NavigationCommand {


    override val arguments = emptyList()


    override val destination = "peoplelist"


    }


    val details = object : NavigationCommand {


    override val arguments = emptyList()


    override val destination = "details"


    }


    }

    View Slide

  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"


    }


    }

    View Slide

  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"


    }


    }

    View Slide

  40. Se
    tt
    ing up

    Navigation Graph

    View Slide

  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

    View Slide

  42. val navController = rememberNavController()

    View Slide

  43. NavHost(


    navController,


    startDestination =
    NavigationDirections.Authentication.destination


    ) {


    }

    View Slide

  44. Se
    tt
    ing up

    Navigation Destinations

    View Slide

  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

    View Slide

  46. composable(NavigationDirections.List.destination) {


    StarWarsPeopleList()


    }

    View Slide

  47. composable(NavigationDirections.Details.destination) {


    Details()


    }

    View Slide

  48. Providing ViewModels

    to Composables

    View Slide

  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

    View Slide

  50. androidx.hilt:hilt-navigation-compose

    View Slide

  51. StarWarsPeopleList(


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


    )

    View Slide

  52. StarWarsPeopleList(


    hiltNavGraphViewModel()

    )

    View Slide

  53. Handling Navigation

    Events

    View Slide

  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

    View Slide

  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

    View Slide

  56. class NavigationManager {


    var commands = MutableStateFlow(Default)


    fun navigate(


    directions: NavigationCommand


    ) {


    commands.value = directions


    }


    }

    View Slide

  57. class NavigationManager {


    var commands = MutableStateFlow(Default)


    fun navigate(


    directions: NavigationCommand


    ) {


    commands.value = directions


    }


    }

    View Slide

  58. class NavigationManager {


    var commands = MutableStateFlow(Default)


    fun navigate(


    directions: NavigationCommand


    ) {


    commands.value = directions


    }


    }

    View Slide

  59. @Module


    @InstallIn(SingletonComponent::class)


    class AppModule {


    @Singleton


    @Provides


    fun providesNavigationManager() = NavigationManager()


    }

    View Slide

  60. @Module


    @InstallIn(SingletonComponent::class)


    class AppModule {


    @Singleton


    @Provides


    fun providesNavigationManager() = NavigationManager()


    }

    View Slide

  61. @Inject


    lateinit var navigationManager: NavigationManager


    navigationManager.commands.collectAsState().value.also { command ->


    if (command.destination.isNotEmpty())
    navController.navigate(command.destination)


    }

    View Slide

  62. @Inject


    lateinit var navigationManager: NavigationManager


    navigationManager.commands.collectAsState().value.also { command ->


    if (command.destination.isNotEmpty())
    navController.navigate(command.destination)


    }

    View Slide

  63. Triggering Navigation

    Events

    View Slide

  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

    View Slide

  65. @HiltViewModel


    class PeopleListViewModel @Inject constructor(


    private val savedStateHandle: SavedStateHandle,


    private val results: Results,


    private val sharedPrefs: Preferences,


    private val navigationManager: NavigationManager


    )

    View Slide

  66. @HiltViewModel


    class PeopleListViewModel @Inject constructor(


    private val savedStateHandle: SavedStateHandle,


    private val results: Results,


    private val sharedPrefs: Preferences,


    private val navigationManager: NavigationManager


    )

    View Slide

  67. navigationManager.navigate(NavigationDirections.Details)

    View Slide

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

    View Slide

  69. Recap

    Navigation
    Source :

    www.joebirch.co

    View Slide

  70. We’ve removed the need to pass around
    a NavHostController to our composables
    Source :

    www.joebirch.co

    View Slide

  71. Hilt ViewModels in Composables but w/o
    any direct deps in feature modules
    Source :

    www.joebirch.co

    View Slide

  72. We’ve centralised our navigation logic
    and created a contract for the things
    which can be triggered
    Source :

    www.joebirch.co

    View Slide

  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.


    View Slide

  74. Thats all folks!
    https://cal.com/adit/30min
    🎯@aditlal
    🔗aditlal.dev

    View Slide