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
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
@@ -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
@@ -64,6 +65,9 @@ class MainActivity : ComponentActivity() {
@Inject
lateinit var networkMonitor: NetworkMonitor

@Inject
lateinit var errorMonitor: ErrorMonitor

@Inject
lateinit var timeZoneMonitor: TimeZoneMonitor

@@ -135,6 +139,7 @@ class MainActivity : ComponentActivity() {
setContent {
val appState = rememberNiaAppState(
networkMonitor = networkMonitor,
errorMonitor = errorMonitor,
userNewsResourceRepository = userNewsResourceRepository,
timeZoneMonitor = timeZoneMonitor,
)
Original file line number Diff line number Diff line change
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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) }
@@ -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)
}
}

@@ -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
@@ -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
@@ -53,6 +56,7 @@ import kotlinx.datetime.TimeZone
@Composable
fun rememberNiaAppState(
networkMonitor: NetworkMonitor,
errorMonitor: ErrorMonitor,
userNewsResourceRepository: UserNewsResourceRepository,
timeZoneMonitor: TimeZoneMonitor,
coroutineScope: CoroutineScope = rememberCoroutineScope(),
@@ -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,
)
@@ -81,6 +87,7 @@ class NiaAppState(
val navController: NavHostController,
coroutineScope: CoroutineScope,
networkMonitor: NetworkMonitor,
val errorMonitor: ErrorMonitor,
userNewsResourceRepository: UserNewsResourceRepository,
timeZoneMonitor: TimeZoneMonitor,
) {
@@ -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.
1 change: 1 addition & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
@@ -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
@@ -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
@@ -80,6 +81,9 @@ class NiaAppScreenSizesScreenshotTests {
@Inject
lateinit var networkMonitor: NetworkMonitor

@Inject
lateinit var errorMonitor: ErrorMonitor

@Inject
lateinit var timeZoneMonitor: TimeZoneMonitor

@@ -123,6 +127,7 @@ class NiaAppScreenSizesScreenshotTests {
NiaTheme {
val fakeAppState = rememberNiaAppState(
networkMonitor = networkMonitor,
errorMonitor = errorMonitor,
userNewsResourceRepository = userNewsResourceRepository,
timeZoneMonitor = timeZoneMonitor,
)
Original file line number Diff line number Diff line change
@@ -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
@@ -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 =
@@ -79,6 +82,7 @@ class NiaAppStateTest {
navController = navController,
coroutineScope = backgroundScope,
networkMonitor = networkMonitor,
errorMonitor = errorMonitor,
userNewsResourceRepository = userNewsResourceRepository,
timeZoneMonitor = timeZoneMonitor,
)
@@ -101,6 +105,7 @@ class NiaAppStateTest {
composeTestRule.setContent {
state = rememberNiaAppState(
networkMonitor = networkMonitor,
errorMonitor = errorMonitor,
userNewsResourceRepository = userNewsResourceRepository,
timeZoneMonitor = timeZoneMonitor,
)
@@ -119,6 +124,7 @@ class NiaAppStateTest {
navController = NavHostController(LocalContext.current),
coroutineScope = backgroundScope,
networkMonitor = networkMonitor,
errorMonitor = errorMonitor,
userNewsResourceRepository = userNewsResourceRepository,
timeZoneMonitor = timeZoneMonitor,
)
@@ -139,6 +145,7 @@ class NiaAppStateTest {
navController = NavHostController(LocalContext.current),
coroutineScope = backgroundScope,
networkMonitor = networkMonitor,
errorMonitor = errorMonitor,
userNewsResourceRepository = userNewsResourceRepository,
timeZoneMonitor = timeZoneMonitor,
)
Original file line number Diff line number Diff line change
@@ -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
@@ -113,6 +114,9 @@ class SnackbarInsetsScreenshotTests {
@Inject
lateinit var networkMonitor: NetworkMonitor

@Inject
lateinit var errorMonitor: ErrorMonitor

@Inject
lateinit var timeZoneMonitor: TimeZoneMonitor

@@ -254,6 +258,7 @@ class SnackbarInsetsScreenshotTests {
NiaTheme {
val appState = rememberNiaAppState(
networkMonitor = networkMonitor,
errorMonitor = errorMonitor,
userNewsResourceRepository = userNewsResourceRepository,
timeZoneMonitor = timeZoneMonitor,
)
Original file line number Diff line number Diff line change
@@ -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
@@ -86,6 +87,9 @@ class SnackbarScreenshotTests {
@Inject
lateinit var networkMonitor: NetworkMonitor

@Inject
lateinit var errorMonitor: ErrorMonitor

@Inject
lateinit var timeZoneMonitor: TimeZoneMonitor

@@ -203,6 +207,7 @@ class SnackbarScreenshotTests {
NiaTheme {
val appState = rememberNiaAppState(
networkMonitor = networkMonitor,
errorMonitor = errorMonitor,
userNewsResourceRepository = userNewsResourceRepository,
timeZoneMonitor = timeZoneMonitor,
)
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
@@ -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
@@ -70,6 +71,11 @@ internal interface TestDataModule {
networkMonitor: AlwaysOnlineNetworkMonitor,
): NetworkMonitor

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

@Binds
fun binds(impl: DefaultZoneIdTimeZoneMonitor): TimeZoneMonitor
}
Original file line number Diff line number Diff line change
@@ -27,7 +27,9 @@ import com.google.samples.apps.nowinandroid.core.data.repository.SearchContentsR
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.util.ConnectivityManagerNetworkMonitor
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.StateErrorMonitor
import com.google.samples.apps.nowinandroid.core.data.util.TimeZoneBroadcastMonitor
import com.google.samples.apps.nowinandroid.core.data.util.TimeZoneMonitor
import dagger.Binds
@@ -69,6 +71,11 @@ abstract class DataModule {
networkMonitor: ConnectivityManagerNetworkMonitor,
): NetworkMonitor

@Binds
internal abstract fun bindsErrorMonitor(
errorMonitor: StateErrorMonitor,
): ErrorMonitor

@Binds
internal abstract fun binds(impl: TimeZoneBroadcastMonitor): TimeZoneMonitor
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
* Copyright 2024 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.util

import com.google.samples.apps.nowinandroid.core.model.data.MessageData
import kotlinx.coroutines.flow.Flow

/**
* Interface for handling messages.
*/
interface ErrorMonitor {
fun addMessageByString(message: String): MessageData

fun addMessageByData(message: MessageData)

fun clearMessage(message: MessageData)

fun clearAllMessages()

val messages: Flow<List<MessageData?>>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/*
* Copyright 2024 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.util

import com.google.samples.apps.nowinandroid.core.model.data.MessageData
import com.google.samples.apps.nowinandroid.core.model.data.MessageType
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update
import javax.inject.Inject

/**
* Interface implementation for handling general errors.
*/

class StateErrorMonitor @Inject constructor() : ErrorMonitor {
/**
* List of [MessageData] to be shown
*/
override val messages = MutableStateFlow<List<MessageData>>(emptyList())

/**
* Creates a [MessageData] and adds it to the list.
* @param message: String value for message to add.
*/
override fun addMessageByString(message: String): MessageData {
val data = MessageData(type = MessageType.MESSAGE(message))
messages.update { it + data }

return data
}

/**
* Add a [MessageData] to the list.
* @param message: [MessageData] to add.
*/
override fun addMessageByData(message: MessageData) {
messages.update { it + message }
}

/**
* Removes the [MessageData] from the list.
*/
override fun clearMessage(message: MessageData) {
messages.update { list -> list.filterNot { it == message } }
}

/**
* Remove all from list, reset to empty list
*/
override fun clearAllMessages() {
messages.update { emptyList() }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
/*
* Copyright 2024 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.
*/

import com.google.samples.apps.nowinandroid.core.data.util.StateErrorMonitor
import com.google.samples.apps.nowinandroid.core.model.data.MessageData
import com.google.samples.apps.nowinandroid.core.model.data.MessageType
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
import kotlin.test.assertEquals

/**
* Unit tests for [StateErrorMonitor].
*/

class StateErrorMonitorTest {

// Subject under test.
private lateinit var state: StateErrorMonitor

private lateinit var messages: List<MessageData?>

private val testString = "Test Error Message"
private val testData = MessageData(MessageType.MESSAGE("Test Error Message 1"))
private val testData2 = MessageData(MessageType.MESSAGE("Test Error Message 2"))

@Before
fun setup() {
state = StateErrorMonitor()
messages = emptyList()
}

@Test
fun whenErrorIsNotAdded_NullIsPresent() = runTest(UnconfinedTestDispatcher()) {
backgroundScope.launch {
state.messages.collect {
messages = it
}
}
assertEquals(
emptyList(),
messages,
)
}

@Test
fun whenErrorIsAddedByString_ErrorMessageIsPresent() = runTest(UnconfinedTestDispatcher()) {
backgroundScope.launch {
state.messages.collect {
messages = it
}
}

val expect = state.addMessageByString(testString)

assertEquals(
expect,
messages.firstOrNull(),
)
}

@Test
fun whenErrorIsAddedByData_ErrorMessageIsPresent() = runTest(UnconfinedTestDispatcher()) {
backgroundScope.launch {
state.messages.collect {
messages = it
}
}

state.addMessageByData(testData)

assertEquals(
testData,
messages.firstOrNull(),
)
}

@Test
fun whenErrorsAreAdded_FirstErrorMessageIsPresent() =
runTest(UnconfinedTestDispatcher()) {
state.addMessageByData(testData)
state.addMessageByString(testString)

backgroundScope.launch {
state.messages.collect {
messages = it
}
}

assertEquals(
testData,
messages.firstOrNull(),
)
}

@Test
fun whenErrorIsCleared_ErrorMessageIsNotPresent() =
runTest(UnconfinedTestDispatcher()) {
backgroundScope.launch {
state.messages.collect {
messages = it
}
}
state.addMessageByData(testData)
state.clearMessage(testData)

assertEquals(
emptyList(),
messages,
)
}

@Test
fun whenErrorsAreCleared_NextErrorMessageIsPresent() =
runTest(UnconfinedTestDispatcher()) {
backgroundScope.launch {
state.messages.collect {
messages = it
}
}
state.addMessageByData(testData)
state.addMessageByData(testData2)

state.clearMessage(testData)

assertEquals(
testData2,
messages.firstOrNull(),
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
* Copyright 2024 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.model.data

/**
* [MessageData]
* Class to hold messages type objects with actions
*/
data class MessageData(
val type: MessageType,
val label: String? = null,
val onConfirm: (() -> Unit)? = null,
val onDelay: (() -> Unit)? = null,
)

/**
* Specified Errors
*/
sealed class MessageType {
data object OFFLINE : MessageType()
data class MESSAGE(val value: String) : MessageType()
data object UNKNOWN : MessageType()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/*
* 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.testing.util

import com.google.samples.apps.nowinandroid.core.data.util.ErrorMonitor
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 kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow

class TestErrorMonitor : ErrorMonitor {

private val _messages = MutableStateFlow<List<MessageData>>(emptyList())

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() {
_messages.value = emptyList()
}

override val messages: Flow<List<MessageData?>>
get() = _messages

/**
* Test-only API to add message types
*/

fun setOfflineMessage() {
_messages.value = listOf(OFFLINE_MESSAGE)
}

fun addMessage() {
_messages.value.plus(MESSAGE_MESSAGE)
}

fun addUnknownMessage() {
_messages.value.plus(UNKNOWN_MESSAGE)
}

companion object {
val OFFLINE_MESSAGE = MessageData(OFFLINE)
val MESSAGE_MESSAGE = MessageData(MESSAGE("Message"), "Title", {}, {})
val UNKNOWN_MESSAGE = MessageData(UNKNOWN)
}
}