Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create a general error handler which handles multiple errors - Nia 1435 #1789

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import com.google.samples.apps.nowinandroid.MainActivityUiState.Loading
import com.google.samples.apps.nowinandroid.core.analytics.AnalyticsHelper
import com.google.samples.apps.nowinandroid.core.analytics.LocalAnalyticsHelper
import com.google.samples.apps.nowinandroid.core.data.repository.UserNewsResourceRepository
import com.google.samples.apps.nowinandroid.core.data.util.ErrorMonitor
import com.google.samples.apps.nowinandroid.core.data.util.NetworkMonitor
import com.google.samples.apps.nowinandroid.core.data.util.TimeZoneMonitor
import com.google.samples.apps.nowinandroid.core.designsystem.theme.NiaTheme
Expand Down Expand Up @@ -64,6 +65,9 @@ class MainActivity : ComponentActivity() {
@Inject
lateinit var networkMonitor: NetworkMonitor

@Inject
lateinit var errorMonitor: ErrorMonitor

@Inject
lateinit var timeZoneMonitor: TimeZoneMonitor

Expand Down Expand Up @@ -135,6 +139,7 @@ class MainActivity : ComponentActivity() {
setContent {
val appState = rememberNiaAppState(
networkMonitor = networkMonitor,
errorMonitor = errorMonitor,
userNewsResourceRepository = userNewsResourceRepository,
timeZoneMonitor = timeZoneMonitor,
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

package com.google.samples.apps.nowinandroid.ui

import android.content.Context
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.WindowInsets
Expand All @@ -30,6 +31,7 @@ import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarDuration
import androidx.compose.material3.SnackbarDuration.Indefinite
import androidx.compose.material3.SnackbarDuration.Short
import androidx.compose.material3.SnackbarHost
Expand All @@ -52,6 +54,7 @@ import androidx.compose.ui.composed
import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.semantics
Expand All @@ -69,6 +72,10 @@ import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaTopAp
import com.google.samples.apps.nowinandroid.core.designsystem.icon.NiaIcons
import com.google.samples.apps.nowinandroid.core.designsystem.theme.GradientColors
import com.google.samples.apps.nowinandroid.core.designsystem.theme.LocalGradientColors
import com.google.samples.apps.nowinandroid.core.model.data.MessageData
import com.google.samples.apps.nowinandroid.core.model.data.MessageType.MESSAGE
import com.google.samples.apps.nowinandroid.core.model.data.MessageType.OFFLINE
import com.google.samples.apps.nowinandroid.core.model.data.MessageType.UNKNOWN
import com.google.samples.apps.nowinandroid.feature.settings.SettingsDialog
import com.google.samples.apps.nowinandroid.navigation.NiaNavHost
import com.google.samples.apps.nowinandroid.navigation.TopLevelDestination
Expand All @@ -81,6 +88,8 @@ fun NiaApp(
modifier: Modifier = Modifier,
windowAdaptiveInfo: WindowAdaptiveInfo = currentWindowAdaptiveInfo(),
) {
val context = LocalContext.current

val shouldShowGradientBackground =
appState.currentTopLevelDestination == TopLevelDestination.FOR_YOU
var showSettingsDialog by rememberSaveable { mutableStateOf(false) }
Expand All @@ -95,16 +104,30 @@ fun NiaApp(
) {
val snackbarHostState = remember { SnackbarHostState() }

val isOffline by appState.isOffline.collectAsStateWithLifecycle()
val stateMessage by appState.stateMessage.collectAsStateWithLifecycle()

// If user is not connected to the internet show a snack bar to inform them.
val notConnectedMessage = stringResource(R.string.not_connected)
LaunchedEffect(isOffline) {
if (isOffline) {
snackbarHostState.showSnackbar(
message = notConnectedMessage,
duration = Indefinite,
)
LaunchedEffect(stateMessage) {
stateMessage?.let { message ->

// Text and Duration values dictated by the UI
val (text, duration) = getSnackbarValues(context, message)

// Determine whether user clicked action button
val snackBarResult = snackbarHostState.showSnackbar(
message = text,
actionLabel = message.label,
duration = duration,
) == ActionPerformed

// Handle result action
if (snackBarResult) {
message.onConfirm?.invoke()
} else {
message.onDelay?.invoke()
}

// Remove Message from List
appState.errorMonitor.clearMessage(message)
}
}

Expand Down Expand Up @@ -271,3 +294,11 @@ private fun NavDestination?.isRouteInHierarchy(route: KClass<*>) =
this?.hierarchy?.any {
it.hasRoute(route)
} ?: false

private fun getSnackbarValues(context: Context, message: MessageData): Pair<String, SnackbarDuration> {
return when (message.type) {
OFFLINE -> context.getString(R.string.not_connected) to SnackbarDuration.Indefinite
is MESSAGE -> (message.type as MESSAGE).value to SnackbarDuration.Long
UNKNOWN -> context.getString(R.string.unknown_error) to SnackbarDuration.Short
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,11 @@ import androidx.navigation.compose.rememberNavController
import androidx.navigation.navOptions
import androidx.tracing.trace
import com.google.samples.apps.nowinandroid.core.data.repository.UserNewsResourceRepository
import com.google.samples.apps.nowinandroid.core.data.util.ErrorMonitor
import com.google.samples.apps.nowinandroid.core.data.util.NetworkMonitor
import com.google.samples.apps.nowinandroid.core.data.util.TimeZoneMonitor
import com.google.samples.apps.nowinandroid.core.model.data.MessageData
import com.google.samples.apps.nowinandroid.core.model.data.MessageType
import com.google.samples.apps.nowinandroid.core.ui.TrackDisposableJank
import com.google.samples.apps.nowinandroid.feature.bookmarks.navigation.navigateToBookmarks
import com.google.samples.apps.nowinandroid.feature.foryou.navigation.navigateToForYou
Expand All @@ -53,6 +56,7 @@ import kotlinx.datetime.TimeZone
@Composable
fun rememberNiaAppState(
networkMonitor: NetworkMonitor,
errorMonitor: ErrorMonitor,
userNewsResourceRepository: UserNewsResourceRepository,
timeZoneMonitor: TimeZoneMonitor,
coroutineScope: CoroutineScope = rememberCoroutineScope(),
Expand All @@ -63,13 +67,15 @@ fun rememberNiaAppState(
navController,
coroutineScope,
networkMonitor,
errorMonitor,
userNewsResourceRepository,
timeZoneMonitor,
) {
NiaAppState(
navController = navController,
coroutineScope = coroutineScope,
networkMonitor = networkMonitor,
errorMonitor = errorMonitor,
userNewsResourceRepository = userNewsResourceRepository,
timeZoneMonitor = timeZoneMonitor,
)
Expand All @@ -81,6 +87,7 @@ class NiaAppState(
val navController: NavHostController,
coroutineScope: CoroutineScope,
networkMonitor: NetworkMonitor,
val errorMonitor: ErrorMonitor,
userNewsResourceRepository: UserNewsResourceRepository,
timeZoneMonitor: TimeZoneMonitor,
) {
Expand Down Expand Up @@ -115,6 +122,27 @@ class NiaAppState(
initialValue = false,
)

private val errorMessages: StateFlow<List<MessageData?>> = errorMonitor.messages.stateIn(
scope = coroutineScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = emptyList(),
)

val stateMessage: StateFlow<MessageData?> = combine(isOffline, errorMessages) { offline, errors ->
if (offline) {
// Priority is given to Offline Error Message over other types
MessageData(type = MessageType.OFFLINE)
}
// Otherwise, Display first from error monitor list if exists
else {
errors.firstOrNull()
}
}.stateIn(
scope = coroutineScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = null,
)

/**
* Map of top level destinations to be used in the TopBar, BottomBar and NavRail. The key is the
* route.
Expand Down
1 change: 1 addition & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,5 @@
<resources>
<string name="app_name">Now in Android</string>
<string name="not_connected">⚠️ You aren’t connected to the internet</string>
<string name="unknown_error">Unknown Error</string>
</resources>
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import com.github.takahirom.roborazzi.captureRoboImage
import com.google.samples.apps.nowinandroid.core.data.repository.TopicsRepository
import com.google.samples.apps.nowinandroid.core.data.repository.UserDataRepository
import com.google.samples.apps.nowinandroid.core.data.repository.UserNewsResourceRepository
import com.google.samples.apps.nowinandroid.core.data.util.ErrorMonitor
import com.google.samples.apps.nowinandroid.core.data.util.NetworkMonitor
import com.google.samples.apps.nowinandroid.core.data.util.TimeZoneMonitor
import com.google.samples.apps.nowinandroid.core.designsystem.theme.NiaTheme
Expand Down Expand Up @@ -80,6 +81,9 @@ class NiaAppScreenSizesScreenshotTests {
@Inject
lateinit var networkMonitor: NetworkMonitor

@Inject
lateinit var errorMonitor: ErrorMonitor

@Inject
lateinit var timeZoneMonitor: TimeZoneMonitor

Expand Down Expand Up @@ -123,6 +127,7 @@ class NiaAppScreenSizesScreenshotTests {
NiaTheme {
val fakeAppState = rememberNiaAppState(
networkMonitor = networkMonitor,
errorMonitor = errorMonitor,
userNewsResourceRepository = userNewsResourceRepository,
timeZoneMonitor = timeZoneMonitor,
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import androidx.navigation.testing.TestNavHostController
import com.google.samples.apps.nowinandroid.core.data.repository.CompositeUserNewsResourceRepository
import com.google.samples.apps.nowinandroid.core.testing.repository.TestNewsRepository
import com.google.samples.apps.nowinandroid.core.testing.repository.TestUserDataRepository
import com.google.samples.apps.nowinandroid.core.testing.util.TestErrorMonitor
import com.google.samples.apps.nowinandroid.core.testing.util.TestNetworkMonitor
import com.google.samples.apps.nowinandroid.core.testing.util.TestTimeZoneMonitor
import dagger.hilt.android.testing.HiltAndroidTest
Expand Down Expand Up @@ -60,6 +61,8 @@ class NiaAppStateTest {
// Create the test dependencies.
private val networkMonitor = TestNetworkMonitor()

private var errorMonitor = TestErrorMonitor()

private val timeZoneMonitor = TestTimeZoneMonitor()

private val userNewsResourceRepository =
Expand All @@ -79,6 +82,7 @@ class NiaAppStateTest {
navController = navController,
coroutineScope = backgroundScope,
networkMonitor = networkMonitor,
errorMonitor = errorMonitor,
userNewsResourceRepository = userNewsResourceRepository,
timeZoneMonitor = timeZoneMonitor,
)
Expand All @@ -101,6 +105,7 @@ class NiaAppStateTest {
composeTestRule.setContent {
state = rememberNiaAppState(
networkMonitor = networkMonitor,
errorMonitor = errorMonitor,
userNewsResourceRepository = userNewsResourceRepository,
timeZoneMonitor = timeZoneMonitor,
)
Expand All @@ -119,6 +124,7 @@ class NiaAppStateTest {
navController = NavHostController(LocalContext.current),
coroutineScope = backgroundScope,
networkMonitor = networkMonitor,
errorMonitor = errorMonitor,
userNewsResourceRepository = userNewsResourceRepository,
timeZoneMonitor = timeZoneMonitor,
)
Expand All @@ -139,6 +145,7 @@ class NiaAppStateTest {
navController = NavHostController(LocalContext.current),
coroutineScope = backgroundScope,
networkMonitor = networkMonitor,
errorMonitor = errorMonitor,
userNewsResourceRepository = userNewsResourceRepository,
timeZoneMonitor = timeZoneMonitor,
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ import com.github.takahirom.roborazzi.captureRoboImage
import com.google.samples.apps.nowinandroid.core.data.repository.TopicsRepository
import com.google.samples.apps.nowinandroid.core.data.repository.UserNewsResourceRepository
import com.google.samples.apps.nowinandroid.core.data.test.repository.FakeUserDataRepository
import com.google.samples.apps.nowinandroid.core.data.util.ErrorMonitor
import com.google.samples.apps.nowinandroid.core.data.util.NetworkMonitor
import com.google.samples.apps.nowinandroid.core.data.util.TimeZoneMonitor
import com.google.samples.apps.nowinandroid.core.designsystem.theme.NiaTheme
Expand Down Expand Up @@ -113,6 +114,9 @@ class SnackbarInsetsScreenshotTests {
@Inject
lateinit var networkMonitor: NetworkMonitor

@Inject
lateinit var errorMonitor: ErrorMonitor

@Inject
lateinit var timeZoneMonitor: TimeZoneMonitor

Expand Down Expand Up @@ -254,6 +258,7 @@ class SnackbarInsetsScreenshotTests {
NiaTheme {
val appState = rememberNiaAppState(
networkMonitor = networkMonitor,
errorMonitor = errorMonitor,
userNewsResourceRepository = userNewsResourceRepository,
timeZoneMonitor = timeZoneMonitor,
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import com.github.takahirom.roborazzi.captureRoboImage
import com.google.samples.apps.nowinandroid.core.data.repository.TopicsRepository
import com.google.samples.apps.nowinandroid.core.data.repository.UserNewsResourceRepository
import com.google.samples.apps.nowinandroid.core.data.test.repository.FakeUserDataRepository
import com.google.samples.apps.nowinandroid.core.data.util.ErrorMonitor
import com.google.samples.apps.nowinandroid.core.data.util.NetworkMonitor
import com.google.samples.apps.nowinandroid.core.data.util.TimeZoneMonitor
import com.google.samples.apps.nowinandroid.core.designsystem.theme.NiaTheme
Expand Down Expand Up @@ -86,6 +87,9 @@ class SnackbarScreenshotTests {
@Inject
lateinit var networkMonitor: NetworkMonitor

@Inject
lateinit var errorMonitor: ErrorMonitor

@Inject
lateinit var timeZoneMonitor: TimeZoneMonitor

Expand Down Expand Up @@ -203,6 +207,7 @@ class SnackbarScreenshotTests {
NiaTheme {
val appState = rememberNiaAppState(
networkMonitor = networkMonitor,
errorMonitor = errorMonitor,
userNewsResourceRepository = userNewsResourceRepository,
timeZoneMonitor = timeZoneMonitor,
)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/*
* Copyright 2025 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.google.samples.apps.nowinandroid.core.data.test

import com.google.samples.apps.nowinandroid.core.data.util.ErrorMonitor
import com.google.samples.apps.nowinandroid.core.model.data.MessageData
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOf
import javax.inject.Inject

class EmptyErrorMonitor @Inject constructor() : ErrorMonitor {

override fun addMessageByString(message: String): MessageData {
TODO("Not yet implemented")
}

override fun addMessageByData(message: MessageData) {
TODO("Not yet implemented")
}

override fun clearMessage(message: MessageData) {
TODO("Not yet implemented")
}

override fun clearAllMessages() {
TODO("Not yet implemented")
}

override val messages: Flow<List<MessageData?>>
get() = flowOf(emptyList())
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import com.google.samples.apps.nowinandroid.core.data.test.repository.FakeRecent
import com.google.samples.apps.nowinandroid.core.data.test.repository.FakeSearchContentsRepository
import com.google.samples.apps.nowinandroid.core.data.test.repository.FakeTopicsRepository
import com.google.samples.apps.nowinandroid.core.data.test.repository.FakeUserDataRepository
import com.google.samples.apps.nowinandroid.core.data.util.ErrorMonitor
import com.google.samples.apps.nowinandroid.core.data.util.NetworkMonitor
import com.google.samples.apps.nowinandroid.core.data.util.TimeZoneMonitor
import dagger.Binds
Expand Down Expand Up @@ -70,6 +71,11 @@ internal interface TestDataModule {
networkMonitor: AlwaysOnlineNetworkMonitor,
): NetworkMonitor

@Binds
fun bindsErrorMonitor(
errorMonitor: EmptyErrorMonitor,
): ErrorMonitor

@Binds
fun binds(impl: DefaultZoneIdTimeZoneMonitor): TimeZoneMonitor
}
Loading
Loading