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.
Related
We can write functional interfaces in Kotlin like this - function-interfaces
fun interface Sum {
fun add(a: Int, b: Int): Int
}
val sumImpl = Sum { a, b ->
return#Sum a + b
}
val testSum = sumImpl.add(4, 5)
How can we write Jetpack Composable function in same way? Below code is not working.
`
fun interface SampleText {
#Composable
fun text(data : String)
}
val textImpl = SampleText { data ->
return#SampleText #Composable { Text(data) }
}
#Composable
fun testText() = textImpl.text("Data")
I have tried this as well, but this also didn't work.
fun interface SampleText {
fun text(data : String) : #Composable () -> Unit
}
val textImpl = SampleText { data ->
#Composable { Text(data) }
}
#Composable
fun testText() = textImpl.text("Data")
The first version is not compiling in its lambda form because your interface function returns a Unit and your'e actually having a Type mismatch error, its just weird the compiler reports Internal Error when you try to return a #Composable annotated function, but the issue becomes clear if you simply return something like a String.
vs
To solve your first version, either you fully declare an object of the class like this (though its useless since you want a lambda version of your SAM interface not an actual object in the first place)
val textImpl = object: SampleText {
#Composable
override fun text(data: String) {
Text(data)
}
}
, but it will work just by simply calling the testText() function like this.
testText()
Or change it to your second version.
Now for your second version, since your interface returns a #Composable lambda, you have to invoke it as well in the call-site, making two function invocations to make it work,
testText()() // two invocations
first call invokes your testText() function, second pair of parenthesis invokes the #Composable lambda from your interface.
Or simply call .invoke()
testText().invoke() // just call the .invoke() of the returned composable lambda
Either of the implementations and calls display the text "Data"
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
The following code is from the project.
I find that fun MainScreen() add #Composable, and fun launchDetailsActivity doesn't add #Composable.
It make me confused. I think all function which apply to Compose should to add #Composable, why doesn't fun launchDetailsActivity add #Composable?
#AndroidEntryPoint
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
...
setContent {
ProvideWindowInsets {
ProvideImageLoader {
CraneTheme {
MainScreen(
onExploreItemClicked = { launchDetailsActivity(context = this, item = it) }
)
}
}
}
}
}
}
#Composable
fun MainScreen(onExploreItemClicked: OnExploreItemClicked) {
...
}
fun launchDetailsActivity(context: Context, item: ExploreModel) {
context.startActivity(createDetailsActivityIntent(context, item))
}
Function with #Composable is not just a function it tells the compose compiler that this is a UI element. Take this data and build a Widget with it.
So you have to determine when you will add #Composable based on whether this function draws something in the UI or not. In the non compose world you can think of this function like a View.
For example, this function takes a parameter name and builds a Text widget with the text "Hello $name" which you can see in the UI.
#Composable
fun Greeting(name: String) {
Text("Hello $name")
}
But
fun getName(firstName: String, lastName: String): String {
return "$firstName $lastName"
}
This function is not a composable function. It is not annotated with #Composable because it is not a widget, it shouldn't render anything in the UI. It just takes two parameters, Concatenates them, and returns the String.
In your case, MainScreen is the function that is rendering the Main screen of your app so it is a UI element. But function launchDetailsActivity doesn't draw anything in the UI. It just navigates from one activity to another activity.
Few things to remember:
Function with #Composable doesn't return anything.
You can't call a composable function from a non-composable function.
Unlike non-composable function composable function start with an Uppercase letter.
You can read this doc for details https://developer.android.com/jetpack/compose/mental-model
You need to mark view builder functions with #Composable, to be directly called from an other #Composable.
If you have a side effect function, it shouldn't be called directly from composable. It can be called from touch handlers, like click in your example, or using a side effect, like LaunchedEffect. Check out more about what's side effect in documentation.
#Composable
fun SomeView() {
// this is directly called from view builder and should be marked with #Composable
OtherView()
LaunchedEffect(Unit) {
// LaunchedEffect a side effect function, and as it's called
// from LaunchedEffect it can be a suspend fun (optionally)
handleLaunchedEffect()
}
Button(onClick = { handleButtonClick() }) { // or onClick = ::handleButtonClick
}
}
#Composable
fun OtherView() {
}
suspend fun handleLaunchedEffect() {
}
fun handleButtonClick() {
}
You should use #Composable if you are using calling another function annotated with #Composable that's it pretty simple.
Generally all #Composable functions starts with uppercase letter but some also start with lowercase like everything that starts with remember
So when you are using these functions inside another function you need to use #Composable else even android studio will yell at you because composable function can be invoked from another composable.
assume my code looks like this
#Composable
fun ExampleList() {
val tickers by exampleViewModel.tickers.observeAsState()
LazyColumn() {
items(items = tickers) { ticker ->
ExampleItem(ticker)
}
}
}
#Composable
fun ExampleItem(ticker: Ticker) {
Text(text= ticker.lastPrice)
}
is there anyway to get previous value of ticker in ExampleItem Compose everytime ticker is updated?
I'm wondering if there's something like componentDidUpdate in React Native
While the answer is technically correct, the first example renders too many times and I did not understand the second example unfortunately.
So I got back to React to see how it is done there and it is explained very good here:
This is what the hook (remember function as you will) looks like (for the curious):
function usePrevious<T>(value: T): T {
// The ref object is a generic container whose current property is mutable ...
// ... and can hold any value, similar to an instance property on a class
const ref: any = useRef<T>();
// Store current value in ref
useEffect(() => {
ref.current = value;
}, [value]); // Only re-run if value changes
// Return previous value (happens before update in useEffect above)
return ref.current;
}
The same idea can be implemented in compose un a reusable way (it is important that the #Composable should not be rerendered when setting the previous value):
/**
* Returns a dummy MutableState that does not cause render when setting it
*/
#Composable
fun <T> rememberRef(): MutableState<T?> {
// for some reason it always recreated the value with vararg keys,
// leaving out the keys as a parameter for remember for now
return remember() {
object: MutableState<T?> {
override var value: T? = null
override fun component1(): T? = value
override fun component2(): (T?) -> Unit = { value = it }
}
}
}
and the actual rememberPrevious:
#Composable
fun <T> rememberPrevious(
current: T,
shouldUpdate: (prev: T?, curr: T) -> Boolean = { a: T?, b: T -> a != b },
): T? {
val ref = rememberRef<T>()
// launched after render, so the current render will have the old value anyway
SideEffect {
if (shouldUpdate(ref.value, current)) {
ref.value = current
}
}
return ref.value
}
key values can be added to the remember function, but I've found that the remember did not work in my case, as it always rerendered even when no keys were passed in.
Usage:
#Composable
fun SomeComponent() {
...
val prevValue = rememberPrevious(currentValue)
}
I figured out that I could get last value of ticker by using remember {mutableStateOf} as below:
var lastTicker by remember { mutableStateOf(ticker)}
SideEffect {
if (lastTicker != ticker) {
// compare lastTicker to current ticker before assign new value
lastTicker = ticker
}
}
by using remember { mutableStateOf(ticker)}, I can persist value of ticker throught recomposition.
then inside SideEffect I can use lastTicker value ( to compare last ticker and current ticker in my case) before assign it to new value to use for next composition
or using derivedStateOf to watch ticker change only, avoid recomposition
val compareValue by remember(ticker) {
derivedStateOf {
// compare lastTicker to current ticker before assign new value
lastTicker = ticker
// return value
}
}
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]()