Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

KMM search android #18

Merged
merged 12 commits into from
Sep 11, 2023
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import android.util.Log
package com.gchristov.newsfeed.commoncompose.elements.search

import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.*
Expand All @@ -11,23 +10,26 @@ import androidx.compose.material.icons.filled.Search
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.dp
import com.gchristov.newsfeed.commoncompose.elements.AppSurface
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.tooling.preview.Preview

@Composable
fun SearchAppBar(
fun AppSearchBar(
text: String,
onTextChange: (String) -> Unit,
onCloseClicked: () -> Unit,
onSearchClicked: (String) -> Unit,
) {

TextField(modifier = Modifier
.fillMaxWidth(),
// Right now this search bar is just a text field. However, in the future we may have many more
// screens which need searching, so it would be great if AppSearchBar abstracts away the logic
// of switching the app bars based on the state internally so that the top-level screens don't
// have to know about it and just use it out-of-the-box.
//
// This implementation can then expose the properties or provide defaults as appropriate.
//
// TODO: Make this search bar a bit ore flexible
TextField(modifier = Modifier.fillMaxWidth(),
dfernandezm marked this conversation as resolved.
Show resolved Hide resolved
value = text,
onValueChange = {
onTextChange(it)
Expand All @@ -46,8 +48,7 @@ fun SearchAppBar(
singleLine = true,
leadingIcon = {
IconButton(
modifier = Modifier
.alpha(ContentAlpha.medium),
modifier = Modifier.alpha(ContentAlpha.medium),
onClick = {}
) {
Icon(
Expand Down Expand Up @@ -90,5 +91,6 @@ fun SearchAppBar(
disabledIndicatorColor = Color.Transparent,
textColor = Color.Black

))
)
)
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
package com.gchristov.newsfeed.feed

import SearchAppBar
import android.content.Intent
import android.os.Bundle
import android.util.Log
import androidx.activity.viewModels
import androidx.compose.foundation.background
Expand All @@ -12,6 +10,7 @@ import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Favorite
import androidx.compose.material.icons.filled.Search
import androidx.compose.runtime.Composable
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.remember
Expand All @@ -27,7 +26,7 @@ import com.gchristov.newsfeed.commoncompose.elements.*
import com.gchristov.newsfeed.commoncompose.elements.list.AppGroupedList
import com.gchristov.newsfeed.commoncompose.elements.list.AppListRow
import com.gchristov.newsfeed.commoncompose.elements.list.items
import com.gchristov.newsfeed.commoncompose.elements.search.SearchIconButton
import com.gchristov.newsfeed.commoncompose.elements.search.AppSearchBar
import com.gchristov.newsfeed.commoncompose.theme.Theme
import com.gchristov.newsfeed.commonnavigation.NavigationModule
import com.gchristov.newsfeed.kmmcommonmvvm.createViewModelFactory
Expand All @@ -46,11 +45,6 @@ class FeedActivity : CommonComposeActivity() {
private val feedItemDateFormat = SimpleDateFormat("dd/MM/yyyy", Locale.getDefault())
private val feedSectionDateFormat = SimpleDateFormat("MMM, yyyy", Locale.getDefault())

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel.onScreenVisible()
}

@Composable
override fun Content() = FeedScreen(
viewModel = viewModel,
Expand Down Expand Up @@ -98,10 +92,9 @@ internal fun FeedScreen(
onRefresh = viewModel::refreshContent,
onLoadMore = { viewModel.loadNextPage(startFromFirst = false) },
onFeedItemClick = onFeedItemClick,

searchWidgetState = state?.searchWidgetState ?: SearchWidgetState.CLOSED,
onSearchClick = {
viewModel.onSearchStateChanged(it)
viewModel.onSearchStateChanged(it)
},
searchTextState = state?.searchQuery ?: "",
onSearchTextChange = {
Expand Down Expand Up @@ -179,7 +172,7 @@ private fun FeedState(
}

@Composable
fun MainAppBar(
private fun MainAppBar(
searchWidgetState: SearchWidgetState,
searchTextState: String,
onTextChange: (String) -> Unit,
Expand All @@ -192,12 +185,16 @@ fun MainAppBar(
AppBar(
title = stringResource(R.string.app_name),
actions = {
SearchIconButton(onClick = onSearchTriggered)
AppIconButton(
onClick = onSearchTriggered,
icon = Icons.Filled.Search,
contentDescription = "Search"
)
}
)
}
SearchWidgetState.OPENED -> {
SearchAppBar(
AppSearchBar(
text = searchTextState,
onTextChange = onTextChange,
onCloseClicked = onCloseClicked,
Expand Down Expand Up @@ -367,11 +364,6 @@ private fun ErrorState(
}
}

@Composable
fun SearchBar() {

}

@Composable
private fun SectionedFeed.SectionType.toHeader(headerFormatter: (Long) -> String) = when (this) {
is SectionedFeed.SectionType.ThisWeek -> stringResource(R.string.feed_this_week)
Expand Down
2 changes: 1 addition & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ buildscript {
mavenCentral()
}
dependencies {
classpath("com.android.tools.build:gradle:7.3.1")
classpath("com.android.tools.build:gradle:7.1.3")
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.6.10")
classpath("org.jetbrains.kotlin:kotlin-serialization:1.6.10")
classpath("com.codingfeline.buildkonfig:buildkonfig-gradle-plugin:0.7.0")
Expand Down
2 changes: 1 addition & 1 deletion gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#Sat Sep 25 17:05:28 BST 2021
distributionBase=GRADLE_USER_HOME
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip
distributionPath=wrapper/dists
zipStorePath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,5 @@ interface FeedRepository {

suspend fun saveSearchQuery(searchQuery: String)

suspend fun searchQuery(): String
suspend fun searchQuery(): String?
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,9 @@ import com.gchristov.newsfeed.kmmfeeddata.model.DecoratedFeedPage
import com.gchristov.newsfeed.kmmfeeddata.model.toFeedPage
import com.gchristov.newsfeed.kmmpostdata.PostRepository
import com.russhwolf.settings.Settings
import com.russhwolf.settings.contains
import com.russhwolf.settings.set
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.withContext
import kotlinx.datetime.Clock
import kotlinx.datetime.Instant

internal class RealFeedRepository(
Expand Down Expand Up @@ -109,21 +107,13 @@ internal class RealFeedRepository(
}
}

override suspend fun searchQuery(): String =
withContext(dispatcher) {
return@withContext sharedPreferences.getString(
key = SEARCH_QUERY_PREFERENCES_KEY,
defaultValue = "brexit,fintech"
)
}
override suspend fun searchQuery(): String? = withContext(dispatcher) {
return@withContext sharedPreferences.getStringOrNull(key = SEARCH_QUERY_PREFERENCES_KEY)
}

override suspend fun saveSearchQuery(searchQuery: String) =
withContext(dispatcher) {
if (searchQuery.length >= MINIMUM_SEARCH_QUERY_LENGTH) {
sharedPreferences[SEARCH_QUERY_PREFERENCES_KEY] = searchQuery
}
}
override suspend fun saveSearchQuery(searchQuery: String) = withContext(dispatcher) {
sharedPreferences[SEARCH_QUERY_PREFERENCES_KEY] = searchQuery
}
}

private const val SEARCH_QUERY_PREFERENCES_KEY = "searchQuery"
private const val MINIMUM_SEARCH_QUERY_LENGTH = 3
private const val SEARCH_QUERY_PREFERENCES_KEY = "searchQuery"
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ class FakeFeedRepository(

private var _cacheCleared = false
private var _pageIndex = 0
private var _lastSearchQuery = "brexit,fintech"
private var _lastSearchQuery: String? = null

override suspend fun feedPage(
pageId: Int,
Expand Down Expand Up @@ -49,7 +49,7 @@ class FakeFeedRepository(
_lastSearchQuery = searchQuery
}

override suspend fun searchQuery(): String {
override suspend fun searchQuery(): String? {
return _lastSearchQuery
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,9 @@ import com.gchristov.newsfeed.kmmfeeddata.usecase.GetSectionedFeedUseCase
import com.gchristov.newsfeed.kmmfeeddata.usecase.RedecorateSectionedFeedUseCase
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.filterNotNull


class FeedViewModel(
Expand All @@ -23,32 +22,11 @@ class FeedViewModel(
dispatcher = dispatcher,
initialState = State()
) {
private val searchQueryFlow = MutableStateFlow("")

// Added to existing state instead of creating new variables?
// ----
// mutableStateOf("") for search
// mutableStateOf(value = SearchWidgetState.CLOSED) for widget state
private val searchQueryFlow: MutableStateFlow<String?> = MutableStateFlow(null)

init {
observeSearchQuery()
}

fun onScreenVisible() {
loadFeedWithStoredSearchQuery()
}

private fun loadFeedWithStoredSearchQuery() {
launchUiCoroutine {
val savedSearchQuery = feedRepository.searchQuery()
println("Saved query from DB $savedSearchQuery")

setState { copy(searchQuery = savedSearchQuery) }
println("State after load from DB ${state.value.searchQuery}")
loadNextPage()


}
loadNextPage()
}

/**
Expand All @@ -64,26 +42,21 @@ class FeedViewModel(
launchUiCoroutine {
searchQueryFlow
.debounce(DEBOUNCE_INTERVAL_MS)
.filter { text -> text.isNotEmpty() }
.filterNotNull()
.collect { debouncedText ->
setState { copy(searchQuery = debouncedText) }
loadNextPage(startFromFirst = true)
feedRepository.saveSearchQuery(debouncedText)
loadNextPage()
}
}
}

fun onSearchTextChanged(newQuery: String) {
setState { copy(searchQuery = newQuery) }
searchQueryFlow.value = newQuery
setState{ copy(searchQuery = newQuery) }
}

fun onSearchStateChanged(newSearchState: SearchWidgetState) {
setState {
copy(
searchWidgetState = newSearchState
)
}
setState { copy(searchWidgetState = newSearchState) }
}

fun redecorateContent() {
Expand Down Expand Up @@ -113,21 +86,23 @@ class FeedViewModel(
if (state.value.loadingMore) {
return
}
setState {
copy(
loading = startFromFirst,
loadingMore = !startFromFirst,
reachedEnd = false,
blockingError = null,
nonBlockingError = null,
)
}
launchUiCoroutine {
println("State before feed update from DB ${state.value.searchQuery}")
try {
val searchQuery = feedRepository.searchQuery() ?: DEFAULT_SEARCH_QUERY
setState {
copy(
loading = startFromFirst,
loadingMore = !startFromFirst,
reachedEnd = false,
blockingError = null,
nonBlockingError = null,
searchQuery = searchQuery,
)
}

val feedUpdate = getSectionedFeedUseCase(
pageId = nextPage,
feedQuery = state.value.searchQuery ?: "test",
feedQuery = searchQuery,
currentFeed = state.value.sectionedFeed,
// Only request cache if we're starting from the first page
onCache = if (startFromFirst) { cache ->
Expand Down Expand Up @@ -169,11 +144,10 @@ class FeedViewModel(
val blockingError: Throwable? = null,
val nonBlockingError: Throwable? = null,
val sectionedFeed: SectionedFeed? = null,
val searchQuery: String? = null,

// Separate or here?
val searchQuery: String = DEFAULT_SEARCH_QUERY,
val searchWidgetState: SearchWidgetState = SearchWidgetState.CLOSED,
)
}

private const val DEBOUNCE_INTERVAL_MS = 500L
private const val DEFAULT_SEARCH_QUERY = "brexit,fintech"
Loading