From 9b6cb4ee854d8eb23da12b012011624629c692e1 Mon Sep 17 00:00:00 2001 From: Phil Oliver <3497406+poliver@users.noreply.github.com> Date: Tue, 18 Nov 2025 03:26:25 -0500 Subject: [PATCH] Use TextFieldState --- .../node/component/NodeFilterTextField.kt | 38 +++++++------------ .../feature/node/list/NodeListScreen.kt | 1 - .../feature/node/list/NodeListViewModel.kt | 14 +++---- 3 files changed, 18 insertions(+), 35 deletions(-) diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/NodeFilterTextField.kt b/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/NodeFilterTextField.kt index 9102be32e..a52b0bd01 100644 --- a/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/NodeFilterTextField.kt +++ b/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/NodeFilterTextField.kt @@ -28,8 +28,10 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.wrapContentHeight -import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.text.input.TextFieldLineLimits +import androidx.compose.foundation.text.input.TextFieldState +import androidx.compose.foundation.text.input.clearText import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.Sort import androidx.compose.material.icons.filled.Clear @@ -37,6 +39,7 @@ import androidx.compose.material.icons.filled.Search import androidx.compose.material3.Checkbox import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton @@ -53,7 +56,6 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.focus.onFocusEvent -import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.style.TextAlign @@ -82,7 +84,6 @@ import org.meshtastic.feature.node.list.NodeFilterState fun NodeFilterTextField( modifier: Modifier = Modifier, filterState: NodeFilterState, - onTextChange: (String) -> Unit, currentSortOption: NodeSortOption, onSortSelect: (NodeSortOption) -> Unit, onToggleIncludeUnknown: () -> Unit, @@ -94,11 +95,7 @@ fun NodeFilterTextField( ) { Column(modifier = modifier.background(MaterialTheme.colorScheme.background)) { Row { - NodeFilterTextField( - filterText = filterState.filterText, - onTextChange = onTextChange, - modifier = Modifier.weight(1f), - ) + NodeFilterTextField(textState = filterState.filterText, modifier = Modifier.weight(1f)) NodeSortButton( modifier = Modifier.align(Alignment.CenterVertically), @@ -140,14 +137,14 @@ fun NodeFilterTextField( } } +@OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable -private fun NodeFilterTextField(filterText: String, onTextChange: (String) -> Unit, modifier: Modifier = Modifier) { - val focusManager = LocalFocusManager.current +private fun NodeFilterTextField(textState: TextFieldState, modifier: Modifier = Modifier) { var isFocused by remember { mutableStateOf(false) } OutlinedTextField( modifier = modifier.defaultMinSize(minHeight = 48.dp).onFocusEvent { isFocused = it.isFocused }, - value = filterText, + state = textState, placeholder = { Text( text = stringResource(Res.string.node_filter_placeholder), @@ -158,24 +155,16 @@ private fun NodeFilterTextField(filterText: String, onTextChange: (String) -> Un leadingIcon = { Icon(Icons.Default.Search, contentDescription = stringResource(Res.string.node_filter_placeholder)) }, - onValueChange = onTextChange, trailingIcon = { - if (filterText.isNotEmpty() || isFocused) { - Icon( - Icons.Default.Clear, - contentDescription = stringResource(Res.string.desc_node_filter_clear), - modifier = - Modifier.clickable { - onTextChange("") - focusManager.clearFocus() - }, - ) + if (textState.text.isNotEmpty() || isFocused) { + IconButton(onClick = { textState.clearText() }) { + Icon(Icons.Default.Clear, contentDescription = stringResource(Res.string.desc_node_filter_clear)) + } } }, textStyle = MaterialTheme.typography.bodyLarge.copy(color = MaterialTheme.colorScheme.onBackground), - maxLines = 1, + lineLimits = TextFieldLineLimits.SingleLine, keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done), - keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), ) } @@ -304,7 +293,6 @@ private fun NodeFilterTextFieldPreview() { AppTheme { NodeFilterTextField( filterState = NodeFilterState(), - onTextChange = {}, currentSortOption = NodeSortOption.LAST_HEARD, onSortSelect = {}, onToggleIncludeUnknown = {}, diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/list/NodeListScreen.kt b/feature/node/src/main/kotlin/org/meshtastic/feature/node/list/NodeListScreen.kt index 18b493173..3709630df 100644 --- a/feature/node/src/main/kotlin/org/meshtastic/feature/node/list/NodeListScreen.kt +++ b/feature/node/src/main/kotlin/org/meshtastic/feature/node/list/NodeListScreen.kt @@ -154,7 +154,6 @@ fun NodeListScreen( .background(MaterialTheme.colorScheme.surfaceDim) .padding(8.dp), filterState = state.filter, - onTextChange = { viewModel.setFilterText(it) }, currentSortOption = state.sort, onSortSelect = viewModel::setSortOption, onToggleIncludeUnknown = { viewModel.nodeFilterPreferences.toggleIncludeUnknown() }, diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/list/NodeListViewModel.kt b/feature/node/src/main/kotlin/org/meshtastic/feature/node/list/NodeListViewModel.kt index 645fca925..a1db32fe7 100644 --- a/feature/node/src/main/kotlin/org/meshtastic/feature/node/list/NodeListViewModel.kt +++ b/feature/node/src/main/kotlin/org/meshtastic/feature/node/list/NodeListViewModel.kt @@ -17,6 +17,7 @@ package org.meshtastic.feature.node.list +import androidx.compose.foundation.text.input.TextFieldState import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -61,7 +62,7 @@ constructor( private val _sharedContactRequested: MutableStateFlow = MutableStateFlow(null) val sharedContactRequested = _sharedContactRequested.asStateFlow() - private val filterText = savedStateHandle.getStateFlow(KEY_FILTER_TEXT, "") + private val filterText = mutableStateOf(TextFieldState()) private val moleculeScope = CoroutineScope(viewModelScope.coroutineContext + AndroidUiDispatcher.Main) val uiState: StateFlow by @@ -71,7 +72,6 @@ constructor( val onlineNodeCount by nodeRepository.onlineNodeCount.collectAsState(0) val totalNodeCount by nodeRepository.totalNodeCount.collectAsState(0) val connectionState by serviceRepository.connectionState.collectAsState() - val filterText by filterText val includeUnknown by nodeFilterPreferences.includeUnknown.collectAsState() val excludeInfrastructure by nodeFilterPreferences.excludeInfrastructure.collectAsState() val onlyOnline by nodeFilterPreferences.onlyOnline.collectAsState() @@ -81,7 +81,7 @@ constructor( val filter = NodeFilterState( - filterText = filterText, + filterText = filterText.value, includeUnknown = includeUnknown, excludeInfrastructure = excludeInfrastructure, onlyOnline = onlyOnline, @@ -97,7 +97,7 @@ constructor( nodeRepository .getNodes( sort = sort, - filter = filter.filterText, + filter = filter.filterText.text.toString(), includeUnknown = filter.includeUnknown, onlyOnline = filter.onlyOnline, onlyDirect = filter.onlyDirect, @@ -138,10 +138,6 @@ constructor( } } - fun setFilterText(filterText: String) { - savedStateHandle[KEY_FILTER_TEXT] = value - } - fun setSortOption(sort: NodeSortOption) { nodeFilterPreferences.setNodeSort(sort) } @@ -175,7 +171,7 @@ data class NodesUiState( ) data class NodeFilterState( - val filterText: String = "", + val filterText: TextFieldState = TextFieldState(), val includeUnknown: Boolean = false, val excludeInfrastructure: Boolean = false, val onlyOnline: Boolean = false,