Add offline typeaheadSearch and replaced LiveData with Flow

This commit is contained in:
Mohamed
2024-05-30 09:53:03 +03:00
parent b87271169a
commit 74b3581a74
5 changed files with 62 additions and 74 deletions

View File

@ -7,6 +7,7 @@ import app.omnivore.omnivore.core.data.model.LibraryQuery
import app.omnivore.omnivore.core.database.entities.HighlightChange
import app.omnivore.omnivore.core.database.entities.SavedItemLabel
import app.omnivore.omnivore.core.database.entities.SavedItemWithLabelsAndHighlights
import app.omnivore.omnivore.core.database.entities.TypeaheadCardData
import kotlinx.coroutines.flow.Flow
interface LibraryRepository {
@ -46,4 +47,6 @@ interface LibraryRepository {
suspend fun syncHighlightChange(highlightChange: HighlightChange): Boolean
suspend fun sync(context: Context, since: String, cursor: String?, limit: Int = 20): SavedItemSyncResult
suspend fun getTypeaheadData(query: String): List<TypeaheadCardData>
}

View File

@ -20,6 +20,7 @@ import app.omnivore.omnivore.core.database.entities.SavedItem
import app.omnivore.omnivore.core.database.entities.SavedItemAndSavedItemLabelCrossRef
import app.omnivore.omnivore.core.database.entities.SavedItemLabel
import app.omnivore.omnivore.core.database.entities.SavedItemWithLabelsAndHighlights
import app.omnivore.omnivore.core.database.entities.TypeaheadCardData
import app.omnivore.omnivore.core.database.entities.highlightChangeToHighlight
import app.omnivore.omnivore.core.network.Networker
import app.omnivore.omnivore.core.network.ReadingProgressParams
@ -47,8 +48,10 @@ import app.omnivore.omnivore.graphql.generated.type.SetLabelsInput
import app.omnivore.omnivore.graphql.generated.type.UpdateHighlightInput
import com.apollographql.apollo3.api.Optional
import com.google.gson.Gson
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.withContext
import javax.inject.Inject
class LibraryRepositoryImpl @Inject constructor(
@ -94,6 +97,12 @@ class LibraryRepositoryImpl @Inject constructor(
}
}
override suspend fun getTypeaheadData(query: String): List<TypeaheadCardData> {
return withContext(Dispatchers.IO) {
savedItemDao.getTypeaheadData(query)
}
}
override suspend fun updateReadingProgress(
itemId: String,
readingProgressPercentage: Double,

View File

@ -8,6 +8,7 @@ import androidx.room.Update
import app.omnivore.omnivore.core.database.entities.SavedItem
import app.omnivore.omnivore.core.database.entities.SavedItemQueryConstants
import app.omnivore.omnivore.core.database.entities.SavedItemWithLabelsAndHighlights
import app.omnivore.omnivore.core.database.entities.TypeaheadCardData
import kotlinx.coroutines.flow.Flow
@Dao
@ -19,6 +20,9 @@ interface SavedItemDao {
@Query("SELECT * FROM savedItem WHERE savedItemId = :itemID")
suspend fun findById(itemID: String): SavedItem?
@Query("SELECT savedItemId, slug, title, isArchived FROM saveditem WHERE title LIKE '%' || :query || '%'")
suspend fun getTypeaheadData(query: String): List<TypeaheadCardData>
@Query("SELECT * FROM savedItem WHERE serverSyncStatus != 0")
fun getUnSynced(): List<SavedItem>

View File

@ -27,6 +27,7 @@ import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.Alignment
@ -36,10 +37,10 @@ import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavHostController
import app.omnivore.omnivore.R
import app.omnivore.omnivore.core.database.entities.SavedItemWithLabelsAndHighlights
import app.omnivore.omnivore.core.database.entities.TypeaheadCardData
import app.omnivore.omnivore.feature.reader.PDFReaderActivity
import app.omnivore.omnivore.feature.reader.WebReaderLoadingContainerActivity
import app.omnivore.omnivore.feature.savedItemViews.SavedItemCard
@ -51,9 +52,9 @@ fun SearchView(
navController: NavHostController,
viewModel: SearchViewModel = hiltViewModel()
) {
val isRefreshing: Boolean by viewModel.isRefreshing.observeAsState(false)
val typeaheadMode: Boolean by viewModel.typeaheadMode.observeAsState(true)
val searchText: String by viewModel.searchTextLiveData.observeAsState("")
val isRefreshing: Boolean by viewModel.isRefreshing.collectAsStateWithLifecycle()
val typeaheadMode: Boolean by viewModel.typeaheadMode.collectAsStateWithLifecycle()
val searchText: String by viewModel.searchTextFlow.collectAsState("")
val actionsMenuItem: SavedItemWithLabelsAndHighlights? by viewModel.actionsMenuItemLiveData.observeAsState(
null
)
@ -158,9 +159,7 @@ fun TypeaheadSearchViewContent(viewModel: SearchViewModel, modifier: Modifier) {
val context = LocalContext.current
val listState = rememberLazyListState()
val searchedCardsData: List<TypeaheadCardData> by viewModel.searchItemsLiveData.observeAsState(
listOf()
)
val searchedCardsData by viewModel.searchItemsFlow.collectAsStateWithLifecycle()
LazyColumn(
state = listState,
@ -193,9 +192,7 @@ fun SearchViewContent(viewModel: SearchViewModel, modifier: Modifier) {
val context = LocalContext.current
val listState = rememberLazyListState()
val cardsData: List<SavedItemWithLabelsAndHighlights> by viewModel.itemsLiveData.observeAsState(
listOf()
)
val cardsData: List<SavedItemWithLabelsAndHighlights> by viewModel.itemsState.collectAsStateWithLifecycle()
LazyColumn(
state = listState,

View File

@ -1,7 +1,6 @@
package app.omnivore.omnivore.feature.library
import android.content.Context
import androidx.lifecycle.MediatorLiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
@ -10,68 +9,70 @@ import app.omnivore.omnivore.core.data.archiveSavedItem
import app.omnivore.omnivore.core.data.deleteSavedItem
import app.omnivore.omnivore.core.data.isSavedItemContentStoredInDB
import app.omnivore.omnivore.core.data.librarySearch
import app.omnivore.omnivore.core.data.repository.LibraryRepository
import app.omnivore.omnivore.core.data.unarchiveSavedItem
import app.omnivore.omnivore.core.database.entities.SavedItemLabel
import app.omnivore.omnivore.core.database.entities.SavedItemWithLabelsAndHighlights
import app.omnivore.omnivore.core.database.entities.TypeaheadCardData
import app.omnivore.omnivore.core.datastore.DatastoreRepository
import app.omnivore.omnivore.core.network.Networker
import app.omnivore.omnivore.core.network.typeaheadSearch
import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import javax.inject.Inject
@OptIn(FlowPreview::class)
@HiltViewModel
class SearchViewModel @Inject constructor(
private val networker: Networker,
private val dataService: DataService,
private val datastoreRepo: DatastoreRepository,
private val repository: LibraryRepository,
@ApplicationContext private val applicationContext: Context
) : ViewModel(), SavedItemViewModel {
private val contentRequestChannel = Channel<String>(capacity = Channel.UNLIMITED)
private var cursor: String? = null
private var librarySearchCursor: String? = null
// These are used to make sure we handle search result
// responses in the right order
private var searchIdx = 0
private var receivedIdx = 0
// Live Data
var isRefreshing = MutableLiveData(false)
val typeaheadMode = MutableLiveData(true)
val searchTextLiveData = MutableLiveData("")
val searchItemsLiveData = MutableLiveData<List<TypeaheadCardData>>(listOf())
val itemsLiveData = MediatorLiveData<List<SavedItemWithLabelsAndHighlights>>()
var isRefreshing = MutableStateFlow(false)
val typeaheadMode = MutableStateFlow(true)
val searchTextFlow = MutableStateFlow("")
val searchItemsFlow = MutableStateFlow<List<TypeaheadCardData>>(emptyList())
val itemsState = MutableStateFlow<List<SavedItemWithLabelsAndHighlights>>(emptyList())
override val actionsMenuItemLiveData = MutableLiveData<SavedItemWithLabelsAndHighlights?>(null)
init {
searchTextFlow
.debounce(300)
.onEach {
if (it.isBlank()){
searchItemsFlow.update { emptyList() }
} else {
performTypeaheadSearch()
}
}.launchIn(viewModelScope)
}
fun updateSearchText(text: String) {
typeaheadMode.postValue(true)
searchTextLiveData.value = text
if (text == "") {
searchItemsLiveData.value = listOf()
} else {
viewModelScope.launch {
performTypeaheadSearch(true)
}
}
typeaheadMode.update { true }
searchTextFlow.update { text }
}
fun performSearch() {
// To perform search we just clear the current state, so the LibraryView infinite scroll
// load will update items.
viewModelScope.launch {
isRefreshing.postValue(true)
itemsLiveData.postValue(listOf())
typeaheadMode.postValue(false)
isRefreshing.update { true }
itemsState.update { (listOf()) }
typeaheadMode.update { false }
loadUsingSearchAPI()
}
@ -79,7 +80,6 @@ class SearchViewModel @Inject constructor(
private fun loadUsingSearchAPI() {
viewModelScope.launch {
val context = applicationContext
withContext(Dispatchers.IO) {
val result = dataService.librarySearch(
context = applicationContext,
@ -122,40 +122,24 @@ class SearchViewModel @Inject constructor(
}
*/
itemsLiveData.value?.let {
itemsLiveData.postValue(newItems + it)
} ?: run {
itemsLiveData.postValue(newItems)
itemsState.update {
it + newItems
}
isRefreshing.postValue(false)
isRefreshing.update { false }
}
}
}
private suspend fun performTypeaheadSearch(clearPreviousSearch: Boolean) {
if (clearPreviousSearch) {
cursor = null
}
val thisSearchIdx = searchIdx
searchIdx += 1
private suspend fun performTypeaheadSearch() {
isRefreshing.update { true }
// Execute the search
val searchResult = networker.typeaheadSearch(searchTextLiveData.value ?: "")
val searchResult = repository.getTypeaheadData(searchTextFlow.value)
// Search results aren't guaranteed to return in order so this
// will discard old results that are returned while a user is typing.
// For example if a user types 'Canucks', often the search results
// for 'C' are returned after 'Canucks' because it takes the backend
// much longer to compute.
if (thisSearchIdx in 1..receivedIdx) {
return
}
searchItemsFlow.update { searchResult }
searchItemsLiveData.postValue(searchResult.cardsData)
isRefreshing.postValue(false)
isRefreshing.update { false }
}
override fun handleSavedItemAction(itemId: String, action: SavedItemAction) {
@ -219,14 +203,5 @@ class SearchViewModel @Inject constructor(
// }
}
private fun searchQueryString(): String {
var query = ""
val searchText = searchTextLiveData.value ?: ""
if (searchText.isNotEmpty()) {
query += " $searchText"
}
return query
}
private fun searchQueryString() = " ${searchTextFlow.value.ifBlank { "" }}"
}