Skip to content

Commit 3e66e3d

Browse files
authored
Merge pull request #595 from android/jr/track-viewed
Add visual indicator of read/unread news resources
2 parents feafb5f + 93953c2 commit 3e66e3d

File tree

51 files changed

+630
-218
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

51 files changed

+630
-218
lines changed

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

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,10 @@ import androidx.compose.ui.test.onNodeWithTag
2525
import androidx.compose.ui.unit.DpSize
2626
import androidx.compose.ui.unit.dp
2727
import com.google.accompanist.testharness.TestHarness
28+
import com.google.samples.apps.nowinandroid.core.data.repository.CompositeUserNewsResourceRepository
2829
import com.google.samples.apps.nowinandroid.core.data.util.NetworkMonitor
30+
import com.google.samples.apps.nowinandroid.core.testing.repository.TestNewsRepository
31+
import com.google.samples.apps.nowinandroid.core.testing.repository.TestUserDataRepository
2932
import com.google.samples.apps.nowinandroid.uitesthiltmanifest.HiltComponentActivity
3033
import dagger.hilt.android.testing.BindValue
3134
import dagger.hilt.android.testing.HiltAndroidRule
@@ -63,6 +66,11 @@ class NavigationUiTest {
6366
@get:Rule(order = 2)
6467
val composeTestRule = createAndroidComposeRule<HiltComponentActivity>()
6568

69+
val userNewsResourceRepository = CompositeUserNewsResourceRepository(
70+
newsRepository = TestNewsRepository(),
71+
userDataRepository = TestUserDataRepository(),
72+
)
73+
6674
@Inject
6775
lateinit var networkMonitor: NetworkMonitor
6876

@@ -81,6 +89,7 @@ class NavigationUiTest {
8189
DpSize(maxWidth, maxHeight),
8290
),
8391
networkMonitor = networkMonitor,
92+
userNewsResourceRepository = userNewsResourceRepository,
8493
)
8594
}
8695
}
@@ -100,6 +109,7 @@ class NavigationUiTest {
100109
DpSize(maxWidth, maxHeight),
101110
),
102111
networkMonitor = networkMonitor,
112+
userNewsResourceRepository = userNewsResourceRepository,
103113
)
104114
}
105115
}
@@ -119,6 +129,7 @@ class NavigationUiTest {
119129
DpSize(maxWidth, maxHeight),
120130
),
121131
networkMonitor = networkMonitor,
132+
userNewsResourceRepository = userNewsResourceRepository,
122133
)
123134
}
124135
}
@@ -138,6 +149,7 @@ class NavigationUiTest {
138149
DpSize(maxWidth, maxHeight),
139150
),
140151
networkMonitor = networkMonitor,
152+
userNewsResourceRepository = userNewsResourceRepository,
141153
)
142154
}
143155
}
@@ -157,6 +169,7 @@ class NavigationUiTest {
157169
DpSize(maxWidth, maxHeight),
158170
),
159171
networkMonitor = networkMonitor,
172+
userNewsResourceRepository = userNewsResourceRepository,
160173
)
161174
}
162175
}
@@ -176,6 +189,7 @@ class NavigationUiTest {
176189
DpSize(maxWidth, maxHeight),
177190
),
178191
networkMonitor = networkMonitor,
192+
userNewsResourceRepository = userNewsResourceRepository,
179193
)
180194
}
181195
}
@@ -195,6 +209,7 @@ class NavigationUiTest {
195209
DpSize(maxWidth, maxHeight),
196210
),
197211
networkMonitor = networkMonitor,
212+
userNewsResourceRepository = userNewsResourceRepository,
198213
)
199214
}
200215
}
@@ -214,6 +229,7 @@ class NavigationUiTest {
214229
DpSize(maxWidth, maxHeight),
215230
),
216231
networkMonitor = networkMonitor,
232+
userNewsResourceRepository = userNewsResourceRepository,
217233
)
218234
}
219235
}
@@ -233,6 +249,7 @@ class NavigationUiTest {
233249
DpSize(maxWidth, maxHeight),
234250
),
235251
networkMonitor = networkMonitor,
252+
userNewsResourceRepository = userNewsResourceRepository,
236253
)
237254
}
238255
}

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

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ 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.data.repository.CompositeUserNewsResourceRepository
34+
import com.google.samples.apps.nowinandroid.core.testing.repository.TestNewsRepository
35+
import com.google.samples.apps.nowinandroid.core.testing.repository.TestUserDataRepository
3336
import com.google.samples.apps.nowinandroid.core.testing.util.TestNetworkMonitor
3437
import kotlinx.coroutines.flow.collect
3538
import kotlinx.coroutines.launch
@@ -56,6 +59,9 @@ class NiaAppStateTest {
5659
// Create the test dependencies.
5760
private val networkMonitor = TestNetworkMonitor()
5861

62+
private val userNewsResourceRepository =
63+
CompositeUserNewsResourceRepository(TestNewsRepository(), TestUserDataRepository())
64+
5965
// Subject under test.
6066
private lateinit var state: NiaAppState
6167

@@ -67,10 +73,11 @@ class NiaAppStateTest {
6773
val navController = rememberTestNavController()
6874
state = remember(navController) {
6975
NiaAppState(
70-
windowSizeClass = getCompactWindowClass(),
7176
navController = navController,
72-
networkMonitor = networkMonitor,
7377
coroutineScope = backgroundScope,
78+
windowSizeClass = getCompactWindowClass(),
79+
networkMonitor = networkMonitor,
80+
userNewsResourceRepository = userNewsResourceRepository,
7481
)
7582
}
7683

@@ -92,6 +99,7 @@ class NiaAppStateTest {
9299
state = rememberNiaAppState(
93100
windowSizeClass = getCompactWindowClass(),
94101
networkMonitor = networkMonitor,
102+
userNewsResourceRepository = userNewsResourceRepository,
95103
)
96104
}
97105

@@ -105,10 +113,11 @@ class NiaAppStateTest {
105113
fun niaAppState_showBottomBar_compact() = runTest {
106114
composeTestRule.setContent {
107115
state = NiaAppState(
108-
windowSizeClass = getCompactWindowClass(),
109116
navController = NavHostController(LocalContext.current),
110-
networkMonitor = networkMonitor,
111117
coroutineScope = backgroundScope,
118+
windowSizeClass = getCompactWindowClass(),
119+
networkMonitor = networkMonitor,
120+
userNewsResourceRepository = userNewsResourceRepository,
112121
)
113122
}
114123

@@ -120,10 +129,11 @@ class NiaAppStateTest {
120129
fun niaAppState_showNavRail_medium() = runTest {
121130
composeTestRule.setContent {
122131
state = NiaAppState(
123-
windowSizeClass = WindowSizeClass.calculateFromSize(DpSize(800.dp, 800.dp)),
124132
navController = NavHostController(LocalContext.current),
125-
networkMonitor = networkMonitor,
126133
coroutineScope = backgroundScope,
134+
windowSizeClass = WindowSizeClass.calculateFromSize(DpSize(800.dp, 800.dp)),
135+
networkMonitor = networkMonitor,
136+
userNewsResourceRepository = userNewsResourceRepository,
127137
)
128138
}
129139

@@ -135,10 +145,11 @@ class NiaAppStateTest {
135145
fun niaAppState_showNavRail_large() = runTest {
136146
composeTestRule.setContent {
137147
state = NiaAppState(
138-
windowSizeClass = WindowSizeClass.calculateFromSize(DpSize(900.dp, 1200.dp)),
139148
navController = NavHostController(LocalContext.current),
140-
networkMonitor = networkMonitor,
141149
coroutineScope = backgroundScope,
150+
windowSizeClass = WindowSizeClass.calculateFromSize(DpSize(900.dp, 1200.dp)),
151+
networkMonitor = networkMonitor,
152+
userNewsResourceRepository = userNewsResourceRepository,
142153
)
143154
}
144155

@@ -150,10 +161,11 @@ class NiaAppStateTest {
150161
fun stateIsOfflineWhenNetworkMonitorIsOffline() = runTest(UnconfinedTestDispatcher()) {
151162
composeTestRule.setContent {
152163
state = NiaAppState(
153-
windowSizeClass = WindowSizeClass.calculateFromSize(DpSize(900.dp, 1200.dp)),
154164
navController = NavHostController(LocalContext.current),
155-
networkMonitor = networkMonitor,
156165
coroutineScope = backgroundScope,
166+
windowSizeClass = WindowSizeClass.calculateFromSize(DpSize(900.dp, 1200.dp)),
167+
networkMonitor = networkMonitor,
168+
userNewsResourceRepository = userNewsResourceRepository,
157169
)
158170
}
159171

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
@@ -40,6 +40,7 @@ import com.google.samples.apps.nowinandroid.MainActivityUiState.Loading
4040
import com.google.samples.apps.nowinandroid.MainActivityUiState.Success
4141
import com.google.samples.apps.nowinandroid.core.analytics.AnalyticsHelper
4242
import com.google.samples.apps.nowinandroid.core.analytics.LocalAnalyticsHelper
43+
import com.google.samples.apps.nowinandroid.core.data.repository.UserNewsResourceRepository
4344
import com.google.samples.apps.nowinandroid.core.data.util.NetworkMonitor
4445
import com.google.samples.apps.nowinandroid.core.designsystem.theme.NiaTheme
4546
import com.google.samples.apps.nowinandroid.core.model.data.DarkThemeConfig
@@ -67,6 +68,9 @@ class MainActivity : ComponentActivity() {
6768
@Inject
6869
lateinit var analyticsHelper: AnalyticsHelper
6970

71+
@Inject
72+
lateinit var userNewsResourceRepository: UserNewsResourceRepository
73+
7074
val viewModel: MainActivityViewModel by viewModels()
7175

7276
override fun onCreate(savedInstanceState: Bundle?) {
@@ -119,6 +123,7 @@ class MainActivity : ComponentActivity() {
119123
NiaApp(
120124
networkMonitor = networkMonitor,
121125
windowSizeClass = calculateWindowSizeClass(this),
126+
userNewsResourceRepository = userNewsResourceRepository,
122127
)
123128
}
124129
}

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

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,16 +44,20 @@ import androidx.compose.runtime.getValue
4444
import androidx.compose.runtime.remember
4545
import androidx.compose.ui.ExperimentalComposeUiApi
4646
import androidx.compose.ui.Modifier
47+
import androidx.compose.ui.draw.drawWithContent
48+
import androidx.compose.ui.geometry.Offset
4749
import androidx.compose.ui.graphics.Color
4850
import androidx.compose.ui.platform.testTag
4951
import androidx.compose.ui.res.painterResource
5052
import androidx.compose.ui.res.stringResource
5153
import androidx.compose.ui.semantics.semantics
5254
import androidx.compose.ui.semantics.testTagsAsResourceId
55+
import androidx.compose.ui.unit.dp
5356
import androidx.lifecycle.compose.collectAsStateWithLifecycle
5457
import androidx.navigation.NavDestination
5558
import androidx.navigation.NavDestination.Companion.hierarchy
5659
import com.google.samples.apps.nowinandroid.R
60+
import com.google.samples.apps.nowinandroid.core.data.repository.UserNewsResourceRepository
5761
import com.google.samples.apps.nowinandroid.core.data.util.NetworkMonitor
5862
import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaBackground
5963
import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaGradientBackground
@@ -81,9 +85,11 @@ import com.google.samples.apps.nowinandroid.feature.settings.R as settingsR
8185
fun NiaApp(
8286
windowSizeClass: WindowSizeClass,
8387
networkMonitor: NetworkMonitor,
88+
userNewsResourceRepository: UserNewsResourceRepository,
8489
appState: NiaAppState = rememberNiaAppState(
8590
networkMonitor = networkMonitor,
8691
windowSizeClass = windowSizeClass,
92+
userNewsResourceRepository = userNewsResourceRepository,
8793
),
8894
) {
8995
val shouldShowGradientBackground =
@@ -128,8 +134,10 @@ fun NiaApp(
128134
snackbarHost = { SnackbarHost(snackbarHostState) },
129135
bottomBar = {
130136
if (appState.shouldShowBottomBar) {
137+
val unreadDestinations by appState.topLevelDestinationsWithUnreadResources.collectAsStateWithLifecycle()
131138
NiaBottomBar(
132139
destinations = appState.topLevelDestinations,
140+
destinationsWithUnreadResources = unreadDestinations,
133141
onNavigateToDestination = appState::navigateToTopLevelDestination,
134142
currentDestination = appState.currentDestination,
135143
modifier = Modifier.testTag("NiaBottomBar"),
@@ -211,13 +219,15 @@ private fun NiaNavRail(
211219
imageVector = icon.imageVector,
212220
contentDescription = null,
213221
)
222+
214223
is DrawableResourceIcon -> Icon(
215224
painter = painterResource(id = icon.id),
216225
contentDescription = null,
217226
)
218227
}
219228
},
220229
label = { Text(stringResource(destination.iconTextId)) },
230+
221231
)
222232
}
223233
}
@@ -226,6 +236,7 @@ private fun NiaNavRail(
226236
@Composable
227237
private fun NiaBottomBar(
228238
destinations: List<TopLevelDestination>,
239+
destinationsWithUnreadResources: Set<TopLevelDestination>,
229240
onNavigateToDestination: (TopLevelDestination) -> Unit,
230241
currentDestination: NavDestination?,
231242
modifier: Modifier = Modifier,
@@ -234,6 +245,7 @@ private fun NiaBottomBar(
234245
modifier = modifier,
235246
) {
236247
destinations.forEach { destination ->
248+
val hasUnread = destinationsWithUnreadResources.contains(destination)
237249
val selected = currentDestination.isTopLevelDestinationInHierarchy(destination)
238250
NiaNavigationBarItem(
239251
selected = selected,
@@ -257,11 +269,31 @@ private fun NiaBottomBar(
257269
}
258270
},
259271
label = { Text(stringResource(destination.iconTextId)) },
272+
modifier = if (hasUnread) notificationDot() else Modifier,
260273
)
261274
}
262275
}
263276
}
264277

278+
@Composable
279+
private fun notificationDot(): Modifier {
280+
val tertiaryColor = MaterialTheme.colorScheme.tertiary
281+
return Modifier.drawWithContent {
282+
drawContent()
283+
drawCircle(
284+
tertiaryColor,
285+
radius = 5.dp.toPx(),
286+
// This is based on the dimensions of the NavigationBar's "indicator pill";
287+
// however, its parameters are private, so we must depend on them implicitly
288+
// (NavigationBarTokens.ActiveIndicatorWidth = 64.dp)
289+
center = center + Offset(
290+
64.dp.toPx() * .45f,
291+
32.dp.toPx() * -.45f - 6.dp.toPx(),
292+
),
293+
)
294+
}
295+
}
296+
265297
private fun NavDestination?.isTopLevelDestinationInHierarchy(destination: TopLevelDestination) =
266298
this?.hierarchy?.any {
267299
it.route?.contains(destination.name, true) ?: false

0 commit comments

Comments
 (0)