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.
Related
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!
On iOS there is EmptyView here https://developer.apple.com/documentation/swiftui/emptyview. But I don't know how to implement it on Compose. If I have it, for some code is much easier for me. For example,
myList.map { item ->
if item is XItem -> EmptyView()
....
}
Don't tell me I need not it, I just know how to implement it. Thanks.
Compose is built much different than SwiftUI.
In SwiftUI you need to use EmptyView in two cases:
When you have a genetic parameter and it should be empty in some cases - e.g. you need to define some default type in case when the parameter is not specified.
When the context requires you to return some view.
On the other side, Compose doesn't have such problems in the first place, that's why no such view exists.
In cases when SwiftUI will give you an error around an empty #ViewBuilder block, Compose will be totally fine.
In your example you can use Unit:
myList.map { item ->
if item is XItem -> Unit
....
}
Or just empty braces:
myList.map { item ->
if item is XItem -> { }
....
}
If you'll find a case when you really need some empty view, you can use Box(Modifier) - it'll be an empty view with zero size.
I think you can use a Spacer component to display an empty space.
Spacer accepts Modifier object as a parameter, you can then use this modifier to set Spacer’s width or height or both.
For instance, you can draw a Spacer in your code but this needs to be done in a composable context or inside another composable.
#Composable
fun MyComposable(){
myList.map { item ->
if item is XItem -> Spacer(modifier = Modifier.size(100.dp, 100.dp))
....
}
}
You can easily create one yourself:
#Composable
fun EmptyView() {
}
which can be replaced / inlined by
{}
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
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,
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.