I saw the new Jetpack Compose in Android and decided to check it out. I have been trying to understand some basic concepts about composables.
My question is: Can composable functions call non-composable functions?
I have searched Google to no avail.
My question is: Can composable functions call non-composable functions?
Yes. Pretty much everything in Kotlin winds up as a function call, and most functions available to you are non-composable.
Here is one of Google's bits of sample Compose UI code:
#Composable
fun NewsStory() {
val image = imageResource(R.drawable.header)
Column(
modifier = Modifier.padding(16.dp)
) {
val imageModifier = Modifier
.preferredHeight(180.dp)
.fillMaxWidth()
Image(image, modifier = imageModifier,
contentScale = ContentScale.Crop)
Spacer(Modifier.preferredHeight(16.dp))
Text("A day in Shark Fin Cove")
Text("Davenport, California")
Text("December 2018")
}
}
In that, the following functions are not #Composable:
imageResource()
Modifier.padding()
Modifier.preferredHeight()
Modifier.fillMaxWidth()
The rule is that a function marked with #Composable needs to be called by another function marked as #Composable or one of a small family of end consumers of composable functions. This is reminiscent of coroutines, where suspend functions need to be called by other suspend functions or one of a small family of end consumers of suspend functions (e.g., coroutine builders like launch()).
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.
What is the "provides" syntax in this code sample and what does it do?
LocalContentAlpha provides ContentAlpha.medium
It doesn't seem to be a standard kotlin keyword and I haven't had much luck googling for queries like "kotlin provides keyword" or "jetpack compose provides".
This shows up on the Jetpack Compose codelab, full snippet below.
#Composable
fun PhotographerCard() {
Column {
Text("Alfred Sisley", fontWeight = FontWeight.Bold)
// LocalContentAlpha is defining opacity level of its children
CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) {
Text("3 minutes ago", style = MaterialTheme.typography.body2)
}
}
}
#Preview
#Composable
fun PhotographerCardPreview() {
LayoutsCodelabTheme {
PhotographerCard()
}
}
This is an example of an infix function:
Functions marked with the infix keyword can also be called using the infix notation (omitting the dot and the parentheses for the call).
As seen by the existence of the infix keyword on the method's documentation.
So the method could be called normally as LocalContentAlpha.provides(ContentAlpha.medium), but the infix notation allows for those extra syntax characters to be dropped.
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 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.