I would like to implement simple navigation by storing Composables into Array/Stack so that I could get them back with Back button. But I don't know how to add Composable into Array. Tried declaring anonymous Composable so that I could put its variable into stack but it doesn't compile? Can I somwhow put function name into Array?
var Details1 = #Composable
fun() {
Column(Modifier.fillMaxSize()) {
Text("Details 1")
}
}
var views = arrayOf(Details1)
This seems to work
// make an alias
typealias ComposableFun = #Composable () -> Unit
// composable function as lambda
val Test : ComposableFun = { Text("Test") }
// list of composable functions
val composableFuns = listOf(Test, Test, Test)
// elsewhere
composableFuns[0]()
Related
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?
Please see my code:
#Composable
fun RecomposeLambdaTest() {
var state by remember {
mutableStateOf("1")
}
val stateHolder = remember {
StateHolder()
}
Column {
Button(onClick = {
state += "1"
}) {
Text(text = "change the state")
}
OuterComposable(state = state) {
stateHolder// just a reference to the instance outer the scope
}
}
}
#Composable
fun OuterComposable(state: String, onClick: () -> Unit) {
LogUtil.d("lambda hashcode: ${onClick.hashCode()}")
Column {
Text(text = state)
Button(onClick = onClick) {
Log.d("Jeck", "compose 2")
Text(text = "Text")
}
}
}
//#Stable
class StateHolder{
private var b = 2
}
Every time I click button, OuterComposable recompose, and log the lambda hashcode——always different! It means that a new lambda instance is created when recompose, everytime
and I uncomment the code in StateHolder and make it look like:
#Stable
class StateHolder{
private var b = 2
}
Every time I click button, OuterComposable recompose, and log the lambda hashcode——always the same! It means that when recompose, Composer reuse the lambda
So what' s under the hood?
Edit:
Ok, make it easier, Let's change the code like this:
val stateHolder = remember {
2
}
the result is lambda is reused.
make val to var, the lambda is created when every recompose.
So I think I know that: If the lambda refenence a valuable outer scope and the valuable is not stable, recreate lambda every time.
So the question is:
Why Compose compiler do this?
Why Compiler think the StateHolder before is not stable, it only contains a private var!?
An author met the same question, here is his article——6 Jetpack Compose Guidelines to Optimize Your App Performance
He said, private property still affact stability, it seems it is a Google team's choice.
I'm trying to make Kotlin's invoke operator a #Composable, everything works fine, until I add a parameter to it, which should have a default value. See the code below:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent{
Button()
}
}
}
object Button{
#Composable
operator fun invoke(text: String = "SomeText"){
println(text) // prints: null
}
}
When the operator is not annotated as #Composable the output is SomeText, as it should be.
Is this some error in Jetpack Compose, or am I missing something?
The behavior is the same on the latest stable Compose v 1.1.1 and on 1.2.0-beta01. Kotlin 1.6.21
Based on the info provided in the comments, I decided to answer:
I'll maybe think of something better, but off the top of my head, this is what you can do for now
enum class ButtonType {
Primary,
Secondary,
Tertiary
}
Return the correct type of Button
#Composable
fun MasterButton(type: ButtonType) {
when(type) {
primary -> PrimaryButton()
secondary -> SecondaryButton()
else -> TertiaryButton() // Must provide an 'else' branch
}
}
This will do the job for you.
CORRECT APPROACH I:
I just got the correct one the moment I started typing the first approach.
#Composable
fun ( #Composable () -> Unit ).Primary(...) {
PrimaryButton()
}
Make copies for every other button.
STRONG NOTICE: This is a RIDICULOUS way of "cleaning" up the code. Nobody should ever use anything remotely resembling this ever, but since that is just what the question is about, this is how you go about doing it. Know that this will attach an extension function called Primary(...) to every single #Composable function, and that cannot change. You can't apply it to select Composable(s) only, since this is basically just an extension function that I have applied on a general labmda, since 'extension functions for extension functions' are not something that exist as of now.
I am going to take this as your question (even though it is in the comments) and try to answer the way I achieve this.
What I'm trying to achieve is a way to clean up the namespace, so that
not all Composables are available as a top-level function. The general
idea is to group all flavors of let's say Buttons (Primary, Secondary,
Tertiary) to be Composables declared as a function of object Button.
But I would like to be able to use also this Button object as a
default Button (let it be Primary) in a Compose way, so just by using
it as it would be a function, thus invoke() operator. I would have
Button.Primary(), Button.Secondary() and Button() which would be an
"alias" for Button.Primary().
My implementation is quite simple,
Expose only one top-level Composable function to have a cleaner namespace.
Pass an argument that denotes the type of the required Composable, using a sealed class.
Button Type
sealed class MyIconButtonType(
open val typeName: String,
) {
data class Default(
override val typeName: String = "default",
) : MyIconButtonType(
typeName = typeName,
)
data class BorderIconButton(
override val typeName: String = "border",
// The variant specific attributes can be added here
val borderWidth: Int,
) : MyIconButtonType(
typeName = typeName,
)
}
Button (The only composable exposed to other files)
#Composable
fun MyTestIconButton(
onClickLabel: String,
modifier: Modifier = Modifier,
data: MyIconButtonType = MyIconButtonType.Default(),
onClick: () -> Unit,
content: #Composable () -> Unit,
) {
when (data) {
is MyIconButtonType.Default -> {
// This composable should be private
MyTestIconDefaultButton(
// parameter as required
)
}
is MyIconButtonType.BorderIconButton -> {
// This composable should be private
MyTestIconDefaultButton(
// parameter as required, also make sure to pass variant specific attributes here
)
}
}
}
Usage
// For default impl
MyTestIconButton(
// default parameters
) {
}
// For specific variants
MyTestIconButton(
// default parameters
data = MyIconButtonType.BorderIconButton(
borderWidth = 10,
),
) {
}
Note:
Data class requires at least one attribute. Use object if no attributes like the typeName are required.
Like this,
sealed class MyIconButtonType {
object Default : MyIconButtonType()
data class BorderIconButton(
val borderWidth: Int,
) : MyIconButtonType()
}
Kotlin concepts that are used for reference,
Sealed classes, data classes and objects
when statement
Visibility modifiers
I'am using Compose now with passing parameters to functions, but I feel, those parameters are getting longer with each new data to be passed down the tree.
I am wondering if there is a way to pass values to other composables down the tree.
Other than using function arguments for Composables (the recommended way),
you can use CompositionLocal:
Avoid CompositionLocal for concepts that aren't thought as tree-scoped or sub-hierarchy scoped.
First, define variable of compositionLocalOf globally (e.g., in LocalCompositions.kt):
import androidx.compose.runtime.compositionLocalOf
data class Post(val title: String)
val LocalPost = compositionLocalOf { Post(title = "Ahmed") }
and then define it inside #Compose function to be used by other composables down the tree:
#Composable
fun MainView() {
...
val post = Post(title = "Shamil")
CompositionLocalProvider(LocalPost provides post) {
PostContentView()
}
...
}
Now, retrieve the value inside a #Composable:
// composable down the nodes tree
#Composable
fun PostContentView() {
...
val post = LocalPost.current
...
}
Observe variable changes
If you want to achieve the above solution but with watching changes to the variable and update the composable (node) accordingly, you can use MutableState variable.
Redefining the previous example:
LocalCompositions.kt:
import androidx.compose.runtime.compositionLocalOf
import androidx.compose.runtime.compositionLocalOf
import androidx.compose.runtime.mutableStateOf
data class Post(val title: MutableState<String> = mutableStateOf(""))
val LocalPost = compositionLocalOf { Post(title = mutableStateOf("Ahmed")) }
and then define it inside #Compose function to be used by other composables down the tree:
#Composable
fun MainView() {
...
val post = Post(title = remember { mutableStateOf("Shamil") })
CompositionLocalProvider(LocalPost provides post) {
PostContentView()
}
...
post.title.value = "Ali"
...
}
Now, retrieve the value inside a #Composable:
// composable down the nodes tree
#Composable
fun PostContentView() {
...
val post = LocalPost.current
// now this composable will be updated whenever post.title value is updated
if (post.title.value == "Jamal") {
...
}
...
}
In Kotlin, function is a first-class citizen. We can store a function in a variable as below
val functionVariable: () -> Unit = ::myFunction
fun myFunction() { }
However, for #Composable function, how can I do so?
If I did the below, it will cry foul i.e. org.jetbrains.kotlin.diagnostics.SimpleDiagnostic#e93b05f8 (error: could not render message)
val functionVariable: () -> Unit = ::myFunction
#Composable
fun myFunction() { }
Is there a way to store composable function as a variable?
Composable function reference is not yet supported (and that's what the error message actually is). Besides, #Composable annotation is part of the function signature because it adds some parameters to the function. So you need to use val functionVariable: #Composable () -> Unit = { myFunction() }.
You can declare a composable function variable, like:
var functionVariable: #Composable () -> Unit = {}
Later you can reassign it (essentially redefining it) like:
functionVariable = { myFunction1() }
or
functionVariable = { myFunction2() }
But if you have functionVariable already planted in your larger Composable, the reassignment likely won't trigger recomposation. What I do is declare a state variable like:
var functionAssigned by mutableStateOf(0)
And in you larger Composable where you plant functionVariable, do:
if (functionAssigned >= 0) functionVariable()
And whenever you want to reassign the variable, do:
functionVariable = { myFunction2() }
functionAssigned++
That will trigger needed recomposation.