Slide 1

Slide 1 text

Navigating with Jetpack Compose in a multi-modular codebase Google Developers Adit Lal 
 Individual Consultant

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

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

Slide 4

Slide 4 text

Jetpack Compose 2021

Slide 5

Slide 5 text

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

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

Require a NavHostController reference in Composable functions

Slide 9

Slide 9 text

Difficult to test navigation logic

Slide 10

Slide 10 text

Adds friction to Compose Migration

Slide 11

Slide 11 text

Coupled to the Navigation Dependencies

Slide 12

Slide 12 text

Multi-Modular Navigation ?

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

Hilt Navigation Compose Activity Compose Nav Controller App module

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

@Composable private fun StarWarsPeopleList( viewState: PeopleListState )

Slide 21

Slide 21 text

@Composable private fun StarWarsPeopleList( viewState: PeopleListState )

Slide 22

Slide 22 text

@Composable private fun StarWarsPeopleList( viewState: PeopleListState )

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

interface NavigationCommand { val arguments: List val destination: String }

Slide 34

Slide 34 text

interface NavigationCommand { val arguments: List val destination: String }

Slide 35

Slide 35 text

interface NavigationCommand { val arguments: List val destination: String }

Slide 36

Slide 36 text

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" } }

Slide 37

Slide 37 text

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" } }

Slide 38

Slide 38 text

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" } }

Slide 39

Slide 39 text

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" } }

Slide 40

Slide 40 text

Se tt ing up 
 Navigation Graph

Slide 41

Slide 41 text

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

Slide 42

Slide 42 text

val navController = rememberNavController()

Slide 43

Slide 43 text

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

Slide 44

Slide 44 text

Se tt ing up 
 Navigation Destinations

Slide 45

Slide 45 text

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

Slide 46

Slide 46 text

composable(NavigationDirections.List.destination) { StarWarsPeopleList() }

Slide 47

Slide 47 text

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

Slide 48

Slide 48 text

Providing ViewModels 
 to Composables

Slide 49

Slide 49 text

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

Slide 50

Slide 50 text

androidx.hilt:hilt-navigation-compose

Slide 51

Slide 51 text

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

Slide 52

Slide 52 text

StarWarsPeopleList( hiltNavGraphViewModel() 
 )

Slide 53

Slide 53 text

Handling Navigation 
 Events

Slide 54

Slide 54 text

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

Slide 55

Slide 55 text

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

Slide 56

Slide 56 text

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

Slide 57

Slide 57 text

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

Slide 58

Slide 58 text

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

Slide 59

Slide 59 text

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

Slide 60

Slide 60 text

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

Slide 61

Slide 61 text

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

Slide 62

Slide 62 text

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

Slide 63

Slide 63 text

Triggering Navigation 
 Events

Slide 64

Slide 64 text

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

Slide 65

Slide 65 text

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

Slide 66

Slide 66 text

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

Slide 67

Slide 67 text

navigationManager.navigate(NavigationDirections.Details)

Slide 68

Slide 68 text

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

Slide 69

Slide 69 text

Recap 
 Navigation Source : 
 www.joebirch.co

Slide 70

Slide 70 text

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

Slide 71

Slide 71 text

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

Slide 72

Slide 72 text

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

Slide 73

Slide 73 text

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.

Slide 74

Slide 74 text

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