Change-Id: I103c903c936137c41d7d11be0731438d9ee557d9recent_search
parent
83b640a7f0
commit
6f9d2fe3ba
@ -0,0 +1,57 @@
|
||||
/*
|
||||
* 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.data.repository
|
||||
|
||||
import com.google.samples.apps.nowinandroid.core.database.dao.NewsResourceDao
|
||||
import com.google.samples.apps.nowinandroid.core.database.dao.NewsResourceFtsDao
|
||||
import com.google.samples.apps.nowinandroid.core.database.dao.TopicDao
|
||||
import com.google.samples.apps.nowinandroid.core.database.dao.TopicFtsDao
|
||||
import com.google.samples.apps.nowinandroid.core.database.model.asExternalModel
|
||||
import com.google.samples.apps.nowinandroid.core.database.model.asFtsEntity
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import javax.inject.Inject
|
||||
|
||||
class DefaultSearchContentsRepository @Inject constructor(
|
||||
private val newsResourceDao: NewsResourceDao,
|
||||
private val newsResourceFtsDao: NewsResourceFtsDao,
|
||||
private val topicDao: TopicDao,
|
||||
private val topicFtsDao: TopicFtsDao,
|
||||
) : SearchContentsRepository {
|
||||
|
||||
override fun populateFtsData() {
|
||||
newsResourceFtsDao.insertAll(newsResourceDao.getOneOffNewsResources().map { it.asFtsEntity() })
|
||||
topicFtsDao.insertAll(topicDao.getOneOffTopicEntities().map { it.asFtsEntity() })
|
||||
}
|
||||
|
||||
override fun searchContents(searchQuery: String): Flow<SearchResult> {
|
||||
// Surround the query by asterisks to match the query when it's in the middle of
|
||||
// a word
|
||||
val newsResourceIds = newsResourceFtsDao.searchAllNewsResources("*${searchQuery}*")
|
||||
val topicIds = topicFtsDao.searchAllTopics("*${searchQuery}*")
|
||||
|
||||
return combine(
|
||||
newsResourceDao.getNewsResources(filterNewsIds = newsResourceIds.toSet()),
|
||||
topicDao.getTopicEntities(topicIds.toSet())
|
||||
) { newsResources, topics ->
|
||||
SearchResult(
|
||||
topics = topics.map { it.asExternalModel() },
|
||||
newsResources = newsResources.map { it.asExternalModel() }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
@ -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.data.repository
|
||||
|
||||
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
|
||||
|
||||
interface SearchContentsRepository {
|
||||
fun populateFtsData()
|
||||
fun searchContents(searchQuery: String): Flow<SearchResult>
|
||||
}
|
||||
|
||||
data class SearchResult(
|
||||
val topics: List<Topic> = emptyList(),
|
||||
val newsResources: List<NewsResource> = emptyList(),
|
||||
)
|
@ -0,0 +1,34 @@
|
||||
/*
|
||||
* 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.data.repository.fake
|
||||
|
||||
import com.google.samples.apps.nowinandroid.core.data.repository.SearchContentsRepository
|
||||
import com.google.samples.apps.nowinandroid.core.data.repository.SearchResult
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* Fake implementation of the [SearchContentsRepository]
|
||||
*/
|
||||
class FakeSearchContentsRepository @Inject constructor(
|
||||
) : SearchContentsRepository {
|
||||
|
||||
override fun populateFtsData() {}
|
||||
override fun searchContents(searchQuery: String): Flow<SearchResult> {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
}
|
@ -0,0 +1,282 @@
|
||||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 13,
|
||||
"identityHash": "b6b299e53da623b16360975581ebfcfe",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "news_resources",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `title` TEXT NOT NULL, `content` TEXT NOT NULL, `url` TEXT NOT NULL, `header_image_url` TEXT, `publish_date` INTEGER NOT NULL, `type` TEXT NOT NULL, PRIMARY KEY(`id`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "title",
|
||||
"columnName": "title",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "content",
|
||||
"columnName": "content",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "url",
|
||||
"columnName": "url",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "headerImageUrl",
|
||||
"columnName": "header_image_url",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "publishDate",
|
||||
"columnName": "publish_date",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "type",
|
||||
"columnName": "type",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "news_resources_topics",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`news_resource_id` TEXT NOT NULL, `topic_id` TEXT NOT NULL, PRIMARY KEY(`news_resource_id`, `topic_id`), FOREIGN KEY(`news_resource_id`) REFERENCES `news_resources`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`topic_id`) REFERENCES `topics`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "newsResourceId",
|
||||
"columnName": "news_resource_id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "topicId",
|
||||
"columnName": "topic_id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"news_resource_id",
|
||||
"topic_id"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_news_resources_topics_news_resource_id",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"news_resource_id"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_news_resources_topics_news_resource_id` ON `${TABLE_NAME}` (`news_resource_id`)"
|
||||
},
|
||||
{
|
||||
"name": "index_news_resources_topics_topic_id",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"topic_id"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_news_resources_topics_topic_id` ON `${TABLE_NAME}` (`topic_id`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "news_resources",
|
||||
"onDelete": "CASCADE",
|
||||
"onUpdate": "NO ACTION",
|
||||
"columns": [
|
||||
"news_resource_id"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
{
|
||||
"table": "topics",
|
||||
"onDelete": "CASCADE",
|
||||
"onUpdate": "NO ACTION",
|
||||
"columns": [
|
||||
"topic_id"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"ftsVersion": "FTS4",
|
||||
"ftsOptions": {
|
||||
"tokenizer": "simple",
|
||||
"tokenizerArgs": [],
|
||||
"contentTable": "",
|
||||
"languageIdColumnName": "",
|
||||
"matchInfo": "FTS4",
|
||||
"notIndexedColumns": [],
|
||||
"prefixSizes": [],
|
||||
"preferredOrder": "ASC"
|
||||
},
|
||||
"contentSyncTriggers": [],
|
||||
"tableName": "newsResourcesFts",
|
||||
"createSql": "CREATE VIRTUAL TABLE IF NOT EXISTS `${TABLE_NAME}` USING FTS4(`newsResourceId` TEXT NOT NULL, `title` TEXT NOT NULL, `content` TEXT NOT NULL)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "newsResourceId",
|
||||
"columnName": "newsResourceId",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "title",
|
||||
"columnName": "title",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "content",
|
||||
"columnName": "content",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": []
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "topics",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `shortDescription` TEXT NOT NULL, `longDescription` TEXT NOT NULL DEFAULT '', `url` TEXT NOT NULL DEFAULT '', `imageUrl` TEXT NOT NULL DEFAULT '', PRIMARY KEY(`id`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "name",
|
||||
"columnName": "name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "shortDescription",
|
||||
"columnName": "shortDescription",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "longDescription",
|
||||
"columnName": "longDescription",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true,
|
||||
"defaultValue": "''"
|
||||
},
|
||||
{
|
||||
"fieldPath": "url",
|
||||
"columnName": "url",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true,
|
||||
"defaultValue": "''"
|
||||
},
|
||||
{
|
||||
"fieldPath": "imageUrl",
|
||||
"columnName": "imageUrl",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true,
|
||||
"defaultValue": "''"
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"ftsVersion": "FTS4",
|
||||
"ftsOptions": {
|
||||
"tokenizer": "simple",
|
||||
"tokenizerArgs": [],
|
||||
"contentTable": "",
|
||||
"languageIdColumnName": "",
|
||||
"matchInfo": "FTS4",
|
||||
"notIndexedColumns": [],
|
||||
"prefixSizes": [],
|
||||
"preferredOrder": "ASC"
|
||||
},
|
||||
"contentSyncTriggers": [],
|
||||
"tableName": "topicsFts",
|
||||
"createSql": "CREATE VIRTUAL TABLE IF NOT EXISTS `${TABLE_NAME}` USING FTS4(`topicId` TEXT NOT NULL, `name` TEXT NOT NULL, `shortDescription` TEXT NOT NULL, `longDescription` TEXT NOT NULL)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "topicId",
|
||||
"columnName": "topicId",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "name",
|
||||
"columnName": "name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "shortDescription",
|
||||
"columnName": "shortDescription",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "longDescription",
|
||||
"columnName": "longDescription",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": []
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
}
|
||||
],
|
||||
"views": [],
|
||||
"setupQueries": [
|
||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'b6b299e53da623b16360975581ebfcfe')"
|
||||
]
|
||||
}
|
||||
}
|
@ -0,0 +1,38 @@
|
||||
/*
|
||||
* 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.database.dao
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import com.google.samples.apps.nowinandroid.core.database.model.NewsResourceFtsEntity
|
||||
|
||||
/**
|
||||
* DAO for [NewsResourceFtsEntity] access.
|
||||
*/
|
||||
@Dao
|
||||
interface NewsResourceFtsDao {
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
fun insertAll(topics: List<NewsResourceFtsEntity>)
|
||||
|
||||
@Query("SELECT newsResourceId FROM newsResourcesFts WHERE newsResourcesFts MATCH :query")
|
||||
fun searchAllNewsResources(query: String): List<String>
|
||||
|
||||
@Query("SELECT count(*) FROM newsResourcesFts")
|
||||
fun getCount(): Int
|
||||
}
|
@ -0,0 +1,37 @@
|
||||
/*
|
||||
* 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.database.dao
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import com.google.samples.apps.nowinandroid.core.database.model.TopicFtsEntity
|
||||
|
||||
/**
|
||||
* DAO for [TopicFtsEntity] access.
|
||||
*/
|
||||
@Dao
|
||||
interface TopicFtsDao {
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
fun insertAll(topics: List<TopicFtsEntity>)
|
||||
@Query("SELECT topicId FROM topicsFts WHERE topicsFts MATCH :query")
|
||||
fun searchAllTopics(query: String): List<String>
|
||||
|
||||
@Query("SELECT count(*) FROM topicsFts")
|
||||
fun getCount(): Int
|
||||
}
|
@ -0,0 +1,44 @@
|
||||
/*
|
||||
* 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.database.model
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.Fts4
|
||||
|
||||
/**
|
||||
* Fts entity for the news resources. See https://developer.android.com/reference/androidx/room/Fts4.
|
||||
*/
|
||||
@Entity(tableName = "newsResourcesFts")
|
||||
@Fts4
|
||||
data class NewsResourceFtsEntity(
|
||||
|
||||
@ColumnInfo(name = "newsResourceId")
|
||||
val newsResourceId: String,
|
||||
|
||||
@ColumnInfo(name = "title")
|
||||
val title: String,
|
||||
|
||||
@ColumnInfo(name = "content")
|
||||
val content: String,
|
||||
)
|
||||
|
||||
fun NewsResourceEntity.asFtsEntity() = NewsResourceFtsEntity(
|
||||
newsResourceId = id,
|
||||
title = title,
|
||||
content = content,
|
||||
)
|
@ -0,0 +1,48 @@
|
||||
/*
|
||||
* 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.database.model
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.Fts4
|
||||
|
||||
/**
|
||||
* Fts entity for the topic. See https://developer.android.com/reference/androidx/room/Fts4.
|
||||
*/
|
||||
@Entity(tableName = "topicsFts")
|
||||
@Fts4
|
||||
data class TopicFtsEntity(
|
||||
|
||||
@ColumnInfo(name = "topicId")
|
||||
val topicId: String,
|
||||
|
||||
@ColumnInfo(name = "name")
|
||||
val name: String,
|
||||
|
||||
@ColumnInfo(name = "shortDescription")
|
||||
val shortDescription: String,
|
||||
|
||||
@ColumnInfo(name = "longDescription")
|
||||
val longDescription: String,
|
||||
)
|
||||
|
||||
fun TopicEntity.asFtsEntity() = TopicFtsEntity(
|
||||
topicId = id,
|
||||
name = name,
|
||||
shortDescription = shortDescription,
|
||||
longDescription = longDescription
|
||||
)
|
@ -0,0 +1,64 @@
|
||||
/*
|
||||
* 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 com.google.samples.apps.nowinandroid.core.data.repository.SearchResult
|
||||
import com.google.samples.apps.nowinandroid.core.data.repository.UserDataRepository
|
||||
import com.google.samples.apps.nowinandroid.core.domain.model.FollowableTopic
|
||||
import com.google.samples.apps.nowinandroid.core.domain.model.UserNewsResource
|
||||
import com.google.samples.apps.nowinandroid.core.model.data.UserData
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.filterNot
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import javax.inject.Inject
|
||||
|
||||
class GetSearchContentsUseCase @Inject constructor(
|
||||
private val searchContentsRepository: SearchContentsRepository,
|
||||
private val userDataRepository: UserDataRepository,
|
||||
) {
|
||||
|
||||
operator fun invoke(
|
||||
searchQuery: String,
|
||||
): Flow<UserSearchResult> =
|
||||
searchContentsRepository.searchContents(searchQuery)
|
||||
.mapToUserSearchResult(userDataRepository.userData)
|
||||
}
|
||||
|
||||
fun Flow<SearchResult>.mapToUserSearchResult(userDataStream: Flow<UserData>): Flow<UserSearchResult> =
|
||||
combine(userDataStream) { searchResult, userData ->
|
||||
UserSearchResult(
|
||||
topics = searchResult.topics.map { topic ->
|
||||
FollowableTopic(
|
||||
topic = topic,
|
||||
isFollowed = topic.id in userData.followedTopics,
|
||||
)
|
||||
},
|
||||
newsResources = searchResult.newsResources.map { news ->
|
||||
UserNewsResource(
|
||||
newsResource = news,
|
||||
userData = userData,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
data class UserSearchResult(
|
||||
val topics: List<FollowableTopic> = emptyList(),
|
||||
val newsResources: List<UserNewsResource> = emptyList(),
|
||||
)
|
@ -0,0 +1,62 @@
|
||||
/*
|
||||
* 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.testing.repository
|
||||
|
||||
import com.google.samples.apps.nowinandroid.core.data.repository.SearchContentsRepository
|
||||
import com.google.samples.apps.nowinandroid.core.data.repository.SearchResult
|
||||
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.flowOf
|
||||
|
||||
class TestSearchContentsRepository : SearchContentsRepository {
|
||||
|
||||
private val cachedTopics: MutableList<Topic> = mutableListOf()
|
||||
private val cachedNewsResources: MutableList<NewsResource> = mutableListOf()
|
||||
|
||||
override fun populateFtsData() {}
|
||||
|
||||
override fun searchContents(searchQuery: String): Flow<SearchResult> {
|
||||
return flowOf(
|
||||
SearchResult(
|
||||
topics = cachedTopics.filter {
|
||||
it.name.contains(searchQuery) ||
|
||||
it.shortDescription.contains(searchQuery) ||
|
||||
it.longDescription.contains(searchQuery)
|
||||
},
|
||||
newsResources = cachedNewsResources.filter {
|
||||
it.content.contains(searchQuery) ||
|
||||
it.title.contains(searchQuery)
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Test only method to add the topics to the stored list in memory
|
||||
*/
|
||||
fun addTopics(topics: List<Topic>) {
|
||||
cachedTopics.addAll(topics)
|
||||
}
|
||||
|
||||
/**
|
||||
* Test only method to add the news resources to the stored list in memory
|
||||
*/
|
||||
fun addNewsResources(newsResources: List<NewsResource>) {
|
||||
cachedNewsResources.addAll(newsResources)
|
||||
}
|
||||
}
|
@ -0,0 +1,89 @@
|
||||
/*
|
||||
* 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.search
|
||||
|
||||
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
|
||||
import com.google.samples.apps.nowinandroid.core.testing.repository.TestSearchContentsRepository
|
||||
import com.google.samples.apps.nowinandroid.core.testing.repository.TestUserDataRepository
|
||||
import com.google.samples.apps.nowinandroid.core.testing.util.MainDispatcherRule
|
||||
import com.google.samples.apps.nowinandroid.feature.search.SearchResultUiState.EmptyQuery
|
||||
import com.google.samples.apps.nowinandroid.feature.search.SearchResultUiState.Loading
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.test.UnconfinedTestDispatcher
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
/**
|
||||
* To learn more about how this test handles Flows created with stateIn, see
|
||||
* https://developer.android.com/kotlin/flow/test#statein
|
||||
*/
|
||||
class SearchViewModelTest {
|
||||
|
||||
@get:Rule
|
||||
val dispatcherRule = MainDispatcherRule()
|
||||
|
||||
private val userDataRepository = TestUserDataRepository()
|
||||
private val searchContentsRepository = TestSearchContentsRepository()
|
||||
private val getSearchContentsUseCase = GetSearchContentsUseCase(
|
||||
searchContentsRepository = searchContentsRepository,
|
||||
userDataRepository = userDataRepository,
|
||||
)
|
||||
private lateinit var viewModel: SearchViewModel
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
viewModel = SearchViewModel(
|
||||
getSearchContentsUseCase = getSearchContentsUseCase,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun stateIsInitiallyLoading() = runTest {
|
||||
assertEquals(Loading, viewModel.searchResultUiState.value)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun stateIsEmptyQuery_withEmptySearchQuery() = runTest {
|
||||
val collectJob = launch(UnconfinedTestDispatcher()) { viewModel.searchResultUiState.collect() }
|
||||
|
||||
viewModel.onSearchQueryChanged("")
|
||||
assertEquals(EmptyQuery, viewModel.searchResultUiState.value)
|
||||
|
||||
collectJob.cancel()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun emptyResultIsReturned_withNotMatchingQuery() = runTest {
|
||||
val collectJob = launch(UnconfinedTestDispatcher()) { viewModel.searchQuery.collect() }
|
||||
|
||||
viewModel.onSearchQueryChanged("XXX")
|
||||
searchContentsRepository.addNewsResources(newsResourcesTestData)
|
||||
searchContentsRepository.addTopics(topicsTestData)
|
||||
|
||||
val result = viewModel.searchResultUiState.value
|
||||
// TODO: Figure out to get the latest emitted ui State? The result is emitted as EmptyQuery
|
||||
// assertIs<Success>(result)
|
||||
|
||||
collectJob.cancel()
|
||||
}
|
||||
}
|
Loading…
Reference in new issue