Add SearchResultNotReadyBody

That is displayed when *Fts tables are not populated
pull/677/head
Takeshi Hagikura 1 year ago
parent ec8db7330e
commit 962d993556

@ -74,4 +74,13 @@ class DefaultSearchContentsRepository @Inject constructor(
)
}
}
override fun getSearchContentsCount(): Flow<Int> {
return combine(
newsResourceFtsDao.getCount(),
topicFtsDao.getCount(),
) { newsResourceCount, topicsCount ->
newsResourceCount + topicsCount
}
}
}

@ -33,4 +33,6 @@ interface SearchContentsRepository {
* Query the contents matched with the [searchQuery] and returns it as a [Flow] of [SearchResult]
*/
fun searchContents(searchQuery: String): Flow<SearchResult>
fun getSearchContentsCount(): Flow<Int>
}

@ -29,4 +29,5 @@ class FakeSearchContentsRepository @Inject constructor() : SearchContentsReposit
override suspend fun populateFtsData() { /* no-op */ }
override fun searchContents(searchQuery: String): Flow<SearchResult> = flowOf()
override fun getSearchContentsCount(): Flow<Int> = flowOf(1)
}

@ -35,5 +35,5 @@ interface NewsResourceFtsDao {
fun searchAllNewsResources(query: String): Flow<List<String>>
@Query("SELECT count(*) FROM newsResourcesFts")
suspend fun getCount(): Int
fun getCount(): Flow<Int>
}

@ -35,5 +35,5 @@ interface TopicFtsDao {
fun searchAllTopics(query: String): Flow<List<String>>
@Query("SELECT count(*) FROM topicsFts")
suspend fun getCount(): Int
fun getCount(): Flow<Int>
}

@ -0,0 +1,31 @@
/*
* 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.core.domain
import com.google.samples.apps.nowinandroid.core.data.repository.SearchContentsRepository
import kotlinx.coroutines.flow.Flow
import javax.inject.Inject
/**
* A use case which returns total count of *Fts tables
*/
class GetSearchContentsCountUseCase @Inject constructor(
private val searchContentsRepository: SearchContentsRepository,
) {
operator fun invoke(): Flow<Int> =
searchContentsRepository.getSearchContentsCount()
}

@ -21,6 +21,7 @@ import com.google.samples.apps.nowinandroid.core.data.repository.SearchContentsR
import com.google.samples.apps.nowinandroid.core.model.data.NewsResource
import com.google.samples.apps.nowinandroid.core.model.data.Topic
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOf
class TestSearchContentsRepository : SearchContentsRepository {
@ -44,6 +45,10 @@ class TestSearchContentsRepository : SearchContentsRepository {
),
)
override fun getSearchContentsCount(): Flow<Int> = flow {
emit(cachedTopics.size + cachedNewsResources.size)
}
/**
* Test only method to add the topics to the stored list in memory
*/

@ -52,6 +52,7 @@ class SearchScreenTest {
private lateinit var topicsString: String
private lateinit var updatesString: String
private lateinit var tryAnotherSearchString: String
private lateinit var searchNotReadyString: String
private val userData: UserData = UserData(
bookmarkedNewsResources = setOf("1", "3"),
@ -75,6 +76,7 @@ class SearchScreenTest {
updatesString = getString(R.string.updates)
tryAnotherSearchString = getString(R.string.try_another_search) +
" " + getString(R.string.interests) + " " + getString(R.string.to_browse_topics)
searchNotReadyString = getString(R.string.search_not_ready)
}
}
@ -199,4 +201,17 @@ class SearchScreenTest {
.onNodeWithText("testing")
.assertIsDisplayed()
}
@Test
fun searchNotReady_verifySearchNotReadyMessageIsVisible() {
composeTestRule.setContent {
SearchScreen(
searchResultUiState = SearchResultUiState.SearchNotReady,
)
}
composeTestRule
.onNodeWithText(searchNotReadyString)
.assertIsDisplayed()
}
}

@ -37,4 +37,10 @@ sealed interface SearchResultUiState {
) : SearchResultUiState {
fun isEmpty(): Boolean = topics.isEmpty() && newsResources.isEmpty()
}
/**
* A state where the search contents are not ready. This happens when the *Fts tables are not
* populated yet.
*/
object SearchNotReady : SearchResultUiState
}

@ -150,6 +150,7 @@ internal fun SearchScreen(
SearchResultUiState.LoadFailed,
-> Unit
SearchResultUiState.SearchNotReady -> SearchNotReadyBody()
SearchResultUiState.EmptyQuery,
-> {
if (recentSearchesUiState is RecentSearchQueriesUiState.Success) {
@ -260,6 +261,21 @@ fun EmptySearchResultBody(
}
}
@Composable
private fun SearchNotReadyBody() {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.padding(horizontal = 48.dp),
) {
Text(
text = stringResource(id = searchR.string.search_not_ready),
fontSize = 18.sp,
textAlign = TextAlign.Center,
modifier = Modifier.padding(vertical = 24.dp),
)
}
}
@Composable
private fun SearchResultBody(
topics: List<FollowableTopic>,
@ -526,6 +542,14 @@ private fun RecentSearchesBodyPreview() {
}
}
@Preview
@Composable
private fun SearchNotReadyBodyPreview() {
NiaTheme {
SearchNotReadyBody()
}
}
@DevicePreviews
@Composable
private fun SearchScreenPreview(

@ -21,6 +21,7 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.google.samples.apps.nowinandroid.core.data.repository.RecentSearchRepository
import com.google.samples.apps.nowinandroid.core.domain.GetRecentSearchQueriesUseCase
import com.google.samples.apps.nowinandroid.core.domain.GetSearchContentsCountUseCase
import com.google.samples.apps.nowinandroid.core.domain.GetSearchContentsUseCase
import com.google.samples.apps.nowinandroid.core.result.Result
import com.google.samples.apps.nowinandroid.core.result.asResult
@ -36,8 +37,8 @@ import javax.inject.Inject
@HiltViewModel
class SearchViewModel @Inject constructor(
// TODO: Add GetSearchContentsCountUseCase to check if the fts tables are populated
getSearchContentsUseCase: GetSearchContentsUseCase,
getSearchContentsCountUseCase: GetSearchContentsCountUseCase,
recentSearchQueriesUseCase: GetRecentSearchQueriesUseCase,
private val recentSearchRepository: RecentSearchRepository,
private val savedStateHandle: SavedStateHandle,
@ -46,25 +47,31 @@ class SearchViewModel @Inject constructor(
val searchQuery = savedStateHandle.getStateFlow(SEARCH_QUERY, "")
val searchResultUiState: StateFlow<SearchResultUiState> =
searchQuery.flatMapLatest { query ->
if (query.length < SEARCH_QUERY_MIN_LENGTH) {
flowOf(SearchResultUiState.EmptyQuery)
getSearchContentsCountUseCase().flatMapLatest { totalCount ->
if (totalCount < SEARCH_MIN_FTS_ENTITY_COUNT) {
flowOf(SearchResultUiState.SearchNotReady)
} else {
getSearchContentsUseCase(query).asResult().map {
when (it) {
is Result.Success -> {
SearchResultUiState.Success(
topics = it.data.topics,
newsResources = it.data.newsResources,
)
}
searchQuery.flatMapLatest { query ->
if (query.length < SEARCH_QUERY_MIN_LENGTH) {
flowOf(SearchResultUiState.EmptyQuery)
} else {
getSearchContentsUseCase(query).asResult().map {
when (it) {
is Result.Success -> {
SearchResultUiState.Success(
topics = it.data.topics,
newsResources = it.data.newsResources,
)
}
is Result.Loading -> {
SearchResultUiState.Loading
}
is Result.Loading -> {
SearchResultUiState.Loading
}
is Result.Error -> {
SearchResultUiState.LoadFailed
is Result.Error -> {
SearchResultUiState.LoadFailed
}
}
}
}
}
@ -109,4 +116,7 @@ class SearchViewModel @Inject constructor(
/** Minimum length where search query is considered as [SearchResultUiState.EmptyQuery] */
const val SEARCH_QUERY_MIN_LENGTH = 2
/** Minimum number of the fts table's entity count where it's considered as search is not ready */
const val SEARCH_MIN_FTS_ENTITY_COUNT = 1
const val SEARCH_QUERY = "searchQuery"

@ -18,6 +18,7 @@
<string name="search">Search</string>
<string name="clear_search_text_content_desc">Clear search text</string>
<string name="search_result_not_found">Sorry, there is no content found for your search \"%1$s\"</string>
<string name="search_not_ready">Sorry, we are still processing the search index. Please come back later</string>
<string name="try_another_search">Try another search or explorer </string>
<string name="interests">Interests</string>
<string name="to_browse_topics"> to browse topics</string>

@ -18,6 +18,7 @@ package com.google.samples.apps.nowinandroid.feature.search
import androidx.lifecycle.SavedStateHandle
import com.google.samples.apps.nowinandroid.core.domain.GetRecentSearchQueriesUseCase
import com.google.samples.apps.nowinandroid.core.domain.GetSearchContentsCountUseCase
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.topicsTestData
@ -55,12 +56,14 @@ class SearchViewModelTest {
)
private val recentSearchRepository = TestRecentSearchRepository()
private val getRecentQueryUseCase = GetRecentSearchQueriesUseCase(recentSearchRepository)
private val getSearchContentsCountUseCase = GetSearchContentsCountUseCase(searchContentsRepository)
private lateinit var viewModel: SearchViewModel
@Before
fun setup() {
viewModel = SearchViewModel(
getSearchContentsUseCase = getSearchContentsUseCase,
getSearchContentsCountUseCase = getSearchContentsCountUseCase,
recentSearchQueriesUseCase = getRecentQueryUseCase,
savedStateHandle = SavedStateHandle(),
recentSearchRepository = recentSearchRepository,

Loading…
Cancel
Save