From 1e544dc821a73203e437713e9cbbdb1064a6d2b2 Mon Sep 17 00:00:00 2001 From: Daniel Frett Date: Wed, 25 Oct 2023 12:59:35 -0600 Subject: [PATCH 1/3] resolve the appLanguage on a background thread --- .../src/main/kotlin/org/cru/godtools/base/AppLanguage.kt | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/library/base/src/main/kotlin/org/cru/godtools/base/AppLanguage.kt b/library/base/src/main/kotlin/org/cru/godtools/base/AppLanguage.kt index 749022d539..992d62ea2a 100644 --- a/library/base/src/main/kotlin/org/cru/godtools/base/AppLanguage.kt +++ b/library/base/src/main/kotlin/org/cru/godtools/base/AppLanguage.kt @@ -3,10 +3,11 @@ package org.cru.godtools.base import android.content.Context import androidx.appcompat.app.AppCompatDelegate import java.util.Locale +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOn import org.ccci.gto.android.common.androidx.core.content.localizeIfPossible val Context.appLanguage: Locale @@ -14,10 +15,10 @@ val Context.appLanguage: Locale .getString(R.string.normalized_app_language) .let { Locale.forLanguageTag(it) } -fun Context.getAppLanguageFlow(): Flow = flow { +fun Context.getAppLanguageFlow() = flow { // TODO: is there a way to actively listen for changes? while (true) { emit(appLanguage) delay(1_000 / 60) } -}.distinctUntilChanged() +}.distinctUntilChanged().flowOn(Dispatchers.Default) From 92f9cd04d80e7633122ca618b492ea66aebe188e Mon Sep 17 00:00:00 2001 From: Daniel Frett Date: Wed, 25 Oct 2023 13:00:15 -0600 Subject: [PATCH 2/3] use remember to prevent a new flow from being created every time LocalAppLanguage.current is called --- .../main/kotlin/org/cru/godtools/base/LocalAppLanguage.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/library/base/src/main/kotlin/org/cru/godtools/base/LocalAppLanguage.kt b/library/base/src/main/kotlin/org/cru/godtools/base/LocalAppLanguage.kt index 8f01365fc9..f4f851a074 100644 --- a/library/base/src/main/kotlin/org/cru/godtools/base/LocalAppLanguage.kt +++ b/library/base/src/main/kotlin/org/cru/godtools/base/LocalAppLanguage.kt @@ -2,6 +2,7 @@ package org.cru.godtools.base import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.remember import androidx.compose.runtime.staticCompositionLocalOf import androidx.compose.ui.platform.LocalContext import java.util.Locale @@ -15,7 +16,9 @@ object LocalAppLanguage { val current: Locale @Composable get() = LocalComposition.current - ?: LocalContext.current.let { it.getAppLanguageFlow().collectAsState(it.appLanguage).value } + ?: LocalContext.current.let { + remember(it) { it.getAppLanguageFlow() }.collectAsState(it.appLanguage).value + } /** * Associates a [LocalAppLanguage] key to a value in a call to [CompositionLocalProvider]. From 1a705d4ec617a2376a7e1cca3f6a69c001a639fa Mon Sep 17 00:00:00 2001 From: Daniel Frett Date: Thu, 26 Oct 2023 14:24:23 -0600 Subject: [PATCH 3/3] switch to a LazyColumn implementation of DropdownMenu for the languages filter --- .../ui/dashboard/tools/ToolFilters.kt | 71 ++++++++++--------- .../ui/dashboard/tools/ToolsViewModel.kt | 11 ++- 2 files changed, 47 insertions(+), 35 deletions(-) diff --git a/app/src/main/kotlin/org/cru/godtools/ui/dashboard/tools/ToolFilters.kt b/app/src/main/kotlin/org/cru/godtools/ui/dashboard/tools/ToolFilters.kt index 15e6bb0718..0f7c3ba1b6 100644 --- a/app/src/main/kotlin/org/cru/godtools/ui/dashboard/tools/ToolFilters.kt +++ b/app/src/main/kotlin/org/cru/godtools/ui/dashboard/tools/ToolFilters.kt @@ -1,11 +1,14 @@ package org.cru.godtools.ui.dashboard.tools +import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.sizeIn +import androidx.compose.foundation.lazy.items import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Search import androidx.compose.material3.DropdownMenu @@ -19,10 +22,8 @@ import androidx.compose.material3.SearchBar import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier @@ -30,15 +31,15 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp -import androidx.lifecycle.viewmodel.compose.viewModel +import org.ccci.gto.android.common.androidx.compose.material3.ui.menu.LazyDropdownMenu import org.cru.godtools.R import org.cru.godtools.base.LocalAppLanguage import org.cru.godtools.base.ui.theme.GodToolsTheme import org.cru.godtools.base.ui.util.getToolCategoryName -import org.cru.godtools.model.Language.Companion.filterByDisplayAndNativeName import org.cru.godtools.ui.languages.LanguageName private val POPUP_MAX_HEIGHT = 600.dp +private val POPUP_MAX_WIDTH = 300.dp @Composable internal fun ToolFilters(viewModel: ToolsViewModel, modifier: Modifier = Modifier) = Column(modifier.fillMaxWidth()) { @@ -105,14 +106,16 @@ private fun CategoryFilter(viewModel: ToolsViewModel, modifier: Modifier = Modif } @Composable -@OptIn(ExperimentalMaterial3Api::class) +@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class) private fun LanguageFilter(viewModel: ToolsViewModel, modifier: Modifier = Modifier) { val context = LocalContext.current var expanded by rememberSaveable { mutableStateOf(false) } - val rawLanguages by viewModel.languages.collectAsState() ElevatedButton( - onClick = { expanded = !expanded }, + onClick = { + if (!expanded) viewModel.setLanguageQuery("") + expanded = !expanded + }, modifier = modifier ) { val language by viewModel.selectedLanguage.collectAsState() @@ -125,43 +128,43 @@ private fun LanguageFilter(viewModel: ToolsViewModel, modifier: Modifier = Modif ) ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) - DropdownMenu( + val query by viewModel.languageQuery.collectAsState() + val languages by viewModel.languages.collectAsState() + LazyDropdownMenu( expanded = expanded, onDismissRequest = { expanded = false }, - modifier = Modifier.heightIn(max = POPUP_MAX_HEIGHT), + modifier = Modifier.sizeIn(maxHeight = POPUP_MAX_HEIGHT, maxWidth = POPUP_MAX_WIDTH), ) { - val appLanguage = LocalAppLanguage.current - var filter by rememberSaveable { mutableStateOf("") } - val languages by remember { - derivedStateOf { rawLanguages.filterByDisplayAndNativeName(filter, context, appLanguage) } + item { + SearchBar( + query, + onQueryChange = { viewModel.setLanguageQuery(it) }, + onSearch = { viewModel.setLanguageQuery(it) }, + active = false, + onActiveChange = {}, + colors = GodToolsTheme.searchBarColors, + leadingIcon = { Icon(Icons.Filled.Search, null) }, + placeholder = { Text(stringResource(R.string.language_settings_downloadable_languages_search)) }, + content = {}, + modifier = Modifier.padding(horizontal = 12.dp) + ) + DropdownMenuItem( + text = { Text(stringResource(R.string.dashboard_tools_section_filter_language_any)) }, + onClick = { + viewModel.setSelectedLanguage(null) + expanded = false + } + ) } - SearchBar( - filter, - onQueryChange = { filter = it }, - onSearch = { filter = it }, - active = false, - onActiveChange = {}, - colors = GodToolsTheme.searchBarColors, - leadingIcon = { Icon(Icons.Filled.Search, null) }, - placeholder = { Text(stringResource(R.string.language_settings_downloadable_languages_search)) }, - content = {}, - modifier = Modifier.padding(horizontal = 12.dp) - ) - DropdownMenuItem( - text = { Text(stringResource(R.string.dashboard_tools_section_filter_language_any)) }, - onClick = { - viewModel.setSelectedLanguage(null) - expanded = false - } - ) - languages.forEach { + items(languages, key = { it.code }) { DropdownMenuItem( text = { LanguageName(it) }, onClick = { viewModel.setSelectedLanguage(it) expanded = false - } + }, + modifier = Modifier.animateItemPlacement() ) } } diff --git a/app/src/main/kotlin/org/cru/godtools/ui/dashboard/tools/ToolsViewModel.kt b/app/src/main/kotlin/org/cru/godtools/ui/dashboard/tools/ToolsViewModel.kt index e0702f2661..3a8c98837b 100644 --- a/app/src/main/kotlin/org/cru/godtools/ui/dashboard/tools/ToolsViewModel.kt +++ b/app/src/main/kotlin/org/cru/godtools/ui/dashboard/tools/ToolsViewModel.kt @@ -8,11 +8,13 @@ import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.qualifiers.ApplicationContext import java.util.Locale import javax.inject.Inject +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.flow.stateIn @@ -22,12 +24,14 @@ import org.cru.godtools.base.Settings import org.cru.godtools.db.repository.LanguagesRepository import org.cru.godtools.db.repository.ToolsRepository import org.cru.godtools.model.Language +import org.cru.godtools.model.Language.Companion.filterByDisplayAndNativeName import org.cru.godtools.model.Tool import org.cru.godtools.ui.banner.BannerType import org.greenrobot.eventbus.EventBus private const val KEY_SELECTED_CATEGORY = "selectedCategory" private const val KEY_SELECTED_LANGUAGE = "selectedLanguage" +private const val KEY_LANGUAGE_QUERY = "languageQuery" @HiltViewModel @OptIn(ExperimentalCoroutinesApi::class) @@ -57,6 +61,9 @@ class ToolsViewModel @Inject constructor( .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), null) fun setSelectedLanguage(language: Language?) = savedState.set(KEY_SELECTED_LANGUAGE, language?.code) + val languageQuery = savedState.getStateFlow(KEY_LANGUAGE_QUERY, "") + fun setLanguageQuery(query: String) = savedState.set(KEY_LANGUAGE_QUERY, query) + private val toolsForLocale = selectedLocale .flatMapLatest { if (it != null) toolsRepository.getToolsFlowForLanguage(it) else toolsRepository.getNormalToolsFlow() @@ -84,8 +91,10 @@ class ToolsViewModel @Inject constructor( compareByDescending { it.code == appLang } .then(compareByDescending { it.isAdded }) .then(Language.displayNameComparator(context, appLang)) - ) + ) to appLang } + .combine(languageQuery) { (langs, appLang), q -> langs.filterByDisplayAndNativeName(q, context, appLang) } + .flowOn(Dispatchers.Default) .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), emptyList()) val tools = toolsForLocale