Skip to content

Commit 828242d

Browse files
committed
Remove Scaffold from top level screens
1 parent 3a10653 commit 828242d

File tree

11 files changed

+246
-166
lines changed

11 files changed

+246
-166
lines changed

app/src/androidTest/java/com/google/samples/apps/nowinandroid/ui/NavigationTest.kt

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import androidx.compose.ui.test.assertIsOn
2121
import androidx.compose.ui.test.assertIsSelected
2222
import androidx.compose.ui.test.junit4.createAndroidComposeRule
2323
import androidx.compose.ui.test.onAllNodesWithText
24+
import androidx.compose.ui.test.onLast
2425
import androidx.compose.ui.test.onNodeWithContentDescription
2526
import androidx.compose.ui.test.onNodeWithText
2627
import androidx.compose.ui.test.performClick
@@ -168,13 +169,13 @@ class NavigationTest {
168169
// Verify that the top bar contains the app name on the first screen.
169170
onNodeWithText(appName).assertExists()
170171

171-
// Go to the bookmarks tab, verify that the top bar contains the app name.
172+
// Go to the saved tab, verify that the top bar contains "saved". This means
173+
// we'll have 2 elements with the text "saved" on screen. One in the top bar, and
174+
// one in the bottom navigation.
172175
onNodeWithText(saved).performClick()
173-
onNodeWithText(appName).assertExists()
176+
onAllNodesWithText(saved).assertCountEquals(2)
174177

175-
// Go to the interests tab, verify that the top bar contains "Interests". This means
176-
// we'll have 2 elements with the text "Interests" on screen. One in the top bar, and
177-
// one in the bottom navigation.
178+
// As above but for the interests tab.
178179
onNodeWithText(interests).performClick()
179180
onAllNodesWithText(interests).assertCountEquals(2)
180181
}
@@ -214,7 +215,7 @@ class NavigationTest {
214215
onNodeWithText(ok).performClick()
215216

216217
// Check that the saved screen is still visible and selected.
217-
onNodeWithText(saved).assertIsSelected()
218+
onAllNodesWithText(saved).onLast().assertIsSelected()
218219
}
219220
}
220221

app/src/androidTest/java/com/google/samples/apps/nowinandroid/ui/NiaAppStateTest.kt

Lines changed: 78 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,19 @@ import androidx.navigation.compose.ComposeNavigator
3030
import androidx.navigation.compose.composable
3131
import androidx.navigation.createGraph
3232
import androidx.navigation.testing.TestNavHostController
33+
import com.google.samples.apps.nowinandroid.core.testing.util.MainDispatcherRule
34+
import com.google.samples.apps.nowinandroid.core.testing.util.TestNetworkMonitor
35+
import kotlinx.coroutines.cancel
36+
import kotlinx.coroutines.flow.collect
37+
import kotlinx.coroutines.launch
38+
import kotlinx.coroutines.test.TestScope
39+
import kotlinx.coroutines.test.UnconfinedTestDispatcher
40+
import kotlinx.coroutines.test.runTest
41+
import org.junit.After
3342
import org.junit.Assert.assertEquals
3443
import org.junit.Assert.assertFalse
3544
import org.junit.Assert.assertTrue
45+
import org.junit.Before
3646
import org.junit.Rule
3747
import org.junit.Test
3848

@@ -45,21 +55,43 @@ import org.junit.Test
4555
@OptIn(ExperimentalMaterial3WindowSizeClassApi::class)
4656
class NiaAppStateTest {
4757

58+
@get:Rule
59+
val mainDispatcherRule = MainDispatcherRule()
60+
4861
@get:Rule
4962
val composeTestRule = createComposeRule()
5063

64+
// Create the test dependencies.
65+
private lateinit var testScope: TestScope
66+
private val networkMonitor = TestNetworkMonitor()
67+
68+
// Subject under test.
5169
private lateinit var state: NiaAppState
5270

71+
@Before
72+
fun setup() {
73+
// We use the Unconfined dispatcher to ensure that coroutines are executed sequentially in
74+
// tests.
75+
testScope = TestScope(UnconfinedTestDispatcher())
76+
}
77+
78+
@After
79+
fun cleanup() {
80+
testScope.cancel()
81+
}
82+
5383
@Test
54-
fun niaAppState_currentDestination() {
84+
fun niaAppState_currentDestination() = runTest {
5585
var currentDestination: String? = null
5686

5787
composeTestRule.setContent {
5888
val navController = rememberTestNavController()
5989
state = remember(navController) {
6090
NiaAppState(
6191
windowSizeClass = getCompactWindowClass(),
62-
navController = navController
92+
navController = navController,
93+
networkMonitor = networkMonitor,
94+
coroutineScope = testScope
6395
)
6496
}
6597

@@ -76,9 +108,12 @@ class NiaAppStateTest {
76108
}
77109

78110
@Test
79-
fun niaAppState_destinations() {
111+
fun niaAppState_destinations() = runTest {
80112
composeTestRule.setContent {
81-
state = rememberNiaAppState(getCompactWindowClass())
113+
state = rememberNiaAppState(
114+
windowSizeClass = getCompactWindowClass(),
115+
networkMonitor = networkMonitor
116+
)
82117
}
83118

84119
assertEquals(3, state.topLevelDestinations.size)
@@ -93,19 +128,22 @@ class NiaAppStateTest {
93128
val navController = rememberTestNavController()
94129
state = rememberNiaAppState(
95130
windowSizeClass = getCompactWindowClass(),
96-
navController = navController
131+
navController = navController,
132+
networkMonitor = networkMonitor
97133
)
98134

99135
// Do nothing - we should already be
100136
}
101137
}
102138

103139
@Test
104-
fun niaAppState_showBottomBar_compact() {
140+
fun niaAppState_showBottomBar_compact() = runTest {
105141
composeTestRule.setContent {
106142
state = NiaAppState(
107143
windowSizeClass = getCompactWindowClass(),
108-
navController = NavHostController(LocalContext.current)
144+
navController = NavHostController(LocalContext.current),
145+
networkMonitor = networkMonitor,
146+
coroutineScope = testScope
109147
)
110148
}
111149

@@ -114,11 +152,13 @@ class NiaAppStateTest {
114152
}
115153

116154
@Test
117-
fun niaAppState_showNavRail_medium() {
155+
fun niaAppState_showNavRail_medium() = runTest {
118156
composeTestRule.setContent {
119157
state = NiaAppState(
120158
windowSizeClass = WindowSizeClass.calculateFromSize(DpSize(800.dp, 800.dp)),
121-
navController = NavHostController(LocalContext.current)
159+
navController = NavHostController(LocalContext.current),
160+
networkMonitor = networkMonitor,
161+
coroutineScope = testScope
122162
)
123163
}
124164

@@ -127,18 +167,45 @@ class NiaAppStateTest {
127167
}
128168

129169
@Test
130-
fun niaAppState_showNavRail_large() {
170+
fun niaAppState_showNavRail_large() = runTest {
171+
131172
composeTestRule.setContent {
132173
state = NiaAppState(
133174
windowSizeClass = WindowSizeClass.calculateFromSize(DpSize(900.dp, 1200.dp)),
134-
navController = NavHostController(LocalContext.current)
175+
navController = NavHostController(LocalContext.current),
176+
networkMonitor = networkMonitor,
177+
coroutineScope = testScope
135178
)
136179
}
137180

138181
assertTrue(state.shouldShowNavRail)
139182
assertFalse(state.shouldShowBottomBar)
140183
}
141184

185+
@Test
186+
fun stateIsOfflineWhenNetworkMonitorIsOffline() = runTest {
187+
188+
composeTestRule.setContent {
189+
state = NiaAppState(
190+
windowSizeClass = WindowSizeClass.calculateFromSize(DpSize(900.dp, 1200.dp)),
191+
navController = NavHostController(LocalContext.current),
192+
networkMonitor = networkMonitor,
193+
coroutineScope = testScope
194+
)
195+
}
196+
197+
val collectJob = testScope.launch { state.isOffline.collect() }
198+
199+
networkMonitor.setConnected(false)
200+
201+
assertEquals(
202+
true,
203+
state.isOffline.value
204+
)
205+
206+
collectJob.cancel()
207+
}
208+
142209
private fun getCompactWindowClass() = WindowSizeClass.calculateFromSize(DpSize(500.dp, 300.dp))
143210
}
144211

app/src/main/java/com/google/samples/apps/nowinandroid/MainActivity.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import androidx.metrics.performance.JankStats
3737
import com.google.accompanist.systemuicontroller.rememberSystemUiController
3838
import com.google.samples.apps.nowinandroid.MainActivityUiState.Loading
3939
import com.google.samples.apps.nowinandroid.MainActivityUiState.Success
40+
import com.google.samples.apps.nowinandroid.core.data.util.NetworkMonitor
4041
import com.google.samples.apps.nowinandroid.core.designsystem.theme.NiaTheme
4142
import com.google.samples.apps.nowinandroid.core.model.data.DarkThemeConfig
4243
import com.google.samples.apps.nowinandroid.core.model.data.ThemeBrand
@@ -57,6 +58,9 @@ class MainActivity : ComponentActivity() {
5758
@Inject
5859
lateinit var lazyStats: dagger.Lazy<JankStats>
5960

61+
@Inject
62+
lateinit var networkMonitor: NetworkMonitor
63+
6064
val viewModel: MainActivityViewModel by viewModels()
6165

6266
override fun onCreate(savedInstanceState: Bundle?) {
@@ -105,6 +109,7 @@ class MainActivity : ComponentActivity() {
105109
androidTheme = shouldUseAndroidTheme(uiState)
106110
) {
107111
NiaApp(
112+
networkMonitor = networkMonitor,
108113
windowSizeClass = calculateWindowSizeClass(this),
109114
)
110115
}

app/src/main/java/com/google/samples/apps/nowinandroid/ui/NiaApp.kt

Lines changed: 46 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,19 +31,28 @@ import androidx.compose.material3.ExperimentalMaterial3Api
3131
import androidx.compose.material3.Icon
3232
import androidx.compose.material3.MaterialTheme
3333
import androidx.compose.material3.Scaffold
34+
import androidx.compose.material3.SnackbarDuration.Indefinite
35+
import androidx.compose.material3.SnackbarHost
36+
import androidx.compose.material3.SnackbarHostState
3437
import androidx.compose.material3.Text
3538
import androidx.compose.material3.TopAppBarDefaults
3639
import androidx.compose.material3.windowsizeclass.WindowSizeClass
3740
import androidx.compose.runtime.Composable
41+
import androidx.compose.runtime.LaunchedEffect
42+
import androidx.compose.runtime.getValue
43+
import androidx.compose.runtime.remember
3844
import androidx.compose.ui.ExperimentalComposeUiApi
3945
import androidx.compose.ui.Modifier
4046
import androidx.compose.ui.graphics.Color
4147
import androidx.compose.ui.res.painterResource
4248
import androidx.compose.ui.res.stringResource
4349
import androidx.compose.ui.semantics.semantics
4450
import androidx.compose.ui.semantics.testTagsAsResourceId
51+
import androidx.lifecycle.compose.ExperimentalLifecycleComposeApi
52+
import androidx.lifecycle.compose.collectAsStateWithLifecycle
4553
import androidx.navigation.NavDestination
4654
import androidx.navigation.NavDestination.Companion.hierarchy
55+
import com.google.samples.apps.nowinandroid.core.data.util.NetworkMonitor
4756
import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaBackground
4857
import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaGradientBackground
4958
import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaNavigationBar
@@ -61,12 +70,16 @@ import com.google.samples.apps.nowinandroid.navigation.TopLevelDestination
6170
@OptIn(
6271
ExperimentalMaterial3Api::class,
6372
ExperimentalLayoutApi::class,
64-
ExperimentalComposeUiApi::class
73+
ExperimentalComposeUiApi::class, ExperimentalLifecycleComposeApi::class
6574
)
6675
@Composable
6776
fun NiaApp(
6877
windowSizeClass: WindowSizeClass,
69-
appState: NiaAppState = rememberNiaAppState(windowSizeClass)
78+
networkMonitor: NetworkMonitor,
79+
appState: NiaAppState = rememberNiaAppState(
80+
networkMonitor = networkMonitor,
81+
windowSizeClass = windowSizeClass
82+
),
7083
) {
7184
val background: @Composable (@Composable () -> Unit) -> Unit =
7285
when (appState.currentDestination?.route) {
@@ -75,26 +88,32 @@ fun NiaApp(
7588
}
7689

7790
background {
91+
92+
val snackbarHostState = remember { SnackbarHostState() }
93+
7894
Scaffold(
7995
modifier = Modifier.semantics {
8096
testTagsAsResourceId = true
8197
},
8298
containerColor = Color.Transparent,
8399
contentColor = MaterialTheme.colorScheme.onBackground,
84100
contentWindowInsets = WindowInsets(0, 0, 0, 0),
101+
snackbarHost = { SnackbarHost(snackbarHostState) },
85102
topBar = {
86-
val destination = appState.topLevelDestinations[appState.currentDestination?.route]
87-
if (appState.shouldShowTopBar && destination != null) {
103+
// Show the top app bar on top level destinations.
104+
val topLevelDestination =
105+
appState.topLevelDestinations[appState.currentDestination?.route]
106+
if (topLevelDestination != null) {
88107
NiaTopAppBar(
89-
titleRes = destination.titleTextId,
108+
titleRes = topLevelDestination.titleTextId,
90109
actionIcon = NiaIcons.Settings,
91110
actionIconContentDescription = stringResource(
92111
id = settingsR.string.top_app_bar_action_icon_description
93112
),
94113
colors = TopAppBarDefaults.centerAlignedTopAppBarColors(
95114
containerColor = Color.Transparent
96115
),
97-
onActionClick = { /*openAccountDialog = true*/ }
116+
onActionClick = { appState.toggleSettingsDialog(true) }
98117
)
99118
}
100119
},
@@ -108,6 +127,24 @@ fun NiaApp(
108127
}
109128
}
110129
) { padding ->
130+
131+
val isOffline by appState.isOffline.collectAsStateWithLifecycle()
132+
133+
// If user is not connected to the internet show a snack bar to inform them.
134+
val notConnected = stringResource(R.string.for_you_not_connected)
135+
LaunchedEffect(isOffline) {
136+
if (isOffline) snackbarHostState.showSnackbar(
137+
message = notConnected,
138+
duration = Indefinite
139+
)
140+
}
141+
142+
if (appState.shouldShowSettingsDialog) {
143+
SettingsDialog(
144+
onDismiss = { appState.toggleSettingsDialog(false) }
145+
)
146+
}
147+
111148
Row(
112149
Modifier
113150
.fillMaxSize()
@@ -134,6 +171,9 @@ fun NiaApp(
134171
.padding(padding)
135172
.consumedWindowInsets(padding)
136173
)
174+
175+
// TODO: We may want to add padding or spacer when the snackbar is shown so that
176+
// content doesn't display behind it.
137177
}
138178
}
139179
}

0 commit comments

Comments
 (0)