Skip to content

Commit fe78600

Browse files
authored
Change View Model Scoping (#41)
* Fix main view model scoping * Bump material version in preparation for pull to refresh * Add documentation * Revert "Bump material version in preparation for pull to refresh" This reverts commit 15ad1d7. * Resolve merge conflicts * oopsies
1 parent 63ae483 commit fe78600

6 files changed

Lines changed: 134 additions & 88 deletions

File tree

app/src/main/java/com/cornellappdev/score/MainActivity.kt

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,14 @@
11
package com.cornellappdev.score
22

3-
import android.os.Build
43
import android.os.Bundle
54
import androidx.activity.compose.setContent
65
import androidx.activity.enableEdgeToEdge
7-
import androidx.annotation.RequiresApi
86
import androidx.appcompat.app.AppCompatActivity
97
import com.cornellappdev.score.nav.root.RootNavigation
108
import dagger.hilt.android.AndroidEntryPoint
119

1210
@AndroidEntryPoint
1311
class MainActivity : AppCompatActivity() {
14-
@RequiresApi(Build.VERSION_CODES.O)
1512
override fun onCreate(savedInstanceState: Bundle?) {
1613
super.onCreate(savedInstanceState)
1714
enableEdgeToEdge()
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package com.cornellappdev.score.nav
2+
3+
import androidx.compose.runtime.Composable
4+
import androidx.compose.runtime.CompositionLocalProvider
5+
import androidx.lifecycle.viewmodel.compose.LocalViewModelStoreOwner
6+
import androidx.navigation.NavHostController
7+
import androidx.navigation.compose.NavHost
8+
import androidx.navigation.compose.composable
9+
import com.cornellappdev.score.nav.root.ScoreScreens
10+
import com.cornellappdev.score.nav.root.ScoreScreens.Home
11+
import com.cornellappdev.score.screen.GameDetailsScreen
12+
import com.cornellappdev.score.screen.HomeScreen
13+
import com.cornellappdev.score.screen.PastGamesScreen
14+
15+
@Composable
16+
fun ScoreNavHost(navController: NavHostController) {
17+
// This ViewModelStoreOwner is used to scope the past and home screen view models to the root
18+
// screen instead of their individual tabs. This way the view models are not reconstructed
19+
// everytime you switch tabs.
20+
val mainScreenViewModelStoreOwner = checkNotNull(LocalViewModelStoreOwner.current)
21+
22+
NavHost(
23+
navController = navController,
24+
startDestination = Home
25+
) {
26+
composable<Home> {
27+
CompositionLocalProvider(LocalViewModelStoreOwner provides mainScreenViewModelStoreOwner) {
28+
HomeScreen(navigateToGameDetails = {
29+
navController.navigate(ScoreScreens.GameDetailsPage(""))
30+
})
31+
}
32+
}
33+
composable<ScoreScreens.ScoresScreen> {
34+
CompositionLocalProvider(LocalViewModelStoreOwner provides mainScreenViewModelStoreOwner) {
35+
PastGamesScreen(navigateToGameDetails = {
36+
navController.navigate(ScoreScreens.GameDetailsPage(""))
37+
})
38+
}
39+
}
40+
composable<ScoreScreens.GameDetailsPage> {
41+
GameDetailsScreen(onBackArrow = {
42+
navController.navigateUp()
43+
})
44+
}
45+
}
46+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package com.cornellappdev.score.nav
2+
3+
import androidx.compose.material3.Icon
4+
import androidx.compose.material3.NavigationBar
5+
import androidx.compose.material3.NavigationBarItem
6+
import androidx.compose.material3.Text
7+
import androidx.compose.runtime.Composable
8+
import androidx.compose.ui.Modifier
9+
import androidx.compose.ui.graphics.Color
10+
import androidx.compose.ui.res.painterResource
11+
import androidx.compose.ui.tooling.preview.Preview
12+
import androidx.navigation.NavBackStackEntry
13+
import com.cornellappdev.score.nav.root.ScoreScreens
14+
import com.cornellappdev.score.nav.root.tabs
15+
import com.cornellappdev.score.nav.root.toScreen
16+
import com.cornellappdev.score.theme.CrimsonPrimary
17+
import com.cornellappdev.score.theme.GrayPrimary
18+
import com.cornellappdev.score.theme.Style.bodyMedium
19+
import com.cornellappdev.score.theme.White
20+
21+
@Composable
22+
fun ScoreNavigationBar(
23+
navigateToScreen: (ScoreScreens) -> Unit,
24+
navBackStackEntry: NavBackStackEntry?,
25+
modifier: Modifier = Modifier,
26+
) {
27+
NavigationBar(modifier = modifier, containerColor = White) {
28+
tabs.map { item ->
29+
val isSelected = item.screen == navBackStackEntry?.toScreen()
30+
31+
NavigationBarItem(
32+
selected = isSelected,
33+
onClick = { navigateToScreen(item.screen) },
34+
icon = {
35+
Icon(
36+
painter = painterResource(id = if (isSelected) item.selectedIcon else item.unselectedIcon),
37+
contentDescription = null,
38+
tint = Color.Unspecified
39+
)
40+
},
41+
label = {
42+
Text(
43+
text = item.label,
44+
style = bodyMedium,
45+
color = if (isSelected) {
46+
CrimsonPrimary
47+
} else {
48+
GrayPrimary
49+
}
50+
)
51+
}
52+
)
53+
}
54+
}
55+
}
56+
57+
@Preview
58+
@Composable
59+
private fun ScoreNavigationBarPreview() {
60+
ScoreNavigationBar({}, null)
61+
}

app/src/main/java/com/cornellappdev/score/nav/root/RootNavigation.kt

Lines changed: 24 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -7,34 +7,23 @@ import androidx.compose.animation.core.rememberInfiniteTransition
77
import androidx.compose.foundation.layout.Box
88
import androidx.compose.foundation.layout.fillMaxSize
99
import androidx.compose.foundation.layout.padding
10-
import androidx.compose.material3.Icon
11-
import androidx.compose.material3.NavigationBar
12-
import androidx.compose.material3.NavigationBarItem
1310
import androidx.compose.material3.Scaffold
14-
import androidx.compose.material3.Text
1511
import androidx.compose.runtime.Composable
1612
import androidx.compose.runtime.CompositionLocalProvider
1713
import androidx.compose.runtime.LaunchedEffect
1814
import androidx.compose.ui.Modifier
19-
import androidx.compose.ui.graphics.Color
20-
import androidx.compose.ui.res.painterResource
2115
import androidx.hilt.navigation.compose.hiltViewModel
2216
import androidx.navigation.NavBackStackEntry
23-
import androidx.navigation.compose.NavHost
24-
import androidx.navigation.compose.composable
2517
import androidx.navigation.compose.currentBackStackEntryAsState
2618
import androidx.navigation.compose.rememberNavController
2719
import androidx.navigation.toRoute
2820
import com.cornellappdev.score.R
29-
import com.cornellappdev.score.nav.root.ScoreRootScreens.Home.toScreen
30-
import com.cornellappdev.score.screen.GameDetailsScreen
31-
import com.cornellappdev.score.screen.HomeScreen
32-
import com.cornellappdev.score.screen.PastGamesScreen
33-
import com.cornellappdev.score.theme.CrimsonPrimary
34-
import com.cornellappdev.score.theme.GrayPrimary
21+
import com.cornellappdev.score.nav.ScoreNavHost
22+
import com.cornellappdev.score.nav.ScoreNavigationBar
23+
import com.cornellappdev.score.nav.root.ScoreScreens.GameDetailsPage
24+
import com.cornellappdev.score.nav.root.ScoreScreens.Home
25+
import com.cornellappdev.score.nav.root.ScoreScreens.ScoresScreen
3526
import com.cornellappdev.score.theme.LocalInfiniteLoading
36-
import com.cornellappdev.score.theme.Style.bodyMedium
37-
import com.cornellappdev.score.theme.White
3827
import kotlinx.serialization.Serializable
3928

4029
@Composable
@@ -67,93 +56,46 @@ fun RootNavigation(
6756
}
6857
}
6958

59+
7060
Scaffold(modifier = Modifier.fillMaxSize(), bottomBar = {
71-
if (navBackStackEntry?.toScreen() is ScoreRootScreens.GameDetailsPage) {
61+
if (navBackStackEntry?.toScreen() is GameDetailsPage) {
7262
return@Scaffold
7363
}
74-
NavigationBar(containerColor = White) {
75-
tabs.map { item ->
76-
val isSelected = item.screen == navBackStackEntry?.toScreen()
77-
78-
NavigationBarItem(
79-
selected = isSelected,
80-
onClick = { navController.navigate(item.screen) },
81-
icon = {
82-
Icon(
83-
painter = painterResource(id = if (isSelected) item.selectedIcon else item.unselectedIcon),
84-
contentDescription = null,
85-
tint = Color.Unspecified
86-
)
87-
},
88-
label = {
89-
Text(
90-
text = item.label,
91-
style = bodyMedium,
92-
color = if (isSelected) {
93-
CrimsonPrimary
94-
} else {
95-
GrayPrimary
96-
}
97-
)
98-
}
99-
)
100-
}
101-
}
64+
ScoreNavigationBar({ navController.navigate(it) }, navBackStackEntry)
10265
}
10366
) { innerPadding ->
10467
Box(modifier = Modifier.padding(innerPadding)) {
10568
CompositionLocalProvider(LocalInfiniteLoading provides animatedValue) {
106-
NavHost(
107-
navController = navController,
108-
startDestination = ScoreRootScreens.Home
109-
) {
110-
composable<ScoreRootScreens.Home> {
111-
HomeScreen(navigateToGameDetails = {
112-
navController.navigate(ScoreRootScreens.GameDetailsPage(it))
113-
})
114-
}
115-
116-
composable<ScoreRootScreens.GameDetailsPage> {
117-
GameDetailsScreen(onBackArrow = {
118-
navController.navigateUp()
119-
})
12069

121-
}
122-
123-
composable<ScoreRootScreens.ScoresScreen> {
124-
PastGamesScreen(navigateToGameDetails = {
125-
navController.navigate(ScoreRootScreens.GameDetailsPage(it))
126-
})
127-
}
128-
}
70+
ScoreNavHost(navController)
12971
}
13072
}
13173
}
13274
}
13375

13476

13577
@Serializable
136-
sealed class ScoreRootScreens {
78+
sealed class ScoreScreens {
13779
@Serializable
138-
data object Home : ScoreRootScreens()
80+
data object Home : ScoreScreens()
13981

14082
@Serializable
141-
data class GameDetailsPage(val gameId: String) : ScoreRootScreens()
83+
data class GameDetailsPage(val gameId: String) : ScoreScreens()
14284

14385
@Serializable
144-
data object ScoresScreen : ScoreRootScreens()
145-
146-
fun NavBackStackEntry.toScreen(): ScoreRootScreens? =
147-
when (destination.route?.substringAfterLast(".")?.substringBefore("/")) {
148-
"Home" -> toRoute<Home>()
149-
"GameDetailsPage" -> toRoute<GameDetailsPage>()
150-
"ScoresScreen" -> toRoute<ScoresScreen>()
151-
else -> throw IllegalArgumentException("Invalid screen")
152-
}
86+
data object ScoresScreen : ScoreScreens()
15387
}
15488

89+
fun NavBackStackEntry.toScreen(): ScoreScreens? =
90+
when (destination.route?.substringAfterLast(".")?.substringBefore("/")) {
91+
"Home" -> toRoute<Home>()
92+
"GameDetailsPage" -> toRoute<GameDetailsPage>()
93+
"ScoresScreen" -> toRoute<ScoresScreen>()
94+
else -> throw IllegalArgumentException("Invalid screen")
95+
}
96+
15597
data class NavItem(
156-
val screen: ScoreRootScreens,
98+
val screen: ScoreScreens,
15799
val label: String,
158100
val unselectedIcon: Int,
159101
val selectedIcon: Int
@@ -164,12 +106,12 @@ val tabs = listOf(
164106
label = "Schedule",
165107
unselectedIcon = R.drawable.ic_schedule,
166108
selectedIcon = R.drawable.ic_schedule_filled,
167-
screen = ScoreRootScreens.Home,
109+
screen = ScoreScreens.Home,
168110
),
169111
NavItem(
170112
label = "Scores",
171113
unselectedIcon = R.drawable.ic_scores,
172114
selectedIcon = R.drawable.ic_scores_filled,
173-
screen = ScoreRootScreens.ScoresScreen,
115+
screen = ScoreScreens.ScoresScreen,
174116
),
175117
)

app/src/main/java/com/cornellappdev/score/nav/root/RootNavigationRepository.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,4 @@ import javax.inject.Inject
55
import javax.inject.Singleton
66

77
@Singleton
8-
class RootNavigationRepository @Inject constructor() : BaseNavigationRepository<ScoreRootScreens>()
8+
class RootNavigationRepository @Inject constructor() : BaseNavigationRepository<ScoreScreens>()

app/src/main/java/com/cornellappdev/score/nav/root/RootNavigationViewModel.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
package com.cornellappdev.score.nav.root
22

3-
import com.cornellappdev.score.viewmodel.BaseViewModel
43
import com.cornellappdev.score.util.UIEvent
4+
import com.cornellappdev.score.viewmodel.BaseViewModel
55
import dagger.hilt.android.lifecycle.HiltViewModel
66
import javax.inject.Inject
77

@@ -13,7 +13,7 @@ class RootNavigationViewModel @Inject constructor(
1313
) {
1414

1515
data class RootNavigationUiState(
16-
val navigationEvent: UIEvent<ScoreRootScreens>? = null,
16+
val navigationEvent: UIEvent<ScoreScreens>? = null,
1717
)
1818

1919
init {

0 commit comments

Comments
 (0)