Route notification deep link through for you screen

pull/712/head
TJ Dahunsi 2 years ago
parent 8a3a16de21
commit ef97cb941c

@ -42,9 +42,13 @@
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter>
<data
android:scheme="https"
android:host="www.nowinandroid.apps.samples.google.com" />
</intent-filter>
</activity>
<!-- Disable Firebase analytics by default. This setting is overwritten for the `prod`

@ -129,7 +129,9 @@ class OfflineFirstNewsRepository @Inject constructor(
.first()
.map(PopulatedNewsResource::asExternalModel)
if (addedNewsResources.isNotEmpty()) notifier.onNewsAdded(addedNewsResources)
if (addedNewsResources.isNotEmpty()) notifier.postNewsNotifications(
newsResources = addedNewsResources,
)
}
},
)

@ -23,5 +23,5 @@ import javax.inject.Inject
* Implementation of [Notifier] which does nothing. Useful for tests and previews.
*/
class NoOpNotifier @Inject constructor() : Notifier {
override fun onNewsAdded(newsResources: List<NewsResource>) = Unit
override fun postNewsNotifications(newsResources: List<NewsResource>) = Unit
}

@ -22,5 +22,5 @@ import com.google.samples.apps.nowinandroid.core.model.data.NewsResource
* Interface for creating notifications in the app
*/
interface Notifier {
fun onNewsAdded(newsResources: List<NewsResource>)
fun postNewsNotifications(newsResources: List<NewsResource>)
}

@ -21,46 +21,56 @@ import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build.VERSION
import android.os.Build.VERSION_CODES
import androidx.browser.customtabs.CustomTabsIntent
import androidx.core.app.ActivityCompat
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationCompat.EXTRA_NOTIFICATION_ID
import androidx.core.app.NotificationCompat.InboxStyle
import androidx.core.app.NotificationManagerCompat
import androidx.core.net.toUri
import com.google.samples.apps.nowinandroid.core.model.data.NewsResource
import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject
import javax.inject.Singleton
private const val MAX_NUM_NOTIFICATIONS = 5
private const val TARGET_ACTIVITY_NAME = "com.google.samples.apps.nowinandroid.MainActivity"
private const val NEWS_NOTIFICATION_REQUEST_CODE = 0
private const val NEWS_NOTIFICATION_SUMMARY_ID = 1
private const val NEWS_NOTIFICATION_CHANNEL_ID = ""
private const val NEWS_NOTIFICATION_GROUP = "NEWS_NOTIFICATIONS"
private const val DEEP_LINK_SCHEME_AND_HOST = "https://www.nowinandroid.apps.samples.google.com"
private const val FOR_YOU_PATH = "foryou"
/**
* Implementation of [Notifier] that displays notifications in the system tray.
*/
@Singleton
class AndroidSystemNotifier @Inject constructor(
class SystemTrayNotifier @Inject constructor(
@ApplicationContext private val context: Context,
) : Notifier {
override fun onNewsAdded(
override fun postNewsNotifications(
newsResources: List<NewsResource>,
) = with(context) {
if (ActivityCompat.checkSelfPermission(
this,
permission.POST_NOTIFICATIONS,
) != PackageManager.PERMISSION_GRANTED
) return
) {
return
}
val truncatedNewsResources = newsResources
.take(MAX_NUM_NOTIFICATIONS)
val newsNotifications = newsResources.map { newsResource ->
newsNotification {
val newsNotifications = truncatedNewsResources
.map { newsResource ->
createNewsNotification {
setSmallIcon(
com.google.samples.apps.nowinandroid.core.common.R.drawable.ic_nia_notification,
)
@ -71,10 +81,10 @@ class AndroidSystemNotifier @Inject constructor(
.setAutoCancel(true)
}
}
val summaryNotification = newsNotification {
val summaryNotification = createNewsNotification {
val title = getString(
R.string.news_notification_group_summary,
newsNotifications.size,
truncatedNewsResources.size,
)
setContentTitle(title)
.setContentText(title)
@ -82,30 +92,31 @@ class AndroidSystemNotifier @Inject constructor(
com.google.samples.apps.nowinandroid.core.common.R.drawable.ic_nia_notification,
)
// Build summary info into InboxStyle template.
.setStyle(newsInboxStyle(newsResources, title))
.setStyle(newsNotificationStyle(truncatedNewsResources, title))
.setGroup(NEWS_NOTIFICATION_GROUP)
.setGroupSummary(true)
.setAutoCancel(true)
.build()
}
with(NotificationManagerCompat.from(this)) {
// Send the notifications
val notificationManager = NotificationManagerCompat.from(this)
newsNotifications.forEachIndexed { index, notification ->
notify(newsResources[index].id.hashCode(), notification)
}
notify(NEWS_NOTIFICATION_SUMMARY_ID, summaryNotification)
notificationManager.notify(
truncatedNewsResources[index].id.hashCode(),
notification,
)
}
notificationManager.notify(NEWS_NOTIFICATION_SUMMARY_ID, summaryNotification)
}
/**
* Creates an inbox style summary notification for news updates
*/
private fun newsInboxStyle(
private fun newsNotificationStyle(
newsResources: List<NewsResource>,
title: String,
): InboxStyle = newsResources
// Show at most 5 lines
.take(5)
.fold(InboxStyle()) { inboxStyle, newsResource ->
inboxStyle.addLine(newsResource.title)
}
@ -116,10 +127,10 @@ class AndroidSystemNotifier @Inject constructor(
/**
* Creates a notification for configured for news updates
*/
private fun Context.newsNotification(
private fun Context.createNewsNotification(
block: NotificationCompat.Builder.() -> Unit,
): Notification {
ensureNotificationChannel()
ensureNotificationChannelExists()
return NotificationCompat.Builder(
this,
NEWS_NOTIFICATION_CHANNEL_ID,
@ -132,7 +143,7 @@ private fun Context.newsNotification(
/**
* Ensures the a notification channel is is present if applicable
*/
private fun Context.ensureNotificationChannel() {
private fun Context.ensureNotificationChannelExists() {
if (VERSION.SDK_INT < VERSION_CODES.O) return
val channel = NotificationChannel(
@ -146,22 +157,20 @@ private fun Context.ensureNotificationChannel() {
NotificationManagerCompat.from(this).createNotificationChannel(channel)
}
private fun Context.newsPendingIntent(newsResource: NewsResource): PendingIntent? =
PendingIntent.getActivity(
private fun Context.newsPendingIntent(
newsResource: NewsResource,
): PendingIntent? = PendingIntent.getActivity(
this,
NEWS_NOTIFICATION_REQUEST_CODE,
// TODO: Read color from material theme to style the chrome custom tab
// this is currently only readable from composition.
CustomTabsIntent.Builder()
.build()
.apply {
intent.data = Uri.parse(newsResource.url)
if (VERSION.SDK_INT < VERSION_CODES.O) {
intent.putExtra(
EXTRA_NOTIFICATION_ID,
newsResource.id.hashCode(),
Intent().apply {
action = Intent.ACTION_VIEW
data = newsResource.newsDeepLinkUri()
component = ComponentName(
packageName,
TARGET_ACTIVITY_NAME,
)
}
}.intent,
},
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
)
)
private fun NewsResource.newsDeepLinkUri() = "$DEEP_LINK_SCHEME_AND_HOST/$FOR_YOU_PATH/$id".toUri()

@ -28,7 +28,7 @@ class TestNotifier : Notifier {
val addedNewsResources: List<List<NewsResource>> = mutableAddedNewResources
override fun onNewsAdded(newsResources: List<NewsResource>) {
override fun postNewsNotifications(newsResources: List<NewsResource>) {
mutableAddedNewResources.add(newsResources)
}
}

@ -52,11 +52,13 @@ class ForYouScreenTest {
isSyncing = false,
onboardingUiState = OnboardingUiState.Loading,
feedState = NewsFeedUiState.Loading,
deepLinkedUserNewsResource = null,
onTopicCheckedChanged = { _, _ -> },
onTopicClick = {},
saveFollowedTopics = {},
onNewsResourcesCheckedChanged = { _, _ -> },
onNewsResourceViewed = {},
onDeepLinkOpened = {},
)
}
}
@ -76,11 +78,13 @@ class ForYouScreenTest {
isSyncing = true,
onboardingUiState = OnboardingUiState.NotShown,
feedState = NewsFeedUiState.Success(emptyList()),
deepLinkedUserNewsResource = null,
onTopicCheckedChanged = { _, _ -> },
onTopicClick = {},
saveFollowedTopics = {},
onNewsResourcesCheckedChanged = { _, _ -> },
onNewsResourceViewed = {},
onDeepLinkOpened = {},
)
}
}
@ -106,11 +110,13 @@ class ForYouScreenTest {
feedState = NewsFeedUiState.Success(
feed = emptyList(),
),
deepLinkedUserNewsResource = null,
onTopicCheckedChanged = { _, _ -> },
onTopicClick = {},
saveFollowedTopics = {},
onNewsResourcesCheckedChanged = { _, _ -> },
onNewsResourceViewed = {},
onDeepLinkOpened = {},
)
}
}
@ -151,11 +157,13 @@ class ForYouScreenTest {
feedState = NewsFeedUiState.Success(
feed = emptyList(),
),
deepLinkedUserNewsResource = null,
onTopicCheckedChanged = { _, _ -> },
onTopicClick = {},
saveFollowedTopics = {},
onNewsResourcesCheckedChanged = { _, _ -> },
onNewsResourceViewed = {},
onDeepLinkOpened = {},
)
}
}
@ -189,11 +197,13 @@ class ForYouScreenTest {
onboardingUiState =
OnboardingUiState.Shown(topics = followableTopicTestData),
feedState = NewsFeedUiState.Loading,
deepLinkedUserNewsResource = null,
onTopicCheckedChanged = { _, _ -> },
onTopicClick = {},
saveFollowedTopics = {},
onNewsResourcesCheckedChanged = { _, _ -> },
onNewsResourceViewed = {},
onDeepLinkOpened = {},
)
}
}
@ -213,11 +223,13 @@ class ForYouScreenTest {
isSyncing = false,
onboardingUiState = OnboardingUiState.NotShown,
feedState = NewsFeedUiState.Loading,
deepLinkedUserNewsResource = null,
onTopicCheckedChanged = { _, _ -> },
onTopicClick = {},
saveFollowedTopics = {},
onNewsResourcesCheckedChanged = { _, _ -> },
onNewsResourceViewed = {},
onDeepLinkOpened = {},
)
}
}
@ -238,11 +250,13 @@ class ForYouScreenTest {
feedState = NewsFeedUiState.Success(
feed = userNewsResourcesTestData,
),
deepLinkedUserNewsResource = null,
onTopicCheckedChanged = { _, _ -> },
onTopicClick = {},
saveFollowedTopics = {},
onNewsResourcesCheckedChanged = { _, _ -> },
onNewsResourceViewed = {},
onDeepLinkOpened = {},
)
}

@ -16,6 +16,7 @@
package com.google.samples.apps.nowinandroid.feature.foryou
import android.net.Uri
import android.os.Build.VERSION
import android.os.Build.VERSION_CODES
import androidx.activity.compose.ReportDrawnWhen
@ -63,7 +64,9 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.layout.layout
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource
@ -91,6 +94,7 @@ import com.google.samples.apps.nowinandroid.core.ui.NewsFeedUiState
import com.google.samples.apps.nowinandroid.core.ui.TrackScreenViewEvent
import com.google.samples.apps.nowinandroid.core.ui.TrackScrollJank
import com.google.samples.apps.nowinandroid.core.ui.UserNewsResourcePreviewParameterProvider
import com.google.samples.apps.nowinandroid.core.ui.launchCustomChromeTab
import com.google.samples.apps.nowinandroid.core.ui.newsFeed
@Composable
@ -102,12 +106,15 @@ internal fun ForYouRoute(
val onboardingUiState by viewModel.onboardingUiState.collectAsStateWithLifecycle()
val feedState by viewModel.feedState.collectAsStateWithLifecycle()
val isSyncing by viewModel.isSyncing.collectAsStateWithLifecycle()
val deepLinkedUserNewsResource by viewModel.deepLinkedNewsResource.collectAsStateWithLifecycle()
ForYouScreen(
isSyncing = isSyncing,
onboardingUiState = onboardingUiState,
feedState = feedState,
deepLinkedUserNewsResource = deepLinkedUserNewsResource,
onTopicCheckedChanged = viewModel::updateTopicSelection,
onDeepLinkOpened = viewModel::onDeepLinkOpened,
onTopicClick = onTopicClick,
saveFollowedTopics = viewModel::dismissOnboarding,
onNewsResourcesCheckedChanged = viewModel::updateNewsResourceSaved,
@ -121,8 +128,10 @@ internal fun ForYouScreen(
isSyncing: Boolean,
onboardingUiState: OnboardingUiState,
feedState: NewsFeedUiState,
deepLinkedUserNewsResource: UserNewsResource?,
onTopicCheckedChanged: (String, Boolean) -> Unit,
onTopicClick: (String) -> Unit,
onDeepLinkOpened: (String) -> Unit,
saveFollowedTopics: () -> Unit,
onNewsResourcesCheckedChanged: (String, Boolean) -> Unit,
onNewsResourceViewed: (String) -> Unit,
@ -205,7 +214,11 @@ internal fun ForYouScreen(
}
}
TrackScreenViewEvent(screenName = "ForYou")
requestNotificationsPermission()
NotificationPermissionEffect()
DeepLinkEffect(
deepLinkedUserNewsResource,
onDeepLinkOpened,
)
}
/**
@ -392,7 +405,7 @@ fun TopicIcon(
@Composable
@OptIn(ExperimentalPermissionsApi::class)
private fun requestNotificationsPermission() {
private fun NotificationPermissionEffect() {
if (VERSION.SDK_INT < VERSION_CODES.TIRAMISU) return
val notificationsPermissionState = rememberPermissionState(
android.Manifest.permission.POST_NOTIFICATIONS,
@ -405,6 +418,26 @@ private fun requestNotificationsPermission() {
}
}
@Composable
private fun DeepLinkEffect(
userNewsResource: UserNewsResource?,
onDeepLinkOpened: (String) -> Unit,
) {
val context = LocalContext.current
val backgroundColor = MaterialTheme.colorScheme.background.toArgb()
LaunchedEffect(userNewsResource) {
if (userNewsResource == null) return@LaunchedEffect
if (!userNewsResource.hasBeenViewed) onDeepLinkOpened(userNewsResource.id)
launchCustomChromeTab(
context = context,
uri = Uri.parse(userNewsResource.url),
toolbarColor = backgroundColor,
)
}
}
@DevicePreviews
@Composable
fun ForYouScreenPopulatedFeed(
@ -419,11 +452,13 @@ fun ForYouScreenPopulatedFeed(
feedState = NewsFeedUiState.Success(
feed = userNewsResources,
),
deepLinkedUserNewsResource = null,
onTopicCheckedChanged = { _, _ -> },
saveFollowedTopics = {},
onNewsResourcesCheckedChanged = { _, _ -> },
onNewsResourceViewed = {},
onTopicClick = {},
onDeepLinkOpened = {},
)
}
}
@ -443,11 +478,13 @@ fun ForYouScreenOfflinePopulatedFeed(
feedState = NewsFeedUiState.Success(
feed = userNewsResources,
),
deepLinkedUserNewsResource = null,
onTopicCheckedChanged = { _, _ -> },
saveFollowedTopics = {},
onNewsResourcesCheckedChanged = { _, _ -> },
onNewsResourceViewed = {},
onTopicClick = {},
onDeepLinkOpened = {},
)
}
}
@ -470,11 +507,13 @@ fun ForYouScreenTopicSelection(
feedState = NewsFeedUiState.Success(
feed = userNewsResources,
),
deepLinkedUserNewsResource = null,
onTopicCheckedChanged = { _, _ -> },
saveFollowedTopics = {},
onNewsResourcesCheckedChanged = { _, _ -> },
onNewsResourceViewed = {},
onTopicClick = {},
onDeepLinkOpened = {},
)
}
}
@ -489,11 +528,13 @@ fun ForYouScreenLoading() {
isSyncing = false,
onboardingUiState = OnboardingUiState.Loading,
feedState = NewsFeedUiState.Loading,
deepLinkedUserNewsResource = null,
onTopicCheckedChanged = { _, _ -> },
saveFollowedTopics = {},
onNewsResourcesCheckedChanged = { _, _ -> },
onNewsResourceViewed = {},
onTopicClick = {},
onDeepLinkOpened = {},
)
}
}
@ -513,11 +554,13 @@ fun ForYouScreenPopulatedAndLoading(
feedState = NewsFeedUiState.Success(
feed = userNewsResources,
),
deepLinkedUserNewsResource = null,
onTopicCheckedChanged = { _, _ -> },
saveFollowedTopics = {},
onNewsResourcesCheckedChanged = { _, _ -> },
onNewsResourceViewed = {},
onTopicClick = {},
onDeepLinkOpened = {},
)
}
}

@ -16,18 +16,23 @@
package com.google.samples.apps.nowinandroid.feature.foryou
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.google.samples.apps.nowinandroid.core.data.repository.NewsResourceQuery
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.SyncManager
import com.google.samples.apps.nowinandroid.core.domain.GetFollowableTopicsUseCase
import com.google.samples.apps.nowinandroid.core.ui.NewsFeedUiState
import com.google.samples.apps.nowinandroid.feature.foryou.navigation.LINKED_NEWS_RESOURCE_ID
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
@ -35,6 +40,7 @@ import javax.inject.Inject
@HiltViewModel
class ForYouViewModel @Inject constructor(
private val savedStateHandle: SavedStateHandle,
syncManager: SyncManager,
private val userDataRepository: UserDataRepository,
userNewsResourceRepository: UserNewsResourceRepository,
@ -44,6 +50,28 @@ class ForYouViewModel @Inject constructor(
private val shouldShowOnboarding: Flow<Boolean> =
userDataRepository.userData.map { !it.shouldHideOnboarding }
val deepLinkedNewsResource = savedStateHandle.getStateFlow<String?>(
key = LINKED_NEWS_RESOURCE_ID,
null,
)
.flatMapLatest { newsResourceId ->
if (newsResourceId == null) {
flowOf(emptyList())
} else {
userNewsResourceRepository.observeAll(
NewsResourceQuery(
filterNewsIds = setOf(newsResourceId),
),
)
}
}
.map { it.firstOrNull() }
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = null,
)
val isSyncing = syncManager.isSyncing
.stateIn(
scope = viewModelScope,
@ -95,6 +123,18 @@ class ForYouViewModel @Inject constructor(
}
}
fun onDeepLinkOpened(newsResourceId: String) {
if (newsResourceId == deepLinkedNewsResource.value?.id) {
savedStateHandle[LINKED_NEWS_RESOURCE_ID] = null
}
viewModelScope.launch {
userDataRepository.setNewsResourceViewed(
newsResourceId = newsResourceId,
viewed = true,
)
}
}
fun dismissOnboarding() {
viewModelScope.launch {
userDataRepository.setShouldHideOnboarding(true)

@ -19,17 +19,31 @@ package com.google.samples.apps.nowinandroid.feature.foryou.navigation
import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavOptions
import androidx.navigation.NavType
import androidx.navigation.compose.composable
import androidx.navigation.navArgument
import androidx.navigation.navDeepLink
import com.google.samples.apps.nowinandroid.feature.foryou.ForYouRoute
const val LINKED_NEWS_RESOURCE_ID = "linkedNewsResourceId"
const val forYouNavigationRoute = "for_you_route"
private const val DEEP_LINK_URI_PATTERN =
"https://www.nowinandroid.apps.samples.google.com/foryou/{$LINKED_NEWS_RESOURCE_ID}"
fun NavController.navigateToForYou(navOptions: NavOptions? = null) {
this.navigate(forYouNavigationRoute, navOptions)
}
fun NavGraphBuilder.forYouScreen(onTopicClick: (String) -> Unit) {
composable(route = forYouNavigationRoute) {
composable(
route = forYouNavigationRoute,
deepLinks = listOf(
navDeepLink { uriPattern = DEEP_LINK_URI_PATTERN },
),
arguments = listOf(
navArgument(LINKED_NEWS_RESOURCE_ID) { type = NavType.StringType },
),
) {
ForYouRoute(onTopicClick)
}
}

@ -16,6 +16,7 @@
package com.google.samples.apps.nowinandroid.feature.foryou
import androidx.lifecycle.SavedStateHandle
import com.google.samples.apps.nowinandroid.core.data.repository.CompositeUserNewsResourceRepository
import com.google.samples.apps.nowinandroid.core.domain.GetFollowableTopicsUseCase
import com.google.samples.apps.nowinandroid.core.model.data.FollowableTopic
@ -32,6 +33,7 @@ import com.google.samples.apps.nowinandroid.core.testing.util.MainDispatcherRule
import com.google.samples.apps.nowinandroid.core.testing.util.TestNetworkMonitor
import com.google.samples.apps.nowinandroid.core.testing.util.TestSyncManager
import com.google.samples.apps.nowinandroid.core.ui.NewsFeedUiState
import com.google.samples.apps.nowinandroid.feature.foryou.navigation.LINKED_NEWS_RESOURCE_ID
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.UnconfinedTestDispatcher
@ -42,6 +44,7 @@ import org.junit.Before
import org.junit.Rule
import org.junit.Test
import kotlin.test.assertEquals
import kotlin.test.assertNull
/**
* To learn more about how this test handles Flows created with stateIn, see
@ -65,12 +68,14 @@ class ForYouViewModelTest {
topicsRepository = topicsRepository,
userDataRepository = userDataRepository,
)
private val savedStateHandle = SavedStateHandle()
private lateinit var viewModel: ForYouViewModel
@Before
fun setup() {
viewModel = ForYouViewModel(
syncManager = syncManager,
savedStateHandle = savedStateHandle,
userDataRepository = userDataRepository,
userNewsResourceRepository = userNewsResourceRepository,
getFollowableTopics = getFollowableTopicsUseCase,
@ -455,6 +460,34 @@ class ForYouViewModelTest {
collectJob1.cancel()
collectJob2.cancel()
}
@Test
fun deepLinkedNewsResourceIsFetchedAndResetAfterViewing() = runTest {
val collectJob =
launch(UnconfinedTestDispatcher()) { viewModel.deepLinkedNewsResource.collect() }
newsRepository.sendNewsResources(sampleNewsResources)
userDataRepository.setUserData(emptyUserData)
savedStateHandle[LINKED_NEWS_RESOURCE_ID] = sampleNewsResources.first().id
assertEquals(
expected = UserNewsResource(
newsResource = sampleNewsResources.first(),
userData = emptyUserData,
),
actual = viewModel.deepLinkedNewsResource.value,
)
viewModel.onDeepLinkOpened(
newsResourceId = sampleNewsResources.first().id,
)
assertNull(
viewModel.deepLinkedNewsResource.value,
)
collectJob.cancel()
}
}
private val sampleTopics = listOf(

Loading…
Cancel
Save