What is the difference between these two approaches?
val result = remember(key1, key2) { computeIt(key1, key2) } (Docs)
val result by remember { derivedStateOf { computeIt(key1, key2) } } (Docs)
Both avoid re-computation if neither key1 nor key2 has changed
.
The second also avoids re-computations if downstream states are derived, but else, they are identical in their behavior, aren't they?
derivedStateOf {} is used when your state or key is changing more than you want to update your UI. It acts as a buffer for you, buffering out the changes you don't need. That is the primary difference between a keyed remember {} and derivedStateOf {}. With remember {}, you will still recompose as much as your key changes, which isn't a bad thing if that's what you need. If you don't need those recompositions though, that is where derivedStateOf {} comes in.
Take for example showing a button only if the user has scrolled a LazyColumn.
val isVisible = lazyListState.firstVisibleItemIndex > 0
firstVisibleItemIndex will change 0, 1, 2 etc as the user scrolls and cause a recomposition for every time it changes.
I only care about if it's 0 or not 0 and only want to recompose when that condition changes.
derivedStateOf is my buffer, it's buffering out all of those extra state changes I don't need and limiting the recomposition to only when the derivedStateOf changes.
val isVisible = remember { derivedStateOf { lazyListState.firstVisibleItemIndex > 0 } }
For the example given in the question, a remember(key1, key2) {} is the correct API to use there, not derivedStateOf {} because you want your UI to update any time the key changes, there isn't any change to buffer out.
Update: There is a detailed explanation of derivedStateOf in this talk https://www.youtube.com/watch?v=ahXLwg2JYpc&t=412s
AFAIK there is no difference here. It's just a coincidence that both constructs are doing the same thing here in this context. But, there are differences!
The biggest one is that derivedStateOf is not composable and it does no caching on it's own (remember does). So derivedStateOf is meant for long running calculations that have to be run only if key changes. Or it can be used to merge multiple states that are not in composable (in custom class for example).
I think the exact explanation is blurred for "outsiders", we need some input from some compose team member here :). My source for the above is this one thread on slack and my own experiments
EDIT:
Today i learned another derivedStateOf usage, very important one. It can be used to limit recomposition count when using some very frequently used value for calculation.
Example:
// we have a variable scrollState: Int that gets updated every time user scrolls
// we want to update our counter for every 100 pixels scrolled.
// To avoid recomposition every single pixel change we are using derivedStateOf
val counter = remember {
derivedStateOf {
(scrollState / 100).roundToInt()
}
}
// this will be recomposed only on counter change, so it will "ignore" scrollState in 99% of cases
Text(counter.toString()).
My source for that is as direct as it can be - from the author of compose runtime and the snapshot system, the Chuck Jazdzewski himself. I highly recommend watching stream with him here: https://www.youtube.com/watch?v=waJ_dklg6fU
EDIT2:
We finally have some official performance documentation with small mention of derivedStateOf. So the official purpose of derivedStateOf is to limit composition count (like in my example). sauce
val result = remember(key1, key2) { computeIt(key1, key2) } re-calculates when key1 or key2 changes but derivedStateOf is for tracking a change in one or more State/MutableState as stated in documents as
var a by remember { mutableStateOf(0) }
var b by remember { mutableStateOf(0) }
val sum = remember { derivedStateOf { a + b } }
// Changing either a or b will cause CountDisplay to recompose but not trigger Example
// to recompose.
CountDisplay(sum)
It's convenient to use derivedStateOf when you need to track a change in property of a State object. The value you store in State can be an object but when you need to track one or some properties of object you need to use derivedStateOf. And if it's not derived from a State/MutableState or object with an interface with #Stable annotation Composable won't recompose since recomposition requires a state change.
For instance an Input layout or number of items that you need to trigger another recomposition after a certain threshold or state.
var numberOfItems by remember {
mutableStateOf(0)
}
// Use derivedStateOf when a certain state is calculated or derived from other state objects.
// Using this function guarantees that the calculation will only occur whenever one
// of the states used in the calculation changes.
val derivedStateMax by remember {
derivedStateOf {
numberOfItems > 5
}
}
Column(modifier = Modifier.padding(horizontal = 8.dp)) {
Row(verticalAlignment = Alignment.CenterVertically) {
Text(text = "Amount to buy: $numberOfItems", modifier = Modifier.weight(1f))
IconButton(onClick = { numberOfItems++ }) {
Icon(imageVector = Icons.Default.Add, contentDescription = "add")
}
Spacer(modifier = Modifier.width(4.dp))
IconButton(onClick = { if (derivedStateMin) numberOfItems-- }) {
Icon(imageVector = Icons.Default.Remove, contentDescription = "remove")
}
}
if (derivedStateMax) {
Text("You cannot buy more than 5 items", color = Color(0xffE53935))
}
}
This is whatsapp text input that displays icons based on whether text is empty or not by reading from text
internal fun ChatInput(modifier: Modifier = Modifier, onMessageChange: (String) -> Unit) {
var input by remember { mutableStateOf(TextFieldValue("")) }
val textEmpty: Boolean by derivedStateOf { input.text.isEmpty() }
Row(
modifier = modifier
.padding(horizontal = 8.dp, vertical = 6.dp)
.fillMaxWidth(),
verticalAlignment = Alignment.Bottom
) {
ChatTextField(
modifier = modifier.weight(1f),
input = input,
empty = textEmpty,
onValueChange = {
input = it
}
)
Spacer(modifier = Modifier.width(6.dp))
FloatingActionButton(
modifier = Modifier.size(48.dp),
backgroundColor = Color(0xff00897B),
onClick = {
if (!textEmpty) {
onMessageChange(input.text)
input = TextFieldValue("")
}
}
) {
Icon(
tint = Color.White,
imageVector = if (textEmpty) Icons.Filled.Mic else Icons.Filled.Send,
contentDescription = null
)
}
}
}
Related
There are several switches in the app's layout, and when these switches are pressed, the value of sharedPreference is changed to determine whether a specific function is performed. For example, if the funcOnOff switch is off, the voice notification function is off, and when fromOnOff is off, caller information cannot be checked when a notification is received.
I am using several source codes that work almost similarly as below. Is it possible to pass multiple android.widgets as parameters to a function so that these actions can be acted upon as a single function?
var funcOnOff: Switch = findViewById(R.id.func_on_off)
var fromOnOff: Switch = findViewById(R.id.from_on_off)
var timeOnOff: Switch = findViewById(R.id.time_on_off)
var contentOnOff: Switch = findViewById(R.id.content_on_off)
funcOnOff.setOnCheckedChangeListener { buttonView, isChecked ->
if (isChecked) {
editor.putString("func", "ON")
} else {
editor.putString("func", "OFF")
}
editor.commit()
}
fromOnOff.setOnCheckedChangeListener { buttonView, isChecked ->
if (isChecked) {
editor.putString("from", "ON")
} else {
editor.putString("from", "OFF")
}
editor.commit()
}
timeOnOff.setOnCheckedChangeListener { buttonView, isChecked ->
if (isChecked) {
editor.putString("time", "ON")
} else {
editor.putString("time", "OFF")
}
editor.commit()
}
If I understand correctly, you can make a factory method for the OnCheckedChangeListeners.
fun onCheckedChangedListenerForPreferenceKey(key: String): CompoundButton.OnCheckedChangeListener = { _, isChecked ->
if (isChecked) {
editor.putString(key, "ON") // wouldn't using putBoolean be better?
} else {
editor.putString(key, "OFF")
}
editor.commit()
}
Then you can do:
funcOnOff.setOnCheckedChangeListener(onCheckedChangedListenerForPreferenceKey("func"))
fromOnOff.setOnCheckedChangeListener(onCheckedChangedListenerForPreferenceKey("from"))
timeOnOff.setOnCheckedChangeListener(onCheckedChangedListenerForPreferenceKey("time"))
Or you can make a list of pairs of switches to preference keys, and iterate through them:
listOf(
funcOnOff to "func"
fromOnOff to "from"
timeOnOff to "time"
).forEach { (switch, key) ->
switch.setOnCheckedChangeListener(onCheckedChangedListenerForPreferenceKey(key))
}
As a general alternative to Sweeper's answer - most widget listeners (and a lot of other UI callbacks in Android) pass in the object that generated the event as a parameter. So you can create a function that checks that parameter, and acts accordingly:
// If the signature here matches the listener function, you can pass a reference
// to this function directly
fun handleSwitch(switch: CompoundButton, isChecked: Boolean) {
when(switch) {
funcOnOff -> "func"
fromOnOff -> "from"
timeOnOff -> "time"
else -> null
}?.let { key ->
editor.putString(key, if (isChecked) "ON" else "OFF").commit()
}
}
Then you can just apply that to all your switches:
val switches = listOf(funcOnOff, fromOnOff, timeOnOff)
switches.forEach { it.setonOnCheckedChangedListener(::handleSwitch) }
// or if you don't want to use a function reference
switches.forEach {
it.setOnCheckedChangedListener { switch, enabled -> handleSwitch(switch, enabled) }
}
// or since you're iterating over the switches anyway, you could make the current one
// part of the callback lambda and ignore the parameters
switches.forEach { switch ->
it.setOnCheckedChangedListener { _, _ -> handleSwitch(switch, switch.isChecked) }
}
You get the idea! Personally I feel like passing a function reference is neater, instead of creating a separate lambda for each switch, but honestly it depends - no need to overcomplicate things if a neat one-liner setup will get the job done!
Also personally I'd probably use a Map instead of the when, like in Sweeper's answer - that way you can define the switches in use and their keys in one place, and then you can assign your listeners by iterating over the map's keys. No repeating yourself by listing the keys in one place, and then again in the when, y'know? Easier to maintain too, you might forget to update one of those places - but this is just a general example!
I have created a custom UI for player an audio file and I am using Exoplayer to get the file and play it. I am not using the custom controller for exoplayer and my UI has a Slider that needs to update based on the current audio position. How can I achieve this? Please help.
var currentValue by remember { mutableStateOf(0f) }
currentValue = mediaPlayer.getCurrentPosition()
Slider(
modifier = Modifier.weight(1f),
value = currentValue ,
onValueChange = {currentValue = it },
valueRange = 0f.. mediaPlayer.contentDuration()
)
According to your code, you're expecting mediaPlayer.getCurrentPosition() to trigger recomposition somehow.
Compose can only track State object changes, which a special type created to trigger recompositions.
When you need to work with some non Compose library, you need to search for the way to track changes. In most of old libraries there're some listeners for such case.
In case of ExoPlayer there's no direct listener. In this issue you can see suggestion to use the listener to track isPlaying state. In Compose to work with listeners you can use DisposableEffect so when the view disappears you can remove the listener.
And then when it's playing - call currentPosition repeatedly with some interval. As Compose is built around Coroutines, it's pretty easy to do it with LaunchedEffect:
var currentValue by remember { mutableStateOf(0L) }
var isPlaying by remember { mutableStateOf(false) }
DisposableEffect(Unit) {
val listener = object : Player.Listener {
override fun onIsPlayingChanged(isPlaying_: Boolean) {
isPlaying = isPlaying_
}
}
mediaPlayer.addListener(listener)
onDispose {
mediaPlayer.removeListener(listener)
}
}
if (isPlaying) {
LaunchedEffect(Unit) {
while(true) {
currentValue = mediaPlayer.currentPosition
delay(1.seconds / 30)
}
}
}
The following content is from the article.
1: I don't understand fully if I can use State<ArrayList<T>> or State<mutableListOf()> for observed by Compose to trigger recomposition when they change?
2: I'm very strange why State<List<T>> and the immutable listOf() can be observed by Compose to trigger recomposition when they change but in fact List<T> and immutable listOf() are immutable, could you give me some sample codes?
Caution: Using mutable objects such as ArrayList or mutableListOf() as state in Compose will cause your users to see incorrect or stale data in your app.
Mutable objects that are not observable, such as ArrayList or a mutable data class, cannot be observed by Compose to trigger recomposition when they change.
Instead of using
non-observable mutable objects, we recommend you use an observable
data holder such as State<List> and the immutable listOf().
Image
The core concept is
Recomposition happens only when an observable state change happens.
For mutable objects, we have options to use add(), remove() and other methods and modify the object directly.
But the change would not trigger a recomposition as the change is not observable. (The object instance is NOT changed)
Even for mutable objects, we can trigger proper recomposition by assigning them to a new object instance. (The object instance is changed)
Hence using mutable objects is error-prone.
We can also, see a lint error due to this problem.
On the other hand, an immutable object like list can not be modified. They are replaced with a new object instance.
Hence they are observable and proper recomposition happens. (The object instance is changed)
Use this as an example to understand the concept.
#Composable
fun ComposeListExample() {
var mutableList: MutableState<MutableList<String>> = remember {
mutableStateOf(mutableListOf())
}
var mutableList1: MutableState<MutableList<String>> = remember {
mutableStateOf(mutableListOf())
}
var arrayList: MutableState<ArrayList<String>> = remember {
mutableStateOf(ArrayList())
}
var arrayList1: MutableState<ArrayList<String>> = remember {
mutableStateOf(ArrayList())
}
var list: MutableState<List<String>> = remember {
mutableStateOf(listOf())
}
Column(
Modifier.verticalScroll(state = rememberScrollState())
) {
// Uncomment the below 5 methods one by one to understand how they work.
// Don't uncomment multiple methods and check.
// ShowListItems("MutableList", mutableList.value)
// ShowListItems("Working MutableList", mutableList1.value)
// ShowListItems("ArrayList", arrayList.value)
// ShowListItems("Working ArrayList", arrayList1.value)
// ShowListItems("List", list.value)
Button(
onClick = {
mutableList.value.add("")
arrayList.value.add("")
val newMutableList1 = mutableListOf<String>()
mutableList1.value.forEach {
newMutableList1.add(it)
}
newMutableList1.add("")
mutableList1.value = newMutableList1
val newArrayList1 = arrayListOf<String>()
arrayList1.value.forEach {
newArrayList1.add(it)
}
newArrayList1.add("")
arrayList1.value = newArrayList1
val newList = mutableListOf<String>()
list.value.forEach {
newList.add(it)
}
newList.add("")
list.value = newList
},
) {
Text(text = "Add")
}
}
}
#Composable
private fun ShowListItems(title: String, list: List<String>) {
Text(title)
Column {
repeat(list.size) {
Text("$title Item Added")
}
}
}
P.S: Use mutableStateListOf if you have a list of items that needs to be modified as well as trigger recomposition properly.
I managed to do like this:
#Composable
fun ComposeListExample(
allObjects: List<Object>,
selectedObjects: List<Object>
) {
val selectedItems = remember {
mutableStateListOf<Object>().apply { addAll(selectedObjects) }
}
Column {
allObjects.forEach { item ->
SomeView(
title = item.title,
onSelect = {
if (selectedItems.contains(item)) {
selectedItems.remove(item)
} else {
selectedItems.add(item)
}
})
}
}
}
The Code A is from the official sample code here.
I know that in order to preserve state across recompositions, remember the mutable state using remember.
I think that the code val extraPadding by animateDpAsState(...) should be val extraPadding by remember { animateDpAsState(...) }, is it right?
BTW, val extraPadding by remember { animateDpAsState(...) } will cause the error
Composable calls are not allowed inside the calculation parameter of inline fun remember(calculation: () -> TypeVariable(T)): TypeVariable(T)
Code A
#Composable
private fun Greeting(name: String) {
var expanded by remember { mutableStateOf(false) }
val extraPadding by animateDpAsState( //Should I add remember
if (expanded) 48.dp else 0.dp
)
Surface(
color = MaterialTheme.colors.primary,
modifier = Modifier.padding(vertical = 4.dp, horizontal = 8.dp)
) {
Row(modifier = Modifier.padding(24.dp)) {
Column(modifier = Modifier
.weight(1f)
.padding(bottom = extraPadding)
) {
Text(text = "Hello, ")
Text(text = name)
}
OutlinedButton(
onClick = { expanded = !expanded }
) {
Text(if (expanded) "Show less" else "Show more")
}
}
}
}
No, you shouldn't. This function is marked with #Composable so it should be used directly in the view builder.
animateDpAsState will calculate its value depending on targetValue on each recomposition.
If you check it source code you'll see, that it uses remember inside, that's why it's marked with #Composable, and that's why you shouldn't bother about remembering some values manually.
For anyone that comes across this in the future, instead of attempting to remember the animateDpAsState it would be better to remember the expansion state. Because in this case we're trying to remember the state in an item of a list we use rememberSaveable:
var expanded by rememberSaveable { mutableStateOf(false) }
A very similar case of storing indexes across list items is shown in the official documentation here
I am writing a compose desktop app.
I have a main window:
Window(size = IntSize(600, 600)) {
// snip logic
Column(horizontalAlignment = Alignment.CenterHorizontally) {
ChessBoard(board.value, modifier = Modifier.fillMaxWidth(0.8f))
Row(modifier = Modifier
.fillMaxSize(), horizontalArrangement = Arrangement.SpaceEvenly) {
Button(onClick = previousBoard) {
Text("<")
}
Button(onClick = nextBoard) {
Text(">")
}
}
}
}
Where ChessBoardis defined as
#Composable
fun ChessBoard(board: Board, modifier: Modifier = Modifier) {
Canvas(modifier = modifier) {
// snip logic, I checked that it does not influence the result
}
}
The chessboard takes up the correct amount of space, but the buttons overlap and aren't added at the bottom as expected.
I tried tweaking the modifiers on ChessBoard, but that did not change the fact that the buttons are at the top.
On the Row, remove the Modifier.fillMaxSize()
I think what you need is Modifier.fillMaxWidth()
The Row is taking up the entire space of the screen, and the buttons inside the row, by default stay at the top of the row. To check that this is the case, try adding a Modifier.align(CenterVerticlly) to the row, and the buttons will move to the center of the screen, since the row is still occupying the whole screen. Removing the Modifier.fillMaxSize() should do it, but if you still want to keep it for other Composables, use Modifier.align(Alignment) to fix it