Android Language change using JetPack Compose - android

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

Related

Why does this code get executed twice? If recomposition... what triggers the recomposition?

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.

Understanding Compose declarative logic

I'm new with Compose and declarative programming, and I'm trying to understand it. For learning, after reading tutorials and watching courses, now I'm creating my first app.
I'm creating a compose desktop application with compose multiplatform which will give you the possibility to select a folder from the computer and display all the files inside that folder. I'm launching a JFileChooser for selecting a folder. When it's selected, a state var is changed and a Box is filled with Texts representing the names of the files inside that folder. These names are obtained by a function which uses the path returned by JFileChooser.
The app has a two strange behaviours. First because that screen has a TextField and if I write inside it, the Box filled with texts seems to be repainted calling again the function which search for the files (and those can be thousands slowing the app).
The second strange behaviour is that if I open again the JFileChooser to change the folder, it repaints correctly the Box getting the file names of that folder, but if I select the same folder selected previously, the Box is not repainted, and if a file is changed in that folder, it is a problem.
I think both issues are related with declarative compose logic - what might be wrong in each case?
This is the button that displays the JFileChooser:
var listRomsState by remember { mutableStateOf(false) }
Button(onClick = {
folderChosenPath = folderChooser()
if (folderChosenPath != "")
listRomsState = true
}) {
Text(text = "List roms")
}
This is the function that shows the JFileChooser
fun folderChooser(): String {
UIManager.setLookAndFeel("com.sun.java.swing.plaf.windows.WindowsLookAndFeel");
val f = JFileChooser()
f.fileSelectionMode = JFileChooser.DIRECTORIES_ONLY
val result: Int = f.showSaveDialog(null)
if (result == JFileChooser.APPROVE_OPTION) {
return f.selectedFile.path
} else {
return ""
}
}
Below the button that displays the file chooser is the list with the file names:
if (listRomsState) {
RomsList(File(folderChosenPath))
}
This is the RomsList function:
#Composable
fun RomsList(folder: File) {
Box (
modifier = Modifier.fillMaxSize().border(1.dp, Color.LightGray)
) {
LazyColumn(
Modifier.fillMaxSize().padding(top = 5.dp, end = 8.dp)
){
var romsList = getRomsFromFolder(folder)
items(romsList.size) {
Box (
modifier = Modifier.padding(5.dp, 0.dp, 5.dp, 0.dp).fillMaxWidth(),
contentAlignment = Alignment.CenterStart
) {
Row (horizontalArrangement = Arrangement.spacedBy(5.dp)){
Text(text = "" + (it+1), modifier = Modifier.weight(0.6f).background(color = Color(0, 0, 0, 20)))
Text(text = romsList[it].title, modifier = Modifier.weight(9.4f).background(color = Color(0, 0, 0, 20)))
}
}
Spacer(modifier = Modifier.height(5.dp))
}
}
}
}
This is the function that recursively gets all the file names of a folder:
fun getRomsFromFolder(curDir: File? = File(".")): MutableList<Rom> {
var romsList = mutableListOf<Rom>()
val filesList = curDir?.listFiles()
filesList?.let {
for (f in filesList) {
if (f.isDirectory) romsList.addAll(getRomsFromFolder(f))
if (f.isFile) {
romsList.add(Rom(f.name))
}
}
}
return romsList
}
The important mechanism you need to get used to is recomposition. I am not sure how it works in Compose Multiplatform, but in android recompositions depend on state changes. When composable function contains some kind of state, it automatically listens for it changes and gets recomposed on mutations.
During recomposition your UI elements of a composable, which is being recomposed, get drawn again with new values to represent actual states.
So, explaining your strange behaviours:
The app has a two strange behaviours. First because that screen has a
TextField and if I write inside it, the Box filled with texts seems to
be repainted calling again the function which search for the files
(and those can be thousands slowing the app).
This happens because you are changing the state - the text value of a text field. So the recomposition happens. The solution is to move all logic, that does need to get called again to separate composable. The explanation is present here
The second strange behaviour is that if I open again the JFileChooser to change the folder, it repaints correctly the Box getting the file names of that folder, but if I select the same folder selected previously, the Box is not repainted, and if a file is changed in that folder, it is a problem.
This is the case, when recomposition is needed but does not happen. This happens because the composable RomsList does not contains folder state and therefore does not recompose automatically on folder change.
You probably should not pass folder as a simple parameter. You should remember it as a state.
val folderState by remember { mutableStateOf(folder) }
However, since your folder comes to the composable from another function, one of the solutions is to create such state in the caller function and mark the function as #Composable. Recompositions are able to go downwards, so all nested composables of a composable will be recomposed on latter's recompositions.
I created a very simple composable that's identical to your compose structure based on our discussion.
Consider this code:
#Composable
fun MyTvScreen() {
Log.e("MyComposableSample", "MyTvScreen Recomposed")
var fileName by remember {
mutableStateOf("")
}
val someFile = File("")
Column {
TextField(
value = fileName,
onValueChange = {
fileName = it
}
)
RomsList(file = someFile)
}
}
#Composable
fun RomsList(file: File) {
Log.e("MyComposableSample", "RomsList Recomposed $file")
}
when you run this, and when you typed anything in the TextField, both composable will re-compose and produces this log output when you type something on the textfield
E/MyComposableSample: MyTvScreen Recomposed // initial composition
E/MyComposableSample: RomsList Recomposed // initial composition
// succeeding re-compositions when you typed something in the TextField
E/MyComposableSample: MyTvScreen Recomposed
E/MyComposableSample: RomsList Recomposed
E/MyComposableSample: MyTvScreen Recomposed
E/MyComposableSample: RomsList Recomposed
From this article I run a command and found that java.io.File is not a stable type. And your RomsList composable is not skippable, meaning, everytime the parent composable re-composes it will also re-compose RomsList
restartable fun RomsList(
unstable file: File
)
Now that we know File is not a #Stable type and we have no control over its API, I wrapped it in a custom data class like this, and modified the call-sites
#Stable
data class FileWrapper(
val file: File
)
So modifying all the codes above using FileWrapper.
#Composable
fun MyTvScreen() {
...
val someFile = FileWrapper(File(""))
Column {
TextField(
...
)
RomsList(fileWrapper = someFile)
}
}
#Composable
fun RomsList(fileWrapper: FileWrapper) {
Log.e("MyComposableSample", "RomsList Recomposed ${fileWrapper.file}")
}
Produces the log output below.
E/MyComposableSample: MyTvScreen Recomposed // initial composition
E/MyComposableSample: RomsList Recomposed // initial composition
// succeeding re-compositions when you typed something in the TextField
E/MyComposableSample: MyTvScreen Recomposed
E/MyComposableSample: MyTvScreen Recomposed
E/MyComposableSample: MyTvScreen Recomposed
E/MyComposableSample: MyTvScreen Recomposed
E/MyComposableSample: MyTvScreen Recomposed
And the running the gradle command, the report was this, RomsList is now skippable with a stable parameter, so when its parent composable recomposes RomsList will get skipped.
restartable skippable fun RomsList(
stable fileWrapper: FileWrapper
)
For your second issue, would you mind trying to replace the mutableList withmutableStateList()? which creates an instance of a SnapshotStateList?, this way any changes to the list will guarantee an update to a composable that reads it
fun getRomsFromFolder(curDir: File? = File(".")): MutableList<Rom> {
var romsList = mutableStateListOf<Rom>() // here
...
Finally I disscovered some things with the help of Steyrix, z.y and some other guys.
First, as noted here, https://developer.android.com/jetpack/compose/mental-model#recomposition composable functions only execute their code if their parameters have been changed, so that is the cause of issue 2.
Also, the main problem of both issues is that I was executing the logic that retrieves all the files inside a folder in a wrong place. It was being executed in a composable function, and that's a problem, because it will be executed each time is recomposed. So moving the logic to the onclick, just after the result of the file chooser has been received solved both issues.
Also, now I understand much more things.
Thank you guys!

rememberLauncherForActivityResult causes composable to lose state?

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.

Android Jetpack Compose Localisation issue

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,

What does Jetpack Compose remember actually do, how does it work under the hood?

Checking out codelab's basic tutorial there is a snippet to increase counter on button when clicked
#Composable
fun MyScreenContent(names: List<String> = listOf("Android", "there")) {
val counterState = remember { mutableStateOf(0) }
Column(modifier = Modifier.fillMaxHeight()) {
Column(modifier = Modifier.weight(1f)) {
for (name in names) {
Greeting(name = name)
Divider(color = Color.Black)
}
}
Counter(
count = counterState.value,
updateCount = { newCount ->
counterState.value = newCount
}
)
}
}
#Composable
fun Counter(count: Int, updateCount: (Int) -> Unit) {
Button(
onClick = { updateCount(count + 1) },
colors = ButtonConstants.defaultButtonColors(
backgroundColor = if (count > 5) Color.Green else Color.White
)
) {
Text("I've been clicked $count times")
}
}
It is clear that remember { mutableStateOf(0) } stores the state/value. My question is what remember does under the hood. Using var count = remember { 0 } or mutableStateOf(0) without remember does not increase the value.
fun MyScreenContent(names: List<String> = listOf("Android", "there")) {
var count = remember { 0 }
Column(modifier = Modifier.fillMaxHeight()) {
Column(modifier = Modifier.weight(1f)) {
for (name in names) {
Greeting(name = name)
Divider(color = Color.Black)
}
}
Counter(
count = count,
updateCount = { newCount ->
count = newCount
}
)
}
}
Snippet above does not update the value printed on Text, does remember only work with MutableState?
remember - allows you to remember state from previous recompose invocation and just this.
So if you for instance randomize color at initial run. The randomized color will going to be calculated once and reused whenever re-compose is necessary.
so ...
remember = store value just in case recompose will be called.
Now the second thing is knowing when re-compose should be actually triggered.
and there mutable states comes to help.
mutablestate = store the value AND in case i update value trigger recompose for all elements using this data.
To learn how composition and recomposition works you can check out Under the hood of Jetpack Compose article by Leland Richardson, which describes inner works very well, also youtube video here. And most of this answer uses article as reference and quoted most from it.
The implementation of the Composer contains a data structure that is closely related to a Gap Buffer. This data structure is commonly used in text editors.
A gap buffer represents a collection with a current index or cursor. It is implemented in memory with a flat array. That flat array is larger than the collection of data that it represents, with the unused space referred to as the gap.
Basically adding space near your Composable function slot table to be able to update UI dynamically with high costs since get, move, insert, and delete — are constant time operations, except for moving the gap. Moving the gap is O(n) but this does not happen often which you need to change all UI structure, on average, UIs don’t change structure very much.
#Composable
fun Counter() {
var count by remember { mutableStateOf(0) }
Button(
text="Count: $count",
onPress={ count += 1 }
)
}
When the compiler sees the Composable annotation, it inserts additional parameters and calls into the body of the function.
First, the compiler adds a call to the composer.start method and passes it a compile time generated key integer.
fun Counter($composer: Composer) {
$composer.start(123)
var count by remember($composer) { mutableStateOf(0) }
Button(
$composer,
text="Count: $count",
onPress={ count += 1 },
)
$composer.end()
}
When this composer executes it does the following:
Composer.start gets called and stores a group object
remember inserts a group object
the value that mutableStateOf returns, the state instance, is stored.
Button stores a group, followed by each of its parameters.
And then finally we arrive at composer.end.
The data structure now holds all of the objects from the composition, the entire tree in execution order, effectively a depth first traversal of the tree.
So remember needed to store a mutableState() to get value from previous composition and mutableState() is required to trigger one.
And MutableState interface uses #Stable annotation
#Stable
interface MutableState<T> : State<T> {
override var value: T
operator fun component1(): T
operator fun component2(): (T) -> Unit
}
Stable is used to communicate some guarantees to the compose compiler about how a certain type or function will behave.
When applied to a class or an interface, Stable indicates that the following must be true:
The result of equals will always return the same result for the same two instances.
When a public property of the type changes, composition will be notified.
All public property types are stable.
When applied to a function or a property, the Stable]annotation indicates that the function will return the same result if the same
parameters are passed in. This is only meaningful if the parameters
and results are themselves Stable, Immutable, or primitive.
The invariants that this annotation implies are used for optimizations by the compose compiler, and have undefined behavior if
the above assumptions are not met. As a result, one should not use this annotation unless they are certain that these conditions are satisfied.
Another source with a Video that describes how Compose works.
Codelab example mentions about remember and mutableState as
Reacting to state changes is at the very heart of Compose. Compose
apps transform data into UI by calling Composable functions. If your
data changes, you recall these functions with the new data, creating
an updated UI. Compose offers tools for observing changes in your
app's data, which will automatically recall your functions—this is
called recomposing. Compose also looks at what data is needed by an
individual composable so that it only needs to recompose components
whose data has changed and can skip composing those that are not
affected.
Under the hood, Compose uses a custom Kotlin compiler plugin so when
the underlying data changes, the composable functions can be
re-invoked to update the UI hierarchy.
To add internal state to a composable, use the mutableStateOf
function, which gives a composable mutable memory. To not have a
different state for every recomposition, remember the mutable state
using remember. And, if there are multiple instances of the composable
at different places on the screen, each copy will get its own version
of the state. You can think of internal state as a private variable in
a class.
remember{X} and remember{mutableStateOf(X)} are useful in different scenarios.
First one is required when your object doesn't need to be instantiated at each recomposition, and there is another trigger that triggers composition.
An example for this is remember{Paint()}, any object that doesn't need to be instantiated more than once or memory heavy to instantiate. If a Composable that possesses this object is recomposed, properties of your object don't change thanks to remember, if you don't use remember your object is instantiated on each recomposition and all the properties previously set are reset.
If you need a trigger(mutableStateOf) and need to have the latest value(remember) like in question choose remember{mutableStateOf()}
Variables are cleared on every compositon.
Using remember will get the previous value.
I think its equivalent to declare a mutableState in ViewModel.

Categories

Resources