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