Skip to content

Commit

Permalink
Merge pull request #3193 from CruGlobal/languagesFilterPerformance
Browse files Browse the repository at this point in the history
GT-2168 Performance Improvements for Language Filter
  • Loading branch information
frett authored Oct 27, 2023
2 parents 29cf871 + 1a705d4 commit 61659ac
Show file tree
Hide file tree
Showing 4 changed files with 55 additions and 39 deletions.
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -19,26 +22,24 @@ 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
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()) {
Expand Down Expand Up @@ -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()
Expand All @@ -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()
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -84,8 +91,10 @@ class ToolsViewModel @Inject constructor(
compareByDescending<Language> { 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,22 @@ 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
get() = localizeIfPossible(AppCompatDelegate.getApplicationLocales())
.getString(R.string.normalized_app_language)
.let { Locale.forLanguageTag(it) }

fun Context.getAppLanguageFlow(): Flow<Locale> = 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)
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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].
Expand Down

0 comments on commit 61659ac

Please sign in to comment.