Skip to content

Commit c52e1b5

Browse files
committed
refactor: use Navigation type safety
1 parent 9162ac7 commit c52e1b5

File tree

19 files changed

+148
-259
lines changed

19 files changed

+148
-259
lines changed

dataconnect/app/build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ dependencies {
5959

6060
implementation(libs.androidx.core.ktx)
6161
implementation(libs.androidx.lifecycle.runtime.ktx)
62-
implementation(libs.androidx.lifecycle.viewmodel.android)
62+
implementation(libs.androidx.lifecycle.viewmodel.compose)
6363
implementation(libs.androidx.activity.compose)
6464
implementation(platform(libs.androidx.compose.bom))
6565
implementation(libs.androidx.ui)

dataconnect/app/src/main/java/com/google/firebase/example/dataconnect/MainActivity.kt

Lines changed: 73 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -11,44 +11,46 @@ import androidx.compose.material.icons.Icons
1111
import androidx.compose.material.icons.filled.Home
1212
import androidx.compose.material.icons.filled.Menu
1313
import androidx.compose.material.icons.filled.Person
14-
import androidx.compose.material.icons.filled.Search
1514
import androidx.compose.material3.Icon
1615
import androidx.compose.material3.NavigationBar
1716
import androidx.compose.material3.NavigationBarItem
1817
import androidx.compose.material3.Scaffold
1918
import androidx.compose.material3.Text
2019
import androidx.compose.runtime.getValue
2120
import androidx.compose.ui.Modifier
21+
import androidx.compose.ui.graphics.vector.ImageVector
2222
import androidx.compose.ui.res.stringResource
23-
import androidx.compose.ui.unit.dp
24-
import androidx.navigation.NavDestination
23+
import androidx.navigation.NavDestination.Companion.hasRoute
2524
import androidx.navigation.NavDestination.Companion.hierarchy
26-
import androidx.navigation.NavGraph.Companion.findStartDestination
2725
import androidx.navigation.compose.NavHost
26+
import androidx.navigation.compose.composable
2827
import androidx.navigation.compose.currentBackStackEntryAsState
2928
import androidx.navigation.compose.rememberNavController
3029
import com.google.firebase.dataconnect.movies.MoviesConnector
3130
import com.google.firebase.dataconnect.movies.instance
32-
import com.google.firebase.example.dataconnect.feature.actordetail.actorDetailScreen
33-
import com.google.firebase.example.dataconnect.feature.actordetail.navigateToActorDetail
34-
import com.google.firebase.example.dataconnect.feature.genredetail.genreDetailScreen
35-
import com.google.firebase.example.dataconnect.feature.genredetail.navigateToGenreDetail
36-
import com.google.firebase.example.dataconnect.feature.genres.GENRES_ROUTE
37-
import com.google.firebase.example.dataconnect.feature.genres.genresScreen
38-
import com.google.firebase.example.dataconnect.feature.genres.navigateToGenres
39-
import com.google.firebase.example.dataconnect.feature.moviedetail.movieDetailScreen
40-
import com.google.firebase.example.dataconnect.feature.moviedetail.navigateToMovieDetail
41-
import com.google.firebase.example.dataconnect.feature.movies.MOVIES_ROUTE
42-
import com.google.firebase.example.dataconnect.feature.movies.moviesScreen
43-
import com.google.firebase.example.dataconnect.feature.movies.navigateToMovies
44-
import com.google.firebase.example.dataconnect.feature.profile.PROFILE_ROUTE
45-
import com.google.firebase.example.dataconnect.feature.profile.navigateToProfile
46-
import com.google.firebase.example.dataconnect.feature.profile.profileScreen
47-
import com.google.firebase.example.dataconnect.feature.search.SEARCH_ROUTE
48-
import com.google.firebase.example.dataconnect.feature.search.navigateToSearch
31+
import com.google.firebase.example.dataconnect.feature.actordetail.ActorDetailRoute
32+
import com.google.firebase.example.dataconnect.feature.actordetail.ActorDetailScreen
33+
import com.google.firebase.example.dataconnect.feature.genredetail.GenreDetailRoute
34+
import com.google.firebase.example.dataconnect.feature.genredetail.GenreDetailScreen
35+
import com.google.firebase.example.dataconnect.feature.genres.GenresRoute
36+
import com.google.firebase.example.dataconnect.feature.genres.GenresScreen
37+
import com.google.firebase.example.dataconnect.feature.moviedetail.MovieDetailRoute
38+
import com.google.firebase.example.dataconnect.feature.moviedetail.MovieDetailScreen
39+
import com.google.firebase.example.dataconnect.feature.movies.MoviesRoute
40+
import com.google.firebase.example.dataconnect.feature.movies.MoviesScreen
41+
import com.google.firebase.example.dataconnect.feature.profile.ProfileRoute
42+
import com.google.firebase.example.dataconnect.feature.profile.ProfileScreen
4943
import com.google.firebase.example.dataconnect.feature.search.searchScreen
5044
import com.google.firebase.example.dataconnect.ui.theme.FirebaseDataConnectTheme
5145

46+
data class TopLevelRoute<T : Any>(val labelResId: Int, val route: T, val icon: ImageVector)
47+
48+
val TOP_LEVEL_ROUTES = listOf(
49+
TopLevelRoute(R.string.label_movies, MoviesRoute, Icons.Filled.Home),
50+
TopLevelRoute(R.string.label_genres, GenresRoute, Icons.Filled.Menu),
51+
TopLevelRoute(R.string.label_profile, ProfileRoute, Icons.Filled.Person)
52+
)
53+
5254
class MainActivity : ComponentActivity() {
5355
override fun onCreate(savedInstanceState: Bundle?) {
5456
super.onCreate(savedInstanceState)
@@ -64,75 +66,70 @@ class MainActivity : ComponentActivity() {
6466
NavigationBar {
6567
val navBackStackEntry by navController.currentBackStackEntryAsState()
6668
val currentDestination = navBackStackEntry?.destination
67-
NavigationBarItem(
68-
icon = { Icon(Icons.Filled.Home, contentDescription = null) },
69-
label = { Text(stringResource(R.string.label_movies)) },
70-
selected = isRouteSelected(currentDestination, MOVIES_ROUTE),
71-
onClick = {
72-
navController.navigateToMovies { launchSingleTop = true }
73-
}
74-
)
75-
NavigationBarItem(
76-
icon = { Icon(Icons.Filled.Menu, contentDescription = null) },
77-
label = { Text(stringResource(R.string.label_genres)) },
78-
selected = isRouteSelected(currentDestination, GENRES_ROUTE),
79-
onClick = {
80-
navController.navigateToGenres { launchSingleTop = true }
81-
}
82-
)
83-
NavigationBarItem(
84-
icon = { Icon(Icons.Filled.Search, contentDescription = null) },
85-
label = { Text(stringResource(R.string.label_search)) },
86-
selected = isRouteSelected(currentDestination, SEARCH_ROUTE),
87-
onClick = {
88-
navController.navigateToSearch { launchSingleTop = true }
89-
}
90-
)
91-
NavigationBarItem(
92-
icon = { Icon(Icons.Filled.Person, contentDescription = null) },
93-
label = { Text(stringResource(R.string.label_profile)) },
94-
selected = isRouteSelected(currentDestination, PROFILE_ROUTE),
95-
onClick = {
96-
navController.navigateToProfile { launchSingleTop = true }
97-
}
98-
)
69+
70+
TOP_LEVEL_ROUTES.forEach { topLevelRoute ->
71+
val label = stringResource(topLevelRoute.labelResId)
72+
NavigationBarItem(
73+
icon = { Icon(topLevelRoute.icon, contentDescription = label) },
74+
label = { Text(label) },
75+
selected = currentDestination?.hierarchy?.any {
76+
it.hasRoute(topLevelRoute.route::class)
77+
} == true,
78+
onClick = {
79+
navController.navigate(
80+
topLevelRoute.route,
81+
{ launchSingleTop = true }
82+
)
83+
}
84+
)
85+
}
9986
}
10087
}
10188
) { innerPadding ->
10289
NavHost(
10390
navController,
104-
startDestination = MOVIES_ROUTE,
91+
startDestination = MoviesRoute,
10592
Modifier
10693
.padding(innerPadding)
10794
.consumeWindowInsets(innerPadding),
10895
) {
109-
moviesScreen(onMovieClicked = { movieId ->
110-
navController.navigateToMovieDetail(movieId) {
111-
launchSingleTop = true
112-
}
113-
})
114-
movieDetailScreen(
115-
onActorClicked = { actorId ->
116-
navController.navigateToActorDetail(actorId) {
117-
launchSingleTop = true
96+
composable<MoviesRoute>() {
97+
MoviesScreen(
98+
onMovieClicked = { movieId ->
99+
navController.navigate(
100+
route = MovieDetailRoute(movieId),
101+
builder = {
102+
launchSingleTop = true
103+
}
104+
)
118105
}
119-
}
120-
)
121-
actorDetailScreen()
122-
genresScreen(onGenreClicked = { genre ->
123-
navController.navigateToGenreDetail(genre) {
124-
launchSingleTop = true
125-
}
126-
})
127-
genreDetailScreen()
106+
)
107+
}
108+
composable<MovieDetailRoute> {
109+
MovieDetailScreen(
110+
onActorClicked = { actorId ->
111+
navController.navigate(
112+
ActorDetailRoute(actorId),
113+
{ launchSingleTop = true }
114+
)
115+
}
116+
)
117+
}
118+
composable<ActorDetailRoute>() { ActorDetailScreen() }
119+
composable<GenresRoute> {
120+
GenresScreen(onGenreClicked = { genre ->
121+
navController.navigate(
122+
GenreDetailRoute(genre),
123+
{ launchSingleTop = true }
124+
)
125+
})
126+
}
127+
composable<GenreDetailRoute> { GenreDetailScreen() }
128128
searchScreen()
129-
profileScreen()
129+
composable<ProfileRoute> { ProfileScreen() }
130130
}
131131
}
132132
}
133133
}
134134
}
135135
}
136-
137-
private fun isRouteSelected(currentDestination: NavDestination?, route: String) =
138-
currentDestination?.hierarchy?.any { it.route?.startsWith(route) ?: false } == true

dataconnect/app/src/main/java/com/google/firebase/example/dataconnect/feature/actordetail/ActorDetailScreen.kt

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,16 @@ import com.google.firebase.example.dataconnect.ui.components.LoadingScreen
3232
import com.google.firebase.example.dataconnect.ui.components.Movie
3333
import com.google.firebase.example.dataconnect.ui.components.MoviesList
3434
import com.google.firebase.example.dataconnect.ui.components.ToggleButton
35+
import kotlinx.serialization.Serializable
36+
37+
38+
@Serializable
39+
data class ActorDetailRoute(val actorId: String)
3540

3641
@Composable
3742
fun ActorDetailScreen(
38-
actorId: String,
3943
actorDetailViewModel: ActorDetailViewModel = viewModel()
4044
) {
41-
actorDetailViewModel.setActorId(actorId)
4245
val uiState by actorDetailViewModel.uiState.collectAsState()
4346
ActorDetailScreen(
4447
uiState = uiState,

dataconnect/app/src/main/java/com/google/firebase/example/dataconnect/feature/actordetail/ActorDetailViewModel.kt

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
package com.google.firebase.example.dataconnect.feature.actordetail
22

3+
import androidx.lifecycle.SavedStateHandle
34
import androidx.lifecycle.ViewModel
45
import androidx.lifecycle.viewModelScope
6+
import androidx.navigation.toRoute
57
import com.google.firebase.Firebase
68
import com.google.firebase.auth.FirebaseAuth
79
import com.google.firebase.auth.auth
@@ -14,17 +16,23 @@ import kotlinx.coroutines.flow.StateFlow
1416
import kotlinx.coroutines.launch
1517

1618
class ActorDetailViewModel(
17-
private val firebaseAuth: FirebaseAuth = Firebase.auth,
18-
private val moviesConnector: MoviesConnector = MoviesConnector.instance
19+
savedStateHandle: SavedStateHandle
1920
) : ViewModel() {
20-
private var actorId: String = ""
21+
private val actorDetailRoute = savedStateHandle.toRoute<ActorDetailRoute>()
22+
private val actorId: String = actorDetailRoute.actorId
23+
24+
private val firebaseAuth: FirebaseAuth = Firebase.auth
25+
private val moviesConnector: MoviesConnector = MoviesConnector.instance
2126

2227
private val _uiState = MutableStateFlow<ActorDetailUIState>(ActorDetailUIState.Loading)
2328
val uiState: StateFlow<ActorDetailUIState>
2429
get() = _uiState
2530

26-
fun setActorId(id: String) {
27-
actorId = id
31+
init {
32+
fetchActor()
33+
}
34+
35+
private fun fetchActor() {
2836
viewModelScope.launch {
2937
try {
3038
val user = firebaseAuth.currentUser
@@ -64,7 +72,7 @@ class ActorDetailViewModel(
6472
)
6573
}
6674
// Re-run the query to fetch the actor details
67-
setActorId(actorId)
75+
fetchActor()
6876
} catch (e: Exception) {
6977
_uiState.value = ActorDetailUIState.Error(e.message ?: "")
7078
}

dataconnect/app/src/main/java/com/google/firebase/example/dataconnect/feature/actordetail/Navigation.kt

Lines changed: 0 additions & 26 deletions
This file was deleted.

dataconnect/app/src/main/java/com/google/firebase/example/dataconnect/feature/genredetail/GenreDetailScreen.kt

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,15 @@ import com.google.firebase.example.dataconnect.ui.components.ErrorCard
1818
import com.google.firebase.example.dataconnect.ui.components.LoadingScreen
1919
import com.google.firebase.example.dataconnect.ui.components.Movie
2020
import com.google.firebase.example.dataconnect.ui.components.MoviesList
21+
import kotlinx.serialization.Serializable
22+
23+
@Serializable
24+
data class GenreDetailRoute(val genre: String)
2125

2226
@Composable
2327
fun GenreDetailScreen(
24-
genre: String,
2528
moviesViewModel: GenreDetailViewModel = viewModel()
2629
) {
27-
moviesViewModel.setGenre(genre)
2830
val movies by moviesViewModel.uiState.collectAsState()
2931
GenreDetailScreen(movies)
3032
}

dataconnect/app/src/main/java/com/google/firebase/example/dataconnect/feature/genredetail/GenreDetailViewModel.kt

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
package com.google.firebase.example.dataconnect.feature.genredetail
22

3+
import androidx.lifecycle.SavedStateHandle
34
import androidx.lifecycle.ViewModel
45
import androidx.lifecycle.viewModelScope
6+
import androidx.navigation.toRoute
57
import com.google.firebase.dataconnect.movies.MoviesConnector
68
import com.google.firebase.dataconnect.movies.execute
79
import com.google.firebase.dataconnect.movies.instance
@@ -10,17 +12,20 @@ import kotlinx.coroutines.flow.StateFlow
1012
import kotlinx.coroutines.launch
1113

1214
class GenreDetailViewModel(
13-
private val moviesConnector: MoviesConnector = MoviesConnector.instance
15+
savedStateHandle: SavedStateHandle
1416
) : ViewModel() {
15-
private var genre = ""
17+
private val genre = savedStateHandle.toRoute<GenreDetailRoute>().genre
18+
private val moviesConnector: MoviesConnector = MoviesConnector.instance
1619

1720
private val _uiState = MutableStateFlow<GenreDetailUIState>(GenreDetailUIState.Loading)
1821
val uiState: StateFlow<GenreDetailUIState>
1922
get() = _uiState
2023

21-
// TODO(thatfiredev): Create a ViewModelFactory to set genre
22-
fun setGenre(genre: String) {
23-
this.genre = genre
24+
init {
25+
fetchGenre()
26+
}
27+
28+
private fun fetchGenre() {
2429
viewModelScope.launch {
2530
try {
2631
val data = moviesConnector.listMoviesByGenre.execute(genre.lowercase()).data

dataconnect/app/src/main/java/com/google/firebase/example/dataconnect/feature/genredetail/Navigation.kt

Lines changed: 0 additions & 27 deletions
This file was deleted.

dataconnect/app/src/main/java/com/google/firebase/example/dataconnect/feature/genres/GenresScreen.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,10 @@ import androidx.compose.material3.Text
1111
import androidx.compose.runtime.Composable
1212
import androidx.compose.ui.Modifier
1313
import androidx.compose.ui.unit.dp
14+
import kotlinx.serialization.Serializable
1415

16+
@Serializable
17+
object GenresRoute
1518

1619
@Composable
1720
fun GenresScreen(

0 commit comments

Comments
 (0)