parent
9cd390c56a
commit
6c6538ff83
@ -0,0 +1,97 @@
|
||||
/*
|
||||
* Copyright 2023 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.feature.interests
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.slideInHorizontally
|
||||
import androidx.compose.animation.slideOutHorizontally
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.safeDrawingPadding
|
||||
import androidx.compose.foundation.layout.widthIn
|
||||
import androidx.compose.foundation.lazy.grid.LazyGridState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
|
||||
@Composable
|
||||
internal fun InterestsRoute(
|
||||
listState: LazyGridState,
|
||||
shouldShowTwoPane: Boolean,
|
||||
onTopicClick: (String) -> Unit,
|
||||
onBackClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
viewModel: InterestsViewModel = hiltViewModel(),
|
||||
) {
|
||||
val interestUiState by viewModel.interestUiState.collectAsStateWithLifecycle()
|
||||
val topicUiState by viewModel.topicUiState.collectAsStateWithLifecycle()
|
||||
|
||||
Row(modifier = modifier.fillMaxSize()) {
|
||||
if (shouldShowTwoPane || topicUiState == null) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxHeight()
|
||||
.then(
|
||||
if (topicUiState != null) {
|
||||
Modifier.widthIn(min = 350.dp)
|
||||
} else {
|
||||
Modifier.weight(1f)
|
||||
},
|
||||
),
|
||||
) {
|
||||
InterestsScreen(
|
||||
uiState = interestUiState,
|
||||
listState = listState,
|
||||
followTopic = viewModel::followTopic,
|
||||
onTopicClick = onTopicClick,
|
||||
modifier = Modifier.matchParentSize(),
|
||||
)
|
||||
}
|
||||
}
|
||||
AnimatedVisibility(
|
||||
visible = topicUiState != null,
|
||||
enter = slideInHorizontally(initialOffsetX = { it / 2 }),
|
||||
exit = slideOutHorizontally(targetOffsetX = { it / 2 }),
|
||||
modifier = Modifier
|
||||
.fillMaxHeight()
|
||||
.weight(1f)
|
||||
.run {
|
||||
if (!shouldShowTwoPane) {
|
||||
safeDrawingPadding()
|
||||
} else {
|
||||
this
|
||||
}
|
||||
},
|
||||
) {
|
||||
topicUiState?.let { state ->
|
||||
TopicScreen(
|
||||
topicUiState = state,
|
||||
onBackClick = onBackClick,
|
||||
onFollowClick = viewModel::followTopic,
|
||||
onTopicClick = onTopicClick,
|
||||
onBookmarkChanged = viewModel::bookmarkNews,
|
||||
onNewsResourceViewed = viewModel::newsViewed,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
/*
|
||||
* Copyright 2023 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.feature.interests
|
||||
|
||||
import com.google.samples.apps.nowinandroid.core.model.data.FollowableTopic
|
||||
|
||||
sealed interface InterestsUiState {
|
||||
object Loading : InterestsUiState
|
||||
|
||||
data class Interests(
|
||||
val topics: List<FollowableTopic>,
|
||||
val selectedTopicId: String?,
|
||||
) : InterestsUiState
|
||||
|
||||
object Empty : InterestsUiState
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
/*
|
||||
* Copyright 2023 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.feature.interests
|
||||
|
||||
import com.google.samples.apps.nowinandroid.core.model.data.FollowableTopic
|
||||
import com.google.samples.apps.nowinandroid.core.model.data.UserNewsResource
|
||||
|
||||
sealed interface TopicUiState {
|
||||
data class Success(
|
||||
val followableTopic: FollowableTopic,
|
||||
val newsResources: List<UserNewsResource>,
|
||||
) : TopicUiState
|
||||
|
||||
object Error : TopicUiState
|
||||
object Loading : TopicUiState
|
||||
}
|
@ -0,0 +1,100 @@
|
||||
/*
|
||||
* Copyright 2023 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.interests
|
||||
|
||||
import com.google.samples.apps.nowinandroid.core.model.data.FollowableTopic
|
||||
import com.google.samples.apps.nowinandroid.core.model.data.Topic
|
||||
|
||||
private const val TOPIC_1_NAME = "Android Studio"
|
||||
private const val TOPIC_2_NAME = "Build"
|
||||
private const val TOPIC_3_NAME = "Compose"
|
||||
private const val TOPIC_SHORT_DESC = "At vero eos et accusamus."
|
||||
private const val TOPIC_LONG_DESC = "At vero eos et accusamus et iusto odio dignissimos ducimus."
|
||||
private const val TOPIC_URL = "URL"
|
||||
private const val TOPIC_IMAGE_URL = "Image URL"
|
||||
|
||||
internal val testInputTopics = listOf(
|
||||
FollowableTopic(
|
||||
Topic(
|
||||
id = "0",
|
||||
name = TOPIC_1_NAME,
|
||||
shortDescription = TOPIC_SHORT_DESC,
|
||||
longDescription = TOPIC_LONG_DESC,
|
||||
url = TOPIC_URL,
|
||||
imageUrl = TOPIC_IMAGE_URL,
|
||||
),
|
||||
isFollowed = true,
|
||||
),
|
||||
FollowableTopic(
|
||||
Topic(
|
||||
id = "1",
|
||||
name = TOPIC_2_NAME,
|
||||
shortDescription = TOPIC_SHORT_DESC,
|
||||
longDescription = TOPIC_LONG_DESC,
|
||||
url = TOPIC_URL,
|
||||
imageUrl = TOPIC_IMAGE_URL,
|
||||
),
|
||||
isFollowed = false,
|
||||
),
|
||||
FollowableTopic(
|
||||
Topic(
|
||||
id = "2",
|
||||
name = TOPIC_3_NAME,
|
||||
shortDescription = TOPIC_SHORT_DESC,
|
||||
longDescription = TOPIC_LONG_DESC,
|
||||
url = TOPIC_URL,
|
||||
imageUrl = TOPIC_IMAGE_URL,
|
||||
),
|
||||
isFollowed = false,
|
||||
),
|
||||
)
|
||||
|
||||
internal val testOutputTopics = listOf(
|
||||
FollowableTopic(
|
||||
Topic(
|
||||
id = "0",
|
||||
name = TOPIC_1_NAME,
|
||||
shortDescription = TOPIC_SHORT_DESC,
|
||||
longDescription = TOPIC_LONG_DESC,
|
||||
url = TOPIC_URL,
|
||||
imageUrl = TOPIC_IMAGE_URL,
|
||||
),
|
||||
isFollowed = true,
|
||||
),
|
||||
FollowableTopic(
|
||||
Topic(
|
||||
id = "1",
|
||||
name = TOPIC_2_NAME,
|
||||
shortDescription = TOPIC_SHORT_DESC,
|
||||
longDescription = TOPIC_LONG_DESC,
|
||||
url = TOPIC_URL,
|
||||
imageUrl = TOPIC_IMAGE_URL,
|
||||
),
|
||||
isFollowed = true,
|
||||
),
|
||||
FollowableTopic(
|
||||
Topic(
|
||||
id = "2",
|
||||
name = TOPIC_3_NAME,
|
||||
shortDescription = TOPIC_SHORT_DESC,
|
||||
longDescription = TOPIC_LONG_DESC,
|
||||
url = TOPIC_URL,
|
||||
imageUrl = TOPIC_IMAGE_URL,
|
||||
),
|
||||
isFollowed = false,
|
||||
),
|
||||
)
|
@ -1 +0,0 @@
|
||||
/build
|
@ -1,3 +0,0 @@
|
||||
# :feature:topic module
|
||||
|
||||

|
@ -1,140 +0,0 @@
|
||||
/*
|
||||
* Copyright 2022 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.feature.topic
|
||||
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.compose.ui.test.hasScrollToNodeAction
|
||||
import androidx.compose.ui.test.hasText
|
||||
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
||||
import androidx.compose.ui.test.onFirst
|
||||
import androidx.compose.ui.test.onNodeWithContentDescription
|
||||
import androidx.compose.ui.test.onNodeWithText
|
||||
import androidx.compose.ui.test.performScrollToNode
|
||||
import com.google.samples.apps.nowinandroid.core.testing.data.followableTopicTestData
|
||||
import com.google.samples.apps.nowinandroid.core.testing.data.userNewsResourcesTestData
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
|
||||
/**
|
||||
* UI test for checking the correct behaviour of the Topic screen;
|
||||
* Verifies that, when a specific UiState is set, the corresponding
|
||||
* composables and details are shown
|
||||
*/
|
||||
class TopicScreenTest {
|
||||
|
||||
@get:Rule
|
||||
val composeTestRule = createAndroidComposeRule<ComponentActivity>()
|
||||
|
||||
private lateinit var topicLoading: String
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
composeTestRule.activity.apply {
|
||||
topicLoading = getString(R.string.topic_loading)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun niaLoadingWheel_whenScreenIsLoading_showLoading() {
|
||||
composeTestRule.setContent {
|
||||
TopicScreen(
|
||||
topicUiState = TopicUiState.Loading,
|
||||
newsUiState = NewsUiState.Loading,
|
||||
onBackClick = {},
|
||||
onFollowClick = {},
|
||||
onTopicClick = {},
|
||||
onBookmarkChanged = { _, _ -> },
|
||||
onNewsResourceViewed = {},
|
||||
)
|
||||
}
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithContentDescription(topicLoading)
|
||||
.assertExists()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun topicTitle_whenTopicIsSuccess_isShown() {
|
||||
val testTopic = followableTopicTestData.first()
|
||||
composeTestRule.setContent {
|
||||
TopicScreen(
|
||||
topicUiState = TopicUiState.Success(testTopic),
|
||||
newsUiState = NewsUiState.Loading,
|
||||
onBackClick = {},
|
||||
onFollowClick = {},
|
||||
onTopicClick = {},
|
||||
onBookmarkChanged = { _, _ -> },
|
||||
onNewsResourceViewed = {},
|
||||
)
|
||||
}
|
||||
|
||||
// Name is shown
|
||||
composeTestRule
|
||||
.onNodeWithText(testTopic.topic.name)
|
||||
.assertExists()
|
||||
|
||||
// Description is shown
|
||||
composeTestRule
|
||||
.onNodeWithText(testTopic.topic.longDescription)
|
||||
.assertExists()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun news_whenTopicIsLoading_isNotShown() {
|
||||
composeTestRule.setContent {
|
||||
TopicScreen(
|
||||
topicUiState = TopicUiState.Loading,
|
||||
newsUiState = NewsUiState.Success(userNewsResourcesTestData),
|
||||
onBackClick = {},
|
||||
onFollowClick = {},
|
||||
onTopicClick = {},
|
||||
onBookmarkChanged = { _, _ -> },
|
||||
onNewsResourceViewed = {},
|
||||
)
|
||||
}
|
||||
|
||||
// Loading indicator shown
|
||||
composeTestRule
|
||||
.onNodeWithContentDescription(topicLoading)
|
||||
.assertExists()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun news_whenSuccessAndTopicIsSuccess_isShown() {
|
||||
val testTopic = followableTopicTestData.first()
|
||||
composeTestRule.setContent {
|
||||
TopicScreen(
|
||||
topicUiState = TopicUiState.Success(testTopic),
|
||||
newsUiState = NewsUiState.Success(
|
||||
userNewsResourcesTestData,
|
||||
),
|
||||
onBackClick = {},
|
||||
onFollowClick = {},
|
||||
onTopicClick = {},
|
||||
onBookmarkChanged = { _, _ -> },
|
||||
onNewsResourceViewed = {},
|
||||
)
|
||||
}
|
||||
|
||||
// Scroll to first news title if available
|
||||
composeTestRule
|
||||
.onAllNodes(hasScrollToNodeAction())
|
||||
.onFirst()
|
||||
.performScrollToNode(hasText(userNewsResourcesTestData.first().title))
|
||||
}
|
||||
}
|
@ -1,19 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Copyright 2022 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
|
||||
|
||||
http://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.
|
||||
-->
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
</manifest>
|
@ -1,190 +0,0 @@
|
||||
/*
|
||||
* Copyright 2021 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.feature.topic
|
||||
|
||||
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.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.decoder.StringDecoder
|
||||
import com.google.samples.apps.nowinandroid.core.model.data.FollowableTopic
|
||||
import com.google.samples.apps.nowinandroid.core.model.data.Topic
|
||||
import com.google.samples.apps.nowinandroid.core.model.data.UserNewsResource
|
||||
import com.google.samples.apps.nowinandroid.core.result.Result
|
||||
import com.google.samples.apps.nowinandroid.core.result.asResult
|
||||
import com.google.samples.apps.nowinandroid.feature.topic.navigation.TopicArgs
|
||||
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.map
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class TopicViewModel @Inject constructor(
|
||||
savedStateHandle: SavedStateHandle,
|
||||
stringDecoder: StringDecoder,
|
||||
private val userDataRepository: UserDataRepository,
|
||||
topicsRepository: TopicsRepository,
|
||||
userNewsResourceRepository: UserNewsResourceRepository,
|
||||
) : ViewModel() {
|
||||
|
||||
private val topicArgs: TopicArgs = TopicArgs(savedStateHandle, stringDecoder)
|
||||
|
||||
val topicId = topicArgs.topicId
|
||||
|
||||
val topicUiState: StateFlow<TopicUiState> = topicUiState(
|
||||
topicId = topicArgs.topicId,
|
||||
userDataRepository = userDataRepository,
|
||||
topicsRepository = topicsRepository,
|
||||
)
|
||||
.stateIn(
|
||||
scope = viewModelScope,
|
||||
started = SharingStarted.WhileSubscribed(5_000),
|
||||
initialValue = TopicUiState.Loading,
|
||||
)
|
||||
|
||||
val newUiState: StateFlow<NewsUiState> = newsUiState(
|
||||
topicId = topicArgs.topicId,
|
||||
userDataRepository = userDataRepository,
|
||||
userNewsResourceRepository = userNewsResourceRepository,
|
||||
)
|
||||
.stateIn(
|
||||
scope = viewModelScope,
|
||||
started = SharingStarted.WhileSubscribed(5_000),
|
||||
initialValue = NewsUiState.Loading,
|
||||
)
|
||||
|
||||
fun followTopicToggle(followed: Boolean) {
|
||||
viewModelScope.launch {
|
||||
userDataRepository.toggleFollowedTopicId(topicArgs.topicId, followed)
|
||||
}
|
||||
}
|
||||
|
||||
fun bookmarkNews(newsResourceId: String, bookmarked: Boolean) {
|
||||
viewModelScope.launch {
|
||||
userDataRepository.updateNewsResourceBookmark(newsResourceId, bookmarked)
|
||||
}
|
||||
}
|
||||
|
||||
fun setNewsResourceViewed(newsResourceId: String, viewed: Boolean) {
|
||||
viewModelScope.launch {
|
||||
userDataRepository.setNewsResourceViewed(newsResourceId, viewed)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun topicUiState(
|
||||
topicId: String,
|
||||
userDataRepository: UserDataRepository,
|
||||
topicsRepository: TopicsRepository,
|
||||
): Flow<TopicUiState> {
|
||||
// Observe the followed topics, as they could change over time.
|
||||
val followedTopicIds: Flow<Set<String>> =
|
||||
userDataRepository.userData
|
||||
.map { it.followedTopics }
|
||||
|
||||
// Observe topic information
|
||||
val topicStream: Flow<Topic> = topicsRepository.getTopic(
|
||||
id = topicId,
|
||||
)
|
||||
|
||||
return combine(
|
||||
followedTopicIds,
|
||||
topicStream,
|
||||
::Pair,
|
||||
)
|
||||
.asResult()
|
||||
.map { followedTopicToTopicResult ->
|
||||
when (followedTopicToTopicResult) {
|
||||
is Result.Success -> {
|
||||
val (followedTopics, topic) = followedTopicToTopicResult.data
|
||||
val followed = followedTopics.contains(topicId)
|
||||
TopicUiState.Success(
|
||||
followableTopic = FollowableTopic(
|
||||
topic = topic,
|
||||
isFollowed = followed,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
is Result.Loading -> {
|
||||
TopicUiState.Loading
|
||||
}
|
||||
|
||||
is Result.Error -> {
|
||||
TopicUiState.Error
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun newsUiState(
|
||||
topicId: String,
|
||||
userNewsResourceRepository: UserNewsResourceRepository,
|
||||
userDataRepository: UserDataRepository,
|
||||
): Flow<NewsUiState> {
|
||||
// Observe news
|
||||
val newsStream: Flow<List<UserNewsResource>> = userNewsResourceRepository.observeAll(
|
||||
NewsResourceQuery(filterTopicIds = setOf(element = topicId)),
|
||||
)
|
||||
|
||||
// Observe bookmarks
|
||||
val bookmark: Flow<Set<String>> = userDataRepository.userData
|
||||
.map { it.bookmarkedNewsResources }
|
||||
|
||||
return combine(
|
||||
newsStream,
|
||||
bookmark,
|
||||
::Pair,
|
||||
)
|
||||
.asResult()
|
||||
.map { newsToBookmarksResult ->
|
||||
when (newsToBookmarksResult) {
|
||||
is Result.Success -> {
|
||||
val news = newsToBookmarksResult.data.first
|
||||
NewsUiState.Success(news)
|
||||
}
|
||||
|
||||
is Result.Loading -> {
|
||||
NewsUiState.Loading
|
||||
}
|
||||
|
||||
is Result.Error -> {
|
||||
NewsUiState.Error
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sealed interface TopicUiState {
|
||||
data class Success(val followableTopic: FollowableTopic) : TopicUiState
|
||||
object Error : TopicUiState
|
||||
object Loading : TopicUiState
|
||||
}
|
||||
|
||||
sealed interface NewsUiState {
|
||||
data class Success(val news: List<UserNewsResource>) : NewsUiState
|
||||
object Error : NewsUiState
|
||||
object Loading : NewsUiState
|
||||
}
|
@ -1,57 +0,0 @@
|
||||
/*
|
||||
* Copyright 2022 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.feature.topic.navigation
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.NavGraphBuilder
|
||||
import androidx.navigation.NavType
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.navArgument
|
||||
import com.google.samples.apps.nowinandroid.core.decoder.StringDecoder
|
||||
import com.google.samples.apps.nowinandroid.feature.topic.TopicRoute
|
||||
|
||||
@VisibleForTesting
|
||||
internal const val topicIdArg = "topicId"
|
||||
|
||||
internal class TopicArgs(val topicId: String) {
|
||||
constructor(savedStateHandle: SavedStateHandle, stringDecoder: StringDecoder) :
|
||||
this(stringDecoder.decodeString(checkNotNull(savedStateHandle[topicIdArg])))
|
||||
}
|
||||
|
||||
fun NavController.navigateToTopic(topicId: String) {
|
||||
val encodedId = Uri.encode(topicId)
|
||||
this.navigate("topic_route/$encodedId") {
|
||||
launchSingleTop = true
|
||||
}
|
||||
}
|
||||
|
||||
fun NavGraphBuilder.topicScreen(
|
||||
onBackClick: () -> Unit,
|
||||
onTopicClick: (String) -> Unit,
|
||||
) {
|
||||
composable(
|
||||
route = "topic_route/{$topicIdArg}",
|
||||
arguments = listOf(
|
||||
navArgument(topicIdArg) { type = NavType.StringType },
|
||||
),
|
||||
) {
|
||||
TopicRoute(onBackClick = onBackClick, onTopicClick = onTopicClick)
|
||||
}
|
||||
}
|
@ -1,19 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Copyright 2022 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
|
||||
|
||||
http://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.
|
||||
-->
|
||||
<resources>
|
||||
<string name="topic_loading">Loading topic</string>
|
||||
</resources>
|
Loading…
Reference in new issue