Jetpack Compose - change variables inside Compsable - android

I'd like to know how I can change variables INSIDE a Composable via a method.
Of course i can do something like:
var test: String by remember { mutableStateOf("hello") }
and can change it like I want to, like (although it is a stupid example)
[stuff...].pointerInput(Unit) {
detectDragGestures(...)
{ change, dragAmount ->
test=dragamount.toString()
}
}
but how would i change the variable with some complicated method e.g.
[stuff...].pointerInput(Unit) {
detectDragGestures(...)
{ change, dragAmount ->
changeText(dragAmount)
}
}
I can only use methods outside of the composable to assign it to the value, aka
test=getMyNewTest(dragAmount)
But how can i change my 'fields' inside a composable, so that i can modify 'test' directly in my method?

If the method can see the a mutable variable (i.e. share a lexical scope) it can change it. For changeText to be able to change test, it must either have test in scope or receive a mutable reference to test as a parameter. Compose doesn't change this.
Any answer to this question outside of compose (e.g. class scopes, modules scopes, global scopes, closure capture, reference passing, etc.) works in compose as well.
For example, you could define the function as local to the composable function as,
#Composable
fun Example() {
var test by remember { mutableStateOf("hello") }
fun changeText(amount: Float) {
test = amount.toString()
}
...
[stuff...].pointerInput(Unit) {
detectDragGestures(...)
{ change, dragAmount ->
changeText(dragAmount)
}
}
...
}

Related

Lambda function used as input argument causes recomposition

Consider snippet below
fun doSomething(){
}
#Composable
fun A() {
Column() {
val counter = remember { mutableStateOf(0) }
B {
doSomething()
}
Button(onClick = { counter.value += 1 }) {
Text("Click me 2")
}
Text(text = "count: ${counter.value}")
}
}
#Composable
fun B(onClick: () -> Unit) {
Button(onClick = onClick) {
Text("click me")
}
}
Now when pressing "click me 2" button the B compose function will get recomposed although nothing inside it is got changed.
Clarification: doSomething is for demonstration purposes. If you insist on having a practical example you can consider below usage of B:
B{
coroutinScope.launch{
bottomSheetState.collapse()
doSomething()
}
}
My questions:
Why this lamda function causes recomposition
Best ways to fix it
My understanding of this problem
From compose compiler report I can see B is an skippable function and the input onClick is stable. At first I though its because lambda function is recreated on every recomposition of A and it is different to previous one. And this difference cause recomposition of B. But it's not true because if I use something else inside the lambda function, like changing a state, it won't cause recomposition.
My solutions
use delegates if possible. Like viewmode::doSomething or ::doSomething. Unfortunately its not always possible.
Use lambda function inside remember:
val action = remember{
{
doSomething()
}
}
B(action)
It seems ugly =)
3. Combinations of above.
When you click the Button "Click me 2" the A composable is recomposed because of Text(text = "count: ${counter.value}"). It happens because it recompose the scope that are reading the values that can change.
If you are using something like:
B {
Log.i("TAG","xxxx")
}
the B composable is NOT recomposed clicking the Button "Click me 2".
If you are using
B{
coroutinScope.launch{
Log.i("TAG","xxxx")
}
}
the B composable is recomposed.
When a State is read it triggers recomposition in nearest scope. And a scope is a function that is not marked with inline and returns Unit.
To use a coroutinScope you have to use rememberCoroutineScope that is a composable inline function. The the body of inline composable functions are simply copied into their call sites, such functions do not get their own recompose scopes.
To avoid it you can use:
B {
Log.i("TAG","xxxx")
}
and
#Composable
fun B(onClick: () -> Unit) {
val scope = rememberCoroutineScope()
Button(
onClick = {
scope.launch {
onClick()
}
}
) {
Text(
"click me ",
)
}
}
Sources and credits:
Thracian's answer: Jetpack Compose Smart Recomposition
What is “donut-hole skipping” in Jetpack Compose? post: https://www.jetpackcompose.app/articles/donut-hole-skipping-in-jetpack-compose
scoped recomposition: https://dev.to/zachklipp/scoped-recomposition-jetpack-compose-what-happens-when-state-changes-l78
You can use the LogCompositions composable described in the 2nd post to check the recomposition in your code.
Generally speaking, if you are using a property inside a lambda function that is unstable, it causes the child compose function unskippable and thus gets recomposed every time its parent gets recomposed. This is not something easily visible and you need to be careful with it. For example, the bellow code will cause B to get recomposed because coroutinScope is an unstable property and we are using it as an indirect input to our lambda function.
fun A(){
...
val coroutinScope = rememberCoroutineScope()
B{
coroutineScope.launch {
doSomething()
}
}
}
To bypass this you need to use remember around your lambda or delegation (:: operator). There is a note inside this video about it. around 40:05
There are many other parameters that are unstable like context. To figure them out you need to use compose compiler report.
Here is a good explanation about the why: https://multithreaded.stitchfix.com/blog/2022/08/05/jetpack-compose-recomposition/

How to make kotlin operator a Composable with parameters having default value

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

Android Compose how to update other Composable function better way?

Screenshot
I just wanna click button can log ComposeableB().or liek this , For example, if you click ComposableA, ComposableB will start an animation instead of updating the data.
Although with Compose it is generally recommended to pass events to the app logic (like the ViewModel) instead of to the app UI (Thinking in Compose), here's how your code could look like if you really need to do that:
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
#Composable
fun ComposableA() {
var addLogEntry by remember { mutableStateOf(false) }
Column {
Button(onClick = {
addLogEntry = true
}) {
Text(text = "Log")
}
ComposableB(addLogEntry = addLogEntry) {
addLogEntry = false
}
}
}
#Composable
fun ComposableB(
addLogEntry: Boolean,
onLogEntryAdded: () -> Unit
) {
if (addLogEntry) {
Log.d("Shadowmeld", "onAddLogEntry")
onLogEntryAdded()
}
}
Here you are passing a function as a second parameter (onLogEntryAdded, in lambda expression format) to ComposableB. This passed lambda expression will be called from ComposableB to modify state in ComposableA.
I believe there are better ways of doing this, like ComposableB being declared inside ComposableA (hoisting state to ComposableA) or, if that is not an option, passing the Button onClick event to a ViewModel that both ComposableA and ComposableB can observe.

Can a method parameter contain a reference to other variable instead of containing a value?

In the code below, i'd like to generalize it so I instead of viewBinding.editText.text and viewModel.property.price can use the same method for e.g viewBinding.secondEditText.text and viewModel.property.income.
I'm thinking exchanging viewBinding.editText.text for a variable defined in the primary constructor, but then I'd need the variable to contain a reference to viewBinding.editText.text/viewBinding.secondEditText.text etc. instead of containing a value.
Is this possible? I've looked at lengths for this but can't find anything useful.
fun updateProperty() {
//... other irrelevant code
if (viewBinding.editText.text.toString() != "") {
viewModel.property.price = viewBinding.editText.text.toString().toDouble()
}
//... other irrelevant code
}
You can pass parameters into a function, yeah!
This is the easy one:
fun updateProperty(editText: EditText) {
val contents = editText.text.toString()
}
simple enough, you just pass in whatever instance of an EditText and the function does something with it.
If you're just using objects with setters and getters, you can just define the type you're going to be using and pass them in. Depending on what viewmodel.property is, you might be able to pass that in as well, and access price and income on it. Maybe use an interface or a sealed class if there are other types you want to use - they need some commonality if you're going to be using a generalised function that works with them all.
Properties are a bit tricker - assuming viewmodel.property contains a var price: Double, and you didn't want to pass in property itself, just a Double that exists somewhere, you can do it like this:
import kotlin.reflect.KMutableProperty0
var wow: Double = 1.2
fun main() {
println(wow)
setVar(::wow, 6.9)
println(wow)
}
fun setVar(variable: KMutableProperty0<Double>, value: Double) {
variable.set(value)
}
>> 1.2
>> 6.9
(see Property references if you're not familiar with the :: syntax)
KMutableProperty0 represents a reference to a mutable property (a var) which doesn't have any receivers - just a basic var. And don't worry about the reflect import, this is basic reflection stuff like function references, it's part of the base Kotlin install
Yes, method parameters can also be references to classes or interfaces. And method parameters can also be references to other methods/functions/lambdas.
If you are dealing with cases that are hard to generalize, consider using some kind of inversion of control (function as parameter or lambda).
You add a lambda parameter to your updateProperty function
fun updateProperty(onUpdate: (viewBinding: YourViewBindingType, viewModel: YourViewModelType) -> Unit) {
//... other irrelevant code
// here you just call the lambda, with any parameters that might be useful 'on the other side'
onUpdate(viewBinding, viewModel)
//... other irrelevant code
}
Elsewhere in code - case 1:
updateProperty() { viewBinding, viewModel ->
if (viewBinding.editText.text.toString() != "") {
viewModel.property.price = viewBinding.editText.text.toString().toDouble()
}
}
Elsewhere in code - case 2:
updateProperty() { viewBinding, viewModel ->
if (viewBinding.secondEditText.text.toString() != "") {
viewModel.property.income = viewBinding.secondEditText.text.toString().toDouble()
}
}
Elsewhere in code - case 3:
updateProperty() { viewBinding, viewModel ->
// I am a totally different case, because I have to update two properties at once!
viewModel.property.somethingElse1 = viewBinding.thirdEditText.text.toString().toBoolean()
viewModel.property.somethingElse2 = viewBinding.fourthEditText.text
.toString().replaceAll("[- ]*", "").toInt()
}
You could then go even further and define a function for the first 2 cases, since those 2 can be generalized, and then call it inside the lambda (or even pass it as the lambda), which would save you some amount of code, if you call updateProperty() in many places in your code or simply define a simple function for each of them, and call that instead, like this
fun updatePrice() = updateProperty() { viewBinding, viewModel ->
if (viewBinding.editText.text.toString() != "") {
viewModel.property.price = viewBinding.editText.text.toString().toDouble()
}
}
fun updateIncome() = updateProperty() { viewBinding, viewModel ->
if (viewBinding.secondEditText.text.toString() != "") {
viewModel.property.income = viewBinding.secondEditText.text.toString().toDouble()
}
}
Then elsewhere in code you just call it in a really simple way
updatePrice()
updateIncome()

What's difference between "= remember" and " by remember" (Kotlin, Jetpack Compose)

I think two use case of remember exist.
first one is
#Composable
fun abc() {
var aa = remember { mutableStateOf(true) }
}
and second
#Composable
fun abc() {
var aa by remember { mutableStateOf(true) }
}
Is there any functional differencies exist? or just for convenience?
It is just for convenience, to shortify syntax. By using delegate (by keyword) you can skip relating to value because it is done under the hood.
In the documentation you can read
There are three ways to declare a MutableState object in a composable:
val mutableState = remember { mutableStateOf(default) }
var value by remember { mutableStateOf(default) }
val (value, setValue) = remember { mutableStateOf(default) }
These declarations are equivalent, and are provided as syntax sugar
for different uses of state. You should pick the one that produces the
easiest-to-read code in the composable you're writing.
Cheers
i've noticed a slight difference between both expressions :
when using
#Composable
fun abc() {
var aa = remember { mutableStateOf(true) }
}
var aa will be of type MutableState.
in case "by" is used
#Composable
fun abc() {
var aa by remember { mutableStateOf(true) }
}
var aa will be considered of type Boolean.
personally i prefer using by to preserve the primitive types.
in the first case: you have to use "aa.value" whenever you want to use the value of "aa".
in the second case: you don't have to, because under the hood you aren't using the getter of the var.
additional:
you know that whenever you call or edit a variable you are under the hood calling its getter and setter functions, so when you use "by" you are using another getter and setter which delegated to another piece of code (usually to the function the comes after "by" keyword) and then you do not have to use "aa.value", you just can write "aa" when call it or "aa = true" when you set it.
"var" have setter and getter functions because it is editable, while "val" have just getter and no setter cause it is not editable (read only).
note: if you want to read more then check this answer for a well understanding of the concept: read more

Categories

Resources