Address review feedback

Change-Id: If336af8fa5c58d6401dfd7a561a1a340ff388175
recent_search
thagikura 2 years ago
parent cd03bd16e7
commit 37d4b65325

@ -54,9 +54,7 @@ fun NiaNavHost(
searchScreen( searchScreen(
onBackClick = navController::popBackStack, onBackClick = navController::popBackStack,
onInterestsClick = { appState.navigateToTopLevelDestination(INTERESTS) }, onInterestsClick = { appState.navigateToTopLevelDestination(INTERESTS) },
onTopicClick = { onTopicClick = navController::navigateToTopic,
navController.navigateToTopic(it)
},
) )
interestsGraph( interestsGraph(
onTopicClick = { topicId -> onTopicClick = { topicId ->

@ -27,7 +27,11 @@ import com.google.samples.apps.nowinandroid.core.network.NiaDispatchers.IO
import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flattenConcat import kotlinx.coroutines.flow.flattenConcat
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import javax.inject.Inject import javax.inject.Inject
@ -54,16 +58,21 @@ class DefaultSearchContentsRepository @Inject constructor(
val newsResourceIds = newsResourceFtsDao.searchAllNewsResources("*$searchQuery*") val newsResourceIds = newsResourceFtsDao.searchAllNewsResources("*$searchQuery*")
val topicIds = topicFtsDao.searchAllTopics("*$searchQuery*") val topicIds = topicFtsDao.searchAllTopics("*$searchQuery*")
return combine(newsResourceIds, topicIds) { newsFlow, topicsFlow -> val newsResourcesFlow = newsResourceIds
combine( .mapLatest { it.toSet() }
newsResourceDao.getNewsResources(filterNewsIds = newsFlow.toSet()), .distinctUntilChanged()
topicDao.getTopicEntities(topicsFlow.toSet()), .flatMapLatest {
) { newsResources, topics -> newsResourceDao.getNewsResources(filterNewsIds = it)
SearchResult(
topics = topics.map { it.asExternalModel() },
newsResources = newsResources.map { it.asExternalModel() },
)
} }
}.flattenConcat() val topicsFlow = topicIds
.mapLatest { it.toSet() }
.distinctUntilChanged()
.flatMapLatest(topicDao::getTopicEntities)
return combine(newsResourcesFlow, topicsFlow) { newsResources, topics ->
SearchResult(
topics = topics.map { it.asExternalModel() },
newsResources = newsResources.map { it.asExternalModel() },
)
}
} }
} }

@ -29,6 +29,8 @@ sealed interface SearchResultUiState {
*/ */
object EmptyQuery : SearchResultUiState object EmptyQuery : SearchResultUiState
object LoadFailed: SearchResultUiState
data class Success( data class Success(
val topics: List<FollowableTopic> = emptyList(), val topics: List<FollowableTopic> = emptyList(),
val newsResources: List<UserNewsResource> = emptyList(), val newsResources: List<UserNewsResource> = emptyList(),

@ -43,9 +43,7 @@ import androidx.compose.material3.TextField
import androidx.compose.material3.TextFieldDefaults import androidx.compose.material3.TextFieldDefaults
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@ -91,6 +89,7 @@ internal fun SearchRoute(
forYouViewModel: ForYouViewModel = hiltViewModel(), forYouViewModel: ForYouViewModel = hiltViewModel(),
) { ) {
val uiState by searchViewModel.searchResultUiState.collectAsStateWithLifecycle() val uiState by searchViewModel.searchResultUiState.collectAsStateWithLifecycle()
val searchQuery by searchViewModel.searchQuery.collectAsStateWithLifecycle()
SearchScreen( SearchScreen(
modifier = modifier, modifier = modifier,
onBackClick = onBackClick, onBackClick = onBackClick,
@ -99,6 +98,7 @@ internal fun SearchRoute(
onSearchQueryChanged = searchViewModel::onSearchQueryChanged, onSearchQueryChanged = searchViewModel::onSearchQueryChanged,
onTopicClick = onTopicClick, onTopicClick = onTopicClick,
onNewsResourcesCheckedChanged = forYouViewModel::updateNewsResourceSaved, onNewsResourcesCheckedChanged = forYouViewModel::updateNewsResourceSaved,
searchQuery = searchQuery,
uiState = uiState, uiState = uiState,
) )
} }
@ -112,9 +112,9 @@ internal fun SearchScreen(
onNewsResourcesCheckedChanged: (String, Boolean) -> Unit = { _, _ -> }, onNewsResourcesCheckedChanged: (String, Boolean) -> Unit = { _, _ -> },
onSearchQueryChanged: (String) -> Unit = {}, onSearchQueryChanged: (String) -> Unit = {},
onTopicClick: (String) -> Unit = {}, onTopicClick: (String) -> Unit = {},
searchQuery: String = "",
uiState: SearchResultUiState = SearchResultUiState.Loading, uiState: SearchResultUiState = SearchResultUiState.Loading,
) { ) {
val searchQuery = remember { mutableStateOf("") }
TrackScreenViewEvent(screenName = "Search") TrackScreenViewEvent(screenName = "Search")
Column(modifier = modifier) { Column(modifier = modifier) {
Spacer(Modifier.windowInsetsTopHeight(WindowInsets.safeDrawing)) Spacer(Modifier.windowInsetsTopHeight(WindowInsets.safeDrawing))
@ -124,7 +124,8 @@ internal fun SearchScreen(
searchQuery = searchQuery, searchQuery = searchQuery,
) )
when (uiState) { when (uiState) {
SearchResultUiState.Loading -> Unit SearchResultUiState.Loading,
SearchResultUiState.LoadFailed,
SearchResultUiState.EmptyQuery -> Unit SearchResultUiState.EmptyQuery -> Unit
is SearchResultUiState.Success -> { is SearchResultUiState.Success -> {
if (uiState.isEmpty()) { if (uiState.isEmpty()) {
@ -150,12 +151,11 @@ internal fun SearchScreen(
@Composable @Composable
fun EmptySearchResultBody( fun EmptySearchResultBody(
onInterestsClick: () -> Unit = {}, onInterestsClick: () -> Unit = {},
searchQuery: MutableState<String>, searchQuery: String,
) { ) {
Column(horizontalAlignment = Alignment.CenterHorizontally) { Column(horizontalAlignment = Alignment.CenterHorizontally) {
val queryValue = searchQuery.value val message = stringResource(id = searchR.string.search_result_not_found, searchQuery)
val message = stringResource(id = searchR.string.search_result_not_found, queryValue) val start = message.indexOf(searchQuery)
val start = message.indexOf(queryValue)
Text( Text(
text = AnnotatedString( text = AnnotatedString(
text = message, text = message,
@ -163,7 +163,7 @@ fun EmptySearchResultBody(
AnnotatedString.Range( AnnotatedString.Range(
SpanStyle(fontWeight = FontWeight.Bold), SpanStyle(fontWeight = FontWeight.Bold),
start = start, start = start,
end = start + queryValue.length, end = start + searchQuery.length,
), ),
), ),
), ),
@ -261,7 +261,7 @@ private fun SearchToolbar(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
onBackClick: () -> Unit = {}, onBackClick: () -> Unit = {},
onSearchQueryChanged: (String) -> Unit = {}, onSearchQueryChanged: (String) -> Unit = {},
searchQuery: MutableState<String> = mutableStateOf(""), searchQuery: String = "",
) { ) {
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
@ -286,7 +286,7 @@ private fun SearchToolbar(
@Composable @Composable
private fun SearchTextField( private fun SearchTextField(
onSearchQueryChanged: (String) -> Unit, onSearchQueryChanged: (String) -> Unit,
searchQuery: MutableState<String>, searchQuery: String,
) { ) {
val focusRequester = remember { FocusRequester() } val focusRequester = remember { FocusRequester() }
TextField( TextField(
@ -306,7 +306,6 @@ private fun SearchTextField(
}, },
trailingIcon = { trailingIcon = {
IconButton(onClick = { IconButton(onClick = {
searchQuery.value = ""
onSearchQueryChanged("") onSearchQueryChanged("")
}) { }) {
Icon( Icon(
@ -319,7 +318,6 @@ private fun SearchTextField(
} }
}, },
onValueChange = { onValueChange = {
searchQuery.value = it
onSearchQueryChanged(it) onSearchQueryChanged(it)
}, },
modifier = Modifier modifier = Modifier
@ -327,7 +325,7 @@ private fun SearchTextField(
.padding(16.dp) .padding(16.dp)
.focusRequester(focusRequester), .focusRequester(focusRequester),
shape = RoundedCornerShape(32.dp), shape = RoundedCornerShape(32.dp),
value = searchQuery.value, value = searchQuery,
) )
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
focusRequester.requestFocus() focusRequester.requestFocus()
@ -346,8 +344,7 @@ private fun SearchToolbarPreview() {
@Composable @Composable
private fun EmptySearchResultColumnPreview() { private fun EmptySearchResultColumnPreview() {
NiaTheme { NiaTheme {
val searchQuery = remember { mutableStateOf("C++") } EmptySearchResultBody(searchQuery = "C++")
EmptySearchResultBody(searchQuery = searchQuery)
} }
} }

@ -16,11 +16,12 @@
package com.google.samples.apps.nowinandroid.feature.search package com.google.samples.apps.nowinandroid.feature.search
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.google.samples.apps.nowinandroid.core.domain.GetSearchContentsUseCase import com.google.samples.apps.nowinandroid.core.domain.GetSearchContentsUseCase
import com.google.samples.apps.nowinandroid.feature.search.SearchResultUiState.LoadFailed
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flatMapLatest
@ -33,20 +34,25 @@ import javax.inject.Inject
class SearchViewModel @Inject constructor( class SearchViewModel @Inject constructor(
// TODO: Add GetSearchContentsCountUseCase to check if the fts tables are populated // TODO: Add GetSearchContentsCountUseCase to check if the fts tables are populated
getSearchContentsUseCase: GetSearchContentsUseCase, getSearchContentsUseCase: GetSearchContentsUseCase,
private val savedStateHandle: SavedStateHandle,
) : ViewModel() { ) : ViewModel() {
val searchQuery = MutableStateFlow("") val searchQuery = savedStateHandle.getStateFlow("searchQuery", "")
val searchResultUiState: StateFlow<SearchResultUiState> = val searchResultUiState: StateFlow<SearchResultUiState> =
searchQuery.flatMapLatest { query -> searchQuery.flatMapLatest { query ->
if (query.length < 2) { if (query.length < 2) {
flowOf(SearchResultUiState.EmptyQuery) flowOf(SearchResultUiState.EmptyQuery)
} else { } else {
getSearchContentsUseCase(query).map { try {
SearchResultUiState.Success( getSearchContentsUseCase(query).map {
topics = it.topics, SearchResultUiState.Success(
newsResources = it.newsResources, topics = it.topics,
) newsResources = it.newsResources,
)
}
} catch (exception: Exception) {
flowOf(LoadFailed)
} }
} }
}.stateIn( }.stateIn(
@ -56,6 +62,6 @@ class SearchViewModel @Inject constructor(
) )
fun onSearchQueryChanged(query: String) { fun onSearchQueryChanged(query: String) {
searchQuery.value = query savedStateHandle["searchQuery"] = query
} }
} }

@ -16,6 +16,7 @@
package com.google.samples.apps.nowinandroid.feature.search package com.google.samples.apps.nowinandroid.feature.search
import androidx.lifecycle.SavedStateHandle
import com.google.samples.apps.nowinandroid.core.domain.GetSearchContentsUseCase import com.google.samples.apps.nowinandroid.core.domain.GetSearchContentsUseCase
import com.google.samples.apps.nowinandroid.core.testing.data.newsResourcesTestData import com.google.samples.apps.nowinandroid.core.testing.data.newsResourcesTestData
import com.google.samples.apps.nowinandroid.core.testing.data.topicsTestData import com.google.samples.apps.nowinandroid.core.testing.data.topicsTestData
@ -54,6 +55,7 @@ class SearchViewModelTest {
fun setup() { fun setup() {
viewModel = SearchViewModel( viewModel = SearchViewModel(
getSearchContentsUseCase = getSearchContentsUseCase, getSearchContentsUseCase = getSearchContentsUseCase,
savedStateHandle = SavedStateHandle()
) )
} }

Loading…
Cancel
Save