Trying to slowly integrate Compose into our app.
The app is released in a few countries, so we support some extra languages other than English.
During our first steps with Compose, we are trying to migrate a "Change Password" screen.
Here is a small code snippet:
#Composable
fun ChangePasswordScreen() {
Scaffold(
topBar = {
CustomTopBarWithBackArrow(title = stringResource(id = R.string.a_change_password))
},
modifier = Modifier
.background(Color.White)
.fillMaxSize(),
content = {
ChangePasswordScreenContent()
}
)
}
#Composable
fun ChangePasswordScreenContent() {
Column(
modifier = Modifier
.padding(16.dp)
.background(colorResource(id = R.color.white))
.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally
) {
PasswordInput(
title = stringResource(id = R.string.d_current_password),
placeholder = stringResource(id = R.string.e_password_length)
)
}
}
#Composable
fun PasswordInput(title: String, placeholder: String) {
TextFieldTitle(title = title)
PasswordTextField(placeholder = placeholder)
}
I am using the stringResource method.
The user is able to change the selected country and based on that country, we update the Locale.
For some reason, the stringResource method is not updated with the latest Locale, unless we restart the app.
The LocalConfiguration.current.locales[0].country is returning always the correct country code. But the resources are not updated.
Has anybody found a possible solution to this?
Or maybe, am I missing something?
Changing the language/region programmatically does not recompose your UI. You either have to update the state of the various screens/composable or restart your app. Updating your screens/composables can be a pain. You are better off restarting the activity:
startActivity(Intent.makeRestartActivityTask(this.intent?.component))
A better solution is to use Jetmagic. It's a framework designed to handle device configuration changes without the need to restart the app. A demo app shows how the locale is updated:
https://github.com/JohannBlake/Jetmagic
Building upon Johann's answer, you can try to manually trigger a recomposition.
Try this,
setContent{
var trigger by mutableStateOf(false)
}
Then, at the place you are changing the Locale, just trigger = !trigger. Now, since the setContent method is reading it, the entire screen will recompose.
Anyway, if your Locale Changer is located somewhere outside setContent in a non-Compose scope, you can just declare this variable on top of the activity, then just type it just like that in the setContent
setContent{
trigger
...
}
This is just to give Compose a message that this trigger is being read by setContent and it should recompose upon its value change. Since we do not really care what the value of trigger is, you can write it without using remember at all.
Seems clean to me,
Related
I am Learning Android Compose, And I was looking/playing with this code from developers.android, in github.
The projects is a simple app to demonstrate adaptive screen. Sports App
Everything works fine, but am a but confused.
I logged an item/line to Logcat. And I see that it gets executed twice? Recomposition? What is causing it?
In your code:
Log.i("info", "xxx")
Column(
modifier = Modifier.padding(4.dp)
) {
Box {
Image(painter = painterResource(R.drawable.xx))
Text()
}
Text(
text = stringResource(R.string.app_name),
)
}
The stringResource and painterResource can cause recomposition.
In compose when something triggers a recomposition, it happens in the nearest scope.
However the Box and the Column are inline function, and it means that both don't have an own recompose scopes.
In your code when the Image and the Text are recomposed all the composable is recomposed.
I am trying to change locale of the application using jetpack compose function like below
#Composable
fun SetLanguage(position: Int) {
val locale = Locale(
when (position) {
0 -> "ar"
1 -> "en"
2 -> "fr"
else -> {
"ar"
}
}
)
Locale.setDefault(locale)
val configuration = LocalConfiguration.current
configuration.setLocale(locale)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1)
configuration.setLocale(locale)
else
configuration.locale = locale
var resources = LocalContext.current.resources
resources.updateConfiguration(configuration, resources.displayMetrics)
}
you can check the working example (without buttons or textfield ) here
https://github.com/MakeItEasyDev/Jetpack-Compose-Multi-Language-Support
but the problem that is not working with OutlinedTextField or Buttons as they dont change when this function is called even rightToLeft support is not working and i dont find a good alternative to this solution for my problem as i cant recreate the activity in my project
The problem many developers make when starting out with Compose is believing that when a recomposition occurs, everything within the composable will get recomposed. This isn't true. Compose looks at the composable signature and tries to determine if anything changes from the last time it was called. Only when the parameter values change will the function be called. In the source code you posted on Github, it didn't include a button or outline text field to demonstrate the problem, so I added one. When you add a button like this:
Button(onClick = {}) {
Text("Do Something")
}
the Text composable inside of the Button will only be called when the initial composition occurs. But when the Button is recomposed, the Text will not be recomposed because the last parameter in the Button function hasn't changed. Lambda functions don't change. In regard to your case, changing the language does initiate a recomposition of the button, but because the last parameter does not change, the content inside of the lambda (in this example, the Text composable) will never be called. To get around this, one solution is to make the string resource that is used by the Text composable mutable. Anything that is mutable will automatically cause any composable that uses it to recompose.
The following code is what I took from your Github project and added a button. Notice how the string resource id is made mutable and this mutable state is used inside the Text:
#Composable
fun LanguageContentPortrait(
selectedPosition: Int,
onLanguageSelected: (Int) -> Unit
) {
val buttonTextResId by remember { mutableStateOf(R.string.hello) }
CompositionLocalProvider(
LocalLayoutDirection provides
if (LocalConfiguration.current.layoutDirection == LayoutDirection.Rtl.ordinal)
LayoutDirection.Rtl
else LayoutDirection.Ltr
) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Spacer(modifier = Modifier.height(100.dp))
ToggleGroup(selectedPosition = selectedPosition, onClick = onLanguageSelected)
Spacer(modifier = Modifier.height(60.dp))
Column(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text(
text = stringResource(id = R.string.content),
modifier = Modifier.fillMaxSize(),
textAlign = TextAlign.Center
)
Button(onClick = {}) {
Text(stringResource(buttonTextResId))
}
}
}
}
}
So anywhere you use trailing lambda expressions including click event handlers and you need language-dependent changes to occur, you will need to add mutable states to those resources inside those lambdas as shown above.
Even though the solution above works, I can't recommend using it. Most apps will have a lot of language dependent components and having to create a mutable state for every resource string would be a pain. A better solution is to force your entire app to recompose whenever the language changes. Since Compose-only apps are generally only a single activity, it will cause the entire app to recompose. This will ensure that all screens recompose and force all the Text composables to recompose without the need to have a mutable state for each one. There are different ways you can force your app to recompose the entire UI tree. Unfortunately, Compose does not contain an API that lets you recompose the entire tree from scratch, so the only real solution is to restart the app.
Since your app is designed to work with device configuration changes such as language changes, you might want to check out a Compose framework I developed that was specifically designed to handle device configuration changes. It's called Jetmagic. It not only handles language changes but all the other changes like screen orientation, screen size, screen density and all the other configuration qualifiers that are used with the older view-based system. Jetmagic allows you to treat your composables like resources instead of just a bunch of functions and it handles them in the exact same way xml resources are handled under the view-based system using the same algorithm. The sample app included also shows how changing the device's system language (under Android's settings) or by changing the language programmatically, causes your composable UIs to recompose rendering the content in the correct language:
https://github.com/JohannBlake/Jetmagic
Let's say I've the following code:
#Composable
fun Widget() {
var text1 by remember { mutableStateOf("DEFAULT") }
val picker = rememberLauncherForActivityResult(
contract = ActivityResultContracts.GetMultipleContents(),
onResult = {
for (uri in it) print(uri)
},
)
Text(
text = text1,
)
Button(
onClick = { text1 = "CHANGED" },
) {
Text(
text = "Change text1",
)
}
Button(
onClick = { picker.launch("image/*") },
) {
Text(
text = "Launch Picker",
)
}
}
When my application is installed and launched for the first time if you change the text1 by pressing on the button labeled 'Change text1' and then press the button to launch the picker the composable state is lost and text1 reverts to "DEFAULT".
What is interesting is this happens only for the first time after installing and launching the app or after restarting the phone and using the app for the first time.
I also like to point out that this happens in both debug and release versions of the app.
So, I would like to know what could be the cause of this? is this a known compose bug? or is it how I instantiate/use the picker?
I think there might be a configuration change happening just the first time, which remember does not handle.
Try replacing remember with rememberSaveable. From docs:
While remember helps you retain state across recompositions, the state is not retained across configuration changes. For this, you must use rememberSaveable. rememberSaveable automatically saves any value that can be saved in a Bundle. For other values, you can pass in a custom saver object.
As the other answer already explains, remember is not enough to keep the object between configuration changes.
If you need to keep a complex object you can use rememberScoped with this library: https://github.com/sebaslogen/resaca
This is an alternative to rememberSaveable so you don't need to implement Parcelable nor Saver interfaces, but it won't keep the object between process deaths.
I have the following code in my activity:
setContent {
var testingText by remember { mutableStateOf("InitialValue") }
TextField(
value = testingText,
onValueChange = { testingText = it },
label = { Text("Testing") },
modifier = Modifier.fillMaxWidth(),
)
}
While running on an emulator, typing anything at the end of the text field, around 1 out of ~5 times - the text that I type ends up getting "InitialValue" added to whatever I type. See the below screenshot:
I believe this trivial example is following the official docs. Wondering what is causing this race condition, could it be a bug? Am I doing something wrong? Maybe emulator/IME issue?
On 1.0.1 of compose.
I have a layout which looks like this:
Row {
...
Box(
modifier = Modifier
.fillMaxHeight()
.width(50.dp)
) {
AnimatedVisibility(
visible = isSelected && selectedAnimationFinished,
enter = fadeIn(),
exit = fadeOut()
) {
...
}
}
}
But I get the compile-time error:
fun RowScope.AnimatedVisibility(visible: Boolean, modifier: Modifier = ..., enter: EnterTransition = ..., exit: ExitTransition = ..., content: AnimatedVisibilityScope.() -> Unit): Unit' can't be called in this context by implicit receiver. Use the explicit one if necessary
It appears that Kotlin finds the AnimatedVisibility function ambiguous, since Compose exposes multiple AnimatedVisibility functions with the same signature: there's a fun AnimatedVisibility with no receiver, and a fun RowScope.AnimatedVisibility which requires RowScope.
From what I can gather, Kotlin is complaining about me using the RowScope version incorrectly, but I just want to use the version with no receiver!
Using this.AnimatedVisibility also doesn't help.
The only workaround I've found that works is to fully qualify the name, like androidx.compose.animation.AnimatedVisibility(...). But I have no idea why this works.
Can anyone shed some light on this? Is there a better option I can use than fully qualifying the name?
One workaround is to use a fully qualified name:
Box {
androidx.compose.animation.AnimatedVisibility(visibile = ...) {
...
}
}
Looks like it's a bug in the language - overload resolution is not aware of #DslMarkers and such stuff. I couldn't find related issues on Kotlin bugtracker so I filed one myself - https://youtrack.jetbrains.com/issue/KT-48215.
Another workaround is creating new composable method and using it in a Row:
#Composable
fun AnimatedThings() {
Box {
AnimatedVisiblity(visible = ...) {
...
}
}
}
I had this problem too, I used StateValue for the value of the AnimatedVisibility with a default value of true.
I found out that was fixed to me by giving the StateValue a default value of false and then using LaunchedEffect to change the value to true, or by clicking on any view on the screen that changes the value to true.