Textfield listener on Jetpack compose? - android

hi i am trying to use a textfield that upon value changes, i want it to execute a function that filters out a list of items based on the text inputted?
It does not seem to do anything.
I tried adding the searchFilter function call inside onValueChanges but upon clicking on the textfield to try and type something, the keyboard doesnt pop up at all
val textState = remember { mutableStateOf(TextFieldValue()) }
TextField(
value = textState.value,
onValueChange = {
textState.value = it
viewModel.searchCharacter(textState.value.text)
}
)
If i remove the viewModel.searchCharacter(textState.value.text) and add it outside the Textfield, i can now enter something in the actual textfield but it never invokes the searchCharacter function.
My search character method is just a list that apply a filter to
fun searchCharacter(name: String) {
characterList.value?.let {
characterList.value?.let { characterList ->
characterList.filter {
it.name.contains(name)
}
}
}
}

Nothing in your code should prevent the keyboard from popping up. Have you tried running the app again? I've had issues with compose TextFields not reliably opening the keyboard.
Your searchCharacter function doesn't make sense.
Why is there a redundant let block?
filter doesn't modify a list but returns a new one. (https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/filter.html)
Here's a better way:
fun searchCharacter(name: String) {
characterList.value.filter { it.name.contains(name) }?.let { filtered ->
characterList.value = filtered
}
}

Related

Unwanted recomposition when using Context/Toast in event - Jetpack Compose

In a Jetpack Compose application, I have two composables similar to here:
#Composable
fun Main() {
println("Composed Main")
val context = LocalContext.current
var text by remember { mutableStateOf("") }
fun update(num: Number) {
text = num.toString()
Toast.makeText(context, "Toast", Toast.LENGTH_SHORT).show()
}
Column {
Text(text)
Keypad { update(it) }
}
}
#Composable
fun Keypad(onClick: (Number) -> Unit) {
println("Composed Keypad")
Column {
for (i in 1..10) {
Button(onClick = {onClick(i)}) {
Text(i.toString())
}
}
}
}
Clicking each button causes the two composables to recompose and produces this output:
I/System.out: Composed Main
I/System.out: Composed Keypad
Recomposing the Keypad composable is unneeded and makes the app freeze (for several seconds in a bigger project).
Removing usages of context in the event handles (in here, commenting out the Toast) solves the problem and does not recompose the Keypad and produces this output:
I/System.out: Composed Main
Is there any other way I could use context in an event without causing unneeded recompositions?
The issue is the Context not being a stable (#Stable) type. The lambda/callback of KeyPad is updating a state and its immediately followed by a component that uses an unstable Context, this results to the onClickLambda to be re-created (you can see its hashcode changing everytime you click a button), thus making the Keypad composable not skippable.
You can consider four approaches to deal with your issue. I also made some changes to your code removing the local function and put everything directly in the lambda/callback to make everything smaller.
For the first two, start first by creating a generic wrapper class like this.
#Stable
data class StableWrapper<T>(val value: T)
Wrapping Context in the #Stable wrapper
Using the generic wrapper class, you can consider wrapping the context and use it like this
#Composable
fun Main() {
Log.e("Composable", "Composed Main")
var text by remember { mutableStateOf("") }
val context = LocalContext.current
val contextStableWrapper = StableWrapper(context)
Column {
Text(text)
Keypad {
text = it.toString()
Toast.makeText(contextStableWrapper.value, "Toast", Toast.LENGTH_SHORT).show()
}
}
}
Wrapping your Toast in the #Stable wrapper
Toast is also an unstable type, so you have to make it "stable" with this second approach.
Note that this only applies if your Toast message will not change.
Hoist them up above your Main where you'll create an instance of your static-message Toast and put it inside the stable wrapper
val toastWrapper = StableWrapper(
Toast.makeText(LocalContext.current, "Toast", Toast.LENGTH_SHORT)
)
Main(toastWrapper = toastWrapper)
and your Main composable will look like this
#Composable
fun Main(toastWrapper: StableWrapper<Toast>) {
Log.e("Composable", "Composed Main")
var text by remember { mutableStateOf("") }
Column {
Text(text)
Keypad {
text = it.toString()
toastWrapper.value.show()
}
}
}
remember{…} the Context
(I might expect some correction here), I think this is called "memoizing the value (Context) inside remember{…}", this looks similar to a deferred read.
#Composable
fun Main() {
Log.e("Composable", "Composed Main")
var text by remember { mutableStateOf("") }
val context = LocalContext.current
val rememberedContext = remember { { context } }
Column {
Text(text)
Keypad {
text = it.toString()
Toast.makeText(rememberedContext(), "Toast", Toast.LENGTH_SHORT).show()
}
}
}
Use Side-Effects
You can utilize Compose Side-Effects and put the Toast in them.
Here, SideEffect will execute every post-recomposition.
SideEffect {
if (text.isNotEmpty()) {
Toast.makeText(context, "Toast", Toast.LENGTH_SHORT).show()
}
}
or you can utilize LaunchedEffect using the text as its key, so on succeeding re-compositions, when the text changes, different from its previous value (invalidates), the LaunchedEffect will re-execute and show the toast again
LaunchedEffect(key1 = text) {
if (text.isNotEmpty()) {
Toast.makeText(context, "Toast", Toast.LENGTH_SHORT).show()
}
}
Replacing your print with Log statements, this is the output of any of the approaches when clicking the buttons
E/Composable: Composed Main // first launch of screen
E/Composable: Composed Keypad // first launch of screen
// succeeding clicks
E/Composable: Composed Main
E/Composable: Composed Main
E/Composable: Composed Main
E/Composable: Composed Main
The only part I'm still not sure of is the first approach, even if Toast is not a stable type based on the second, just wrapping the context in the stable wrapper in the first approach is sufficient enough for the Keypad composable to get skipped.

Jetpack Compose: Provide initial value for TextField

I want to achieve the following use case: A payment flow where you start with a screen to enter the amount (AmountScreen) to pay and some other screens to enter other values for the payment. At the end of the flow, a summary screen (SummaryScreen) is shown where you can modify the values inline. For the sake of simplicity we will assume there is only AmountScreen followed by SummaryScreen.
Now the following requirements should be realized:
on AmountScreen you don't loose your input on configuration change
when changing a value in SummaryScreen and go back to AmountScreen (using system back), the input is set to the changed value
AmountScreen and SummaryScreen must not know about the viewModel of the payment flow (PaymentFlowViewModel, see below)
So the general problem is: we have a screen with an initial value for an input field. The initial value can be changed on another (later) screen and when navigating back to the first screen, the initial value should be set to the changed value.
I tried various approaches to achieve this without reverting to Kotlin flows (or LiveData). Is there an approach without flows to achieve this (I am quite new to compose so I might be overlooking something obvious). If flows is the correct approach, would I keep a MutableStateFlow inside the PaymentFlowViewModel for amount instead of a simple string?
Here is the approach I tried (stripped and simplified from the real world example).
General setup:
internal class PaymentFlowViewModel : ViewModel() {
var amount: String = ""
}
#Composable
internal fun NavigationGraph(viewModel: PaymentFlowViewModel = viewModel()) {
val navController = rememberNavController()
NavHost(
navController = navController,
startDestination = "AMOUNT_INPUT_SCREEN"
) {
composable("AMOUNT_INPUT_SCREEN") {
AmountInputRoute(
// called when the Continue button is clicked
onAmountConfirmed = {
viewModel.amount = it
navController.navigate("SUMMARY_SCREEN")
},
// apply the entered amount as the initial value for the input text
initialAmount = viewModel.amount
)
}
composable("SUMMARY_SCREEN") {
SummaryRoute(
// called when the amount is changed inline
onAmountChanged = {
viewModel.amount = it
},
// apply the entered amount as the initial value for the input text
amount = viewModel.amount
)
}
}
}
The classes of the AmountScreen look like this:
#Composable
internal fun AmountInputRoute(
initialAmount: String,
onAmountConfirmed: (String) -> Unit
) {
// without the "LaunchedEffect" statement below this fulfils all requirements
// except that the changed value from the SummaryScreen is not applied
val amountInputState: MutableState<String> = rememberSaveable { mutableStateOf(initialAmount) }
// inserting this fulfils the req. that the changed value from SummaryScreen is
// applied, but breaks keeping the entered value on configuration change
LaunchedEffect(Unit) {
amountInputState.value = initialAmount
}
Column {
AmountInputView(
amountInput = amountInputState.value,
onAmountChange = { amountInput ->
amountInputState.value = amountInput
}
)
Button(onClick = { onAmountConfirmed(amountInputState.value) }) {
Text(text = "Continue")
}
}
}
```
I achieved the goal with a quite complicated approach - I would think there are better alternatives out there.
What I tried that did not work: using rememberSaveable passing initialAmount as parameter for inputs. Theoretically rememberSaveable would reinitialize its value when inputs changes, but apparently this does not happen when the composable is only on the back stack and also is not executed when it gets restored from the back stack.
What I implemented that did work:
#Composable
internal fun AmountInputRoute(
initialAmount:String,
onAmountConfirmed: (String) -> Unit
) {
var changedAmount by rememberSaveable {
mutableStateOf<String?>(null)
}
val amountInput by derivedStateOf {
if (changedAmount != null)
changedAmount
else
initialAmount
}
AmountInputView(
amountInput = amountInput,
onContinueClicked = {
onAmountConfirmed(amountInput)
changedAmount = null
},
validAmountChanged = {
changedAmount = it
}
)
}
Any better ideas?

Android Compose how to update other Composable function better way?

Screenshot
I just wanna click button can log ComposeableB().or liek this , For example, if you click ComposableA, ComposableB will start an animation instead of updating the data.
Although with Compose it is generally recommended to pass events to the app logic (like the ViewModel) instead of to the app UI (Thinking in Compose), here's how your code could look like if you really need to do that:
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
#Composable
fun ComposableA() {
var addLogEntry by remember { mutableStateOf(false) }
Column {
Button(onClick = {
addLogEntry = true
}) {
Text(text = "Log")
}
ComposableB(addLogEntry = addLogEntry) {
addLogEntry = false
}
}
}
#Composable
fun ComposableB(
addLogEntry: Boolean,
onLogEntryAdded: () -> Unit
) {
if (addLogEntry) {
Log.d("Shadowmeld", "onAddLogEntry")
onLogEntryAdded()
}
}
Here you are passing a function as a second parameter (onLogEntryAdded, in lambda expression format) to ComposableB. This passed lambda expression will be called from ComposableB to modify state in ComposableA.
I believe there are better ways of doing this, like ComposableB being declared inside ComposableA (hoisting state to ComposableA) or, if that is not an option, passing the Button onClick event to a ViewModel that both ComposableA and ComposableB can observe.

Jetpack Compose - Keyboard hides TextField first time

I have create a simple example with six TextFields inside a LazyColumn, when you click the last TextField, the keyboard hides it, if you hide the keyboard and click again last TextField, works fine.
In the AndroidManifest I use "adjustPan"
android:windowSoftInputMode="adjustPan"
This is a capture when you click the last TextField first time, hides the last TextField
This is a capture when you click the last TextField second time, works correctly
This is the code
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
TestComposeTheme {
val numbers = listOf(1,2,3,4,5,6)
LazyColumn() {
items(numbers) { index->
TextField(index = index)
}
}
}
}
}
}
#Composable
fun TextField(index: Int){
var text by remember { mutableStateOf("Hello$index") }
TextField(
modifier = Modifier.padding(25.dp),
value = text,
onValueChange = { text = it },
label = { Text("TextField$index") }
)
}
Does anyone know if there is any way that the first time the last TextField is tapped, it would prevent the keyboard from hiding it
EDIT: There is a known issue:
192043120
This is already a known issue. https://issuetracker.google.com/issues/192043120
One hack to overcome this is use a column with verticalScroll
Column(Modifier.verticalScroll(rememberScrollState(), reverseScrolling = true){
// Content
}
put this in your app AndroidManifest.xml inside activity tag
<activity
...
android:windowSoftInputMode="adjustResize|stateVisible">

Jetpack Compose: Not able to show text in TextField

Recently I'm playing with Jetpack Compose and I noticed that the text may not show up in TextField.
So I have a ViewModel with Flow of ViewState.
In my Compose file, I have something similar to this:
#Composable
internal fun TestScreen() {
val state by viewModel.state.collectAsState()
TestScreen {
viewState = state,
actioner = { ... }
}
}
#Composable
private fun TestScreen(viewState: ViewState, actioner: () -> Unit) {
var name by remember {
mutableStateOf(
TextFieldValue(viewState.name)
)
}
Surface {
....
Column {
....
OutlinedTextField(
...
value = name,
onValueChange = { textFieldValue ->
name = textFieldValue
actioner(...)
}
)
}
}
}
the OutlineTextField will never show what's already inside viewState.name
However, if I change this:
var name by remember {
mutableStateOf(
TextFieldValue(viewState.name)
)
}
To this:
var name = TextFieldValue(viewState.name)
Obviously it could show the value in viewState.name.
According to the Documentation (https://developer.android.com/jetpack/compose/state#state-in-composables) in which it recommends using remember & mutableStateOf to handle the changes.
I'll be very grateful if someone could help me to explain why the code with remember doesn't work but the directly assigned value worked?
EDIT
viewState.name is a String
and I "partially solved" this issue by doing the following:
var name by remember {
mutableStateOf(
TextFieldValue("")
)
}
name = TextFieldValue(viewState.name)
then the name can be shown. But it doesn't look quite right?
remember is used just to ensure that upon recomposition, the value of the mutableStateOf object does not get re-initialised to the initial value.
For example,
#Composable
fun Test1(){
var text by mutableStateOf ("Prev Text")
Text(text)
Button(onClick = { text = "Updated Text" }){
Text("Update The Text")
}
}
would not update the text on button click. This is because button click will change the mutableStateOf text, which will trigger a recomposition. However, when the control reaches the first line of the Composable, it will re-initialise the variable text to "Prev Text".
This is where remember comes in.
If you change the initialisation above to
var text by remember { mutableStateOf ("Prev Text") },
It wil tell compose to track this variable, and "remember" its value, and use it again on recomposition, when the control reaches the initialisation logic again. Hence, remember over there acts as a "guard" that does not let the control reach into the initialisation logic, and returns that latest remembered value of the variable it currently has in store.

Categories

Resources