Updating a Composable Function with a Lambda - android

In the Android developer docs at the following web address: https://developer.android.com/jetpack/compose/mental-model#recomposition
There is a composable function which is given as the following:
#Composable
fun ClickCounter(clicks: Int, onClick: () -> Unit) {
Button(onClick = onClick) {
Text("I've been clicked $clicks times")
}
}
It's said in the text that this produces an element which updates the number of times its been clicked every time it is clicked. However, looking at it, it seems to need a lambda function to do that.
When I try and put it into the SetContent function I get the following:
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
WorkTimerTheme {
Conversation(SampleData.conversationSample)
ClickCounter(clicks = 0) {
//Insert My Function Here
}
}
}
}
}
The comment //Insert My Function Here has been added by me. I presume that within this I have to put a Lambda which updates the clicks value of the composable, but I have no idea what to put. Does anyone know an acceptable way of writing this?

You need a MutableState to trigger recomposition and remember{} to keep previous value when recomposition occurred.
I asked a question about it and my question contains answer to your question.
#Composable
fun MyScreenContent(names: List<String> = listOf("Android", "there")) {
val counterState = remember { mutableStateOf(0) }
Column(modifier = Modifier.fillMaxHeight()) {
Counter(
count = counterState.value,
updateCount = { newCount ->
counterState.value = newCount
}
)
}
}
#Composable
fun Counter(count: Int, updateCount: (Int) -> Unit) {
Button(
onClick = { updateCount(count + 1) },
) {
Text("I've been clicked $count times")
}
}

Thanks very much to #Thracian for linking a similar question. As the answer to mine is related yet slightly different I thought I would post my own.
The correct code is as follows:
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
val counterState = remember { mutableStateOf(0) }
WorkTimerTheme {
Conversation(SampleData.conversationSample)
ClickCounter(clicks = counterState.value) {
counterState.value++
}
}
}
}
}
As suggested I added a mutableState value which appears to remember the value of something at the last "recomposition", unless it is explicitly updated. If a mutablestate is explicitly updated that will trigger a recomposition (as noted in the answer to #Thracian's question).
A recomposition will redraw the Element.
In order to update the value at recomposition the number of times the button has been clicked must be stored in the mutablestate and passed to the Composable Function at each recomposition.
Using the Composable functions Lambda argument to affect the mutable state completes the loop, updating the mutablestate which then recomposes the button with the updated value.
That is the purpose of counterState.value++.
As suggested above for more information on this, try reading this documentation: https://developer.android.com/jetpack/compose/state#viewmodel-state
The video is related to what we're discussing here.

Related

Jetpack compose when to create a new lambda instance in Composable?

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.

Jetpack Compose: MutableState<Boolean> not working as intended

In our Android app we want to introduce Compose to a simple debug screen, where we can enable/disable SharedPreferences. I'm trying to get that running using Compose' interface MutableState - but it does not work how I think it does. My plan is to temporarily use MutableState to set a boolean in SharedPreferences (before migrating to DataStore later).
Here is what I had in mind:
private class MyOwnState(startWith: Boolean) : MutableState<Boolean> {
override var value: Boolean = startWith
override fun component1(): Boolean = value
override fun component2(): (Boolean) -> Unit = { value = it }
}
// then, in composable:
var value by remember { MyOwnState(false) }
Of course in real life I would overwrite the getter+setter of the value - but this example is enough, because it does not work. The state change is not propagated and the UI is not updated.
To illustrate this, I but together the code snippets by remember { mutableStateOf(false) } and by remember { MyOwnState(false) }. The first one works (switch is updated), the second one does not.
Full code:
#Composable
fun SomeStateExamples() {
Column {
SwitchWorks()
SwitchDoesNotWork()
}
}
#Composable
fun SwitchWorks() {
var value by remember { mutableStateOf(false) }
Switch(checked = value, onCheckedChange = { value = it })
}
#Composable
fun SwitchDoesNotWork() {
var value by remember { MyOwnState(false) }
Switch(checked = value, onCheckedChange = { value = it })
}
private class MyOwnState(startWith: Boolean) : MutableState<Boolean> {
override var value: Boolean = startWith
override fun component1(): Boolean = value
override fun component2(): (Boolean) -> Unit = { value = it }
}
The first switch is togglable, the second one is not:
What am I missing? The MutableState interface is pretty simple, and stable - and I didn't find any extra methods (aka invalidate, notifyListeners, ...) that I need to call.
Thank you for your help! 🙏
Adding to Johan's answer, it looks like you also need to implement StateObject to fetch the value and update thd snapshot system. By having a look at SnapshotMutableStateImpl
override var value: T
get() = next.readable(this).value
set(value) = next.withCurrent {
if (!policy.equivalent(it.value, value)) {
next.overwritable(this, it) { this.value = value }
}
}
private var next: StateStateRecord<T> = StateStateRecord(value)
override val firstStateRecord: StateRecord
get() = next
You will see that using StateObject makes you work with StateRecords where you store the updatable value, read it and update it.
In your MyOwnState class you have to implement private mutableState value like this:
private class MyOwnState(startWith: Boolean) : MutableState<Boolean> {
private var _value by mutableStateOf(startWith)
override var value: Boolean = startWith
get() = _value
set(value) {
_value = value
field = value
}
override fun component1(): Boolean = value
override fun component2(): (Boolean) -> Unit = { value = it }
}
When you will try to change value inside composable, composition will recompose because you also changed MutableState _value. Read more about how state works in Jetpack Compose here.
Not an answer directly, but looking at how mutableStateOf works, it's also calling createSnapshotMutableState(value, policy) behind the scenes.
So I don't think just inheriting MutableState and changing that will cause Compose to initiate a recomposition and thus updating the UI.
I would probably instead try to pass in the state of the UI from outside as a model with ViewModel or LiveData and mutate that model data.

When do I need to add #Composable with Android Studio Compose?

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.

Jetpack Compose Slider performance

Using LiveData to store the value of the slider makes it lag and move jerky (I suppose because of postValue ())
#Composable
fun MyComposable(
viewModel: MyViewModel
) {
val someValue = viewModel.someValue.observeAsState()
Slider(someValue) {
viewModel.setValue(it)
}
}
class MyViewModel() : ViewModel() {
val someValue: LiveData<Float> = dataStore.someValue // MutableLiveData
fun setValue(value: Float) {
dataStore.setValue(value)
}
}
class MyDataStore() {
val someValue = MutableLiveData<Float>()
fun setValue(value: Float) {
// Some heavy logic
someValue.postValue(value)
}
}
As I understand it, postValue() takes a while, and because of this, the slider seems to be trying to resist changing the value.
In order to somehow get around this, I had to create additional State variables so that the slider would directly update its value
#Composable
fun MyComposable(
viewModel: MyViewModel
) {
val someValue = viewModel.someValue.observeAsState()
var someValue2 by remember { mutableStateOf(someValue) }
Slider(someValue2) {
someValue2 = it
viewModel.setValue(it) // I also had to remove postValue ()
}
}
As I understand it, if the data from the DataStore comes with a delay, then the value in someValue will not have time to initialize and it will be null by the time the view appears (this has not happened yet, but is it theoretically possible?), And thus the value of the slider will not be relevant. Are there any solutions to this problem?

Get previous value of state in Composable - Jetpack Compose

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
}
}

Categories

Resources