How to use Icon Toggle button in Jetpack Compose? - android

I want to use Icon button in Jetpack Compose, but I couldn't understand Jetpack Compose docs. Can someone share a sample code for toggle button similar to this one?
When user clicks on a button, I want to animate it like in Instagram with a bounce animation.

You can combine IconToggleButton with transitions. Sample code (using version 1.0.0-beta05):
import android.annotation.SuppressLint
import androidx.compose.animation.animateColor
import androidx.compose.animation.core.*
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
import androidx.compose.runtime.*
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
#SuppressLint("UnusedTransitionTargetStateParameter")
#Composable
fun FavoriteButton(
isChecked: Boolean,
onClick: () -> Unit
) {
IconToggleButton(
checked = isChecked,
onCheckedChange = { onClick() }
) {
val transition = updateTransition(isChecked, label = "Checked indicator")
val tint by transition.animateColor(
label = "Tint"
) { isChecked ->
if (isChecked) Color.Red else Color.Black
}
val size by transition.animateDp(
transitionSpec = {
if (false isTransitioningTo true) {
keyframes {
durationMillis = 250
30.dp at 0 with LinearOutSlowInEasing // for 0-15 ms
35.dp at 15 with FastOutLinearInEasing // for 15-75 ms
40.dp at 75 // ms
35.dp at 150 // ms
}
} else {
spring(stiffness = Spring.StiffnessVeryLow)
}
},
label = "Size"
) { 30.dp }
Icon(
imageVector = if (isChecked) Icons.Filled.Favorite else Icons.Filled.FavoriteBorder,
contentDescription = null,
tint = tint,
modifier = Modifier.size(size)
)
}
}
#Preview("Favorite Button")
#Composable
fun FavoriteButtonPreview() {
val (isChecked, setChecked) = remember { mutableStateOf(false) }
MaterialTheme {
Surface {
FavoriteButton(
isChecked = isChecked,
onClick = { setChecked(!isChecked) }
)
}
}
}
These are the required dependencies for this sample:
dependencies {
implementation 'androidx.core:core-ktx:1.6.0-alpha01'
implementation "androidx.compose.ui:ui:1.0.0-beta05"
implementation "androidx.compose.material:material:1.0.0-beta05"
implementation "androidx.compose.ui:ui-tooling:1.0.0-beta05"
implementation 'androidx.activity:activity-compose:1.3.0-alpha06'
}
For more details about transition and keyframes and ways to customize them, see Compose's Animation documentation.

Maybe it's a good idea to use Chips instead.
Filter chips use tags or descriptive words to filter content. They can be a good alternative to toggle buttons or checkboxes.
Also it's possible to use Modifier.selectable.

Related

Strange behaviour on text color in jetpack compose

Hey I am new in jetpack compose. I tried to set window background color black and white according to theme. When I created custom theme and set background color my text color will be black.
theme.kt
package com.vivek.sportsresult.ui.theme
import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import com.google.accompanist.systemuicontroller.rememberSystemUiController
private val DarkColorScheme = darkColorScheme(
primary = Purple80,
secondary = PurpleGrey80,
tertiary = Pink80,
background = Color.Black
)
private val LightColorScheme = lightColorScheme(
primary = Purple40,
secondary = PurpleGrey40,
tertiary = Pink40,
background = Color.White
)
#Composable
fun SportsResultTheme(
darkTheme: Boolean = isDarkTheme(),
// Dynamic color is available on Android 12+
dynamicColor: Boolean = true,
content: #Composable () -> Unit
) {
val systemUiController = rememberSystemUiController()
val colorScheme = when {
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
val context = LocalContext.current
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
}
darkTheme -> {
DarkColorScheme
}
else -> {
LightColorScheme
}
}
if (darkTheme) {
systemUiController.setSystemBarsColor(
color = Color.Black
)
} else {
systemUiController.setSystemBarsColor(
color = Color.White
)
}
MaterialTheme(
colorScheme = colorScheme,
typography = Typography,
content = content
)
}
#Composable
fun isDarkTheme() = isSystemInDarkTheme()
#Composable
fun getBackgroundColor() = if (isDarkTheme()) {
DarkColorScheme.background
} else {
LightColorScheme.background
}
MainActivity.kt
package com.vivek.sportsresult
import android.os.Bundle
import android.util.Log
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.tooling.preview.Preview
import com.vivek.sportsresult.ui.theme.SportsResultTheme
import com.vivek.sportsresult.ui.theme.getBackgroundColor
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
SportsResultTheme {
// A surface container using the 'background' color from the theme
Surface(
modifier = Modifier.fillMaxSize(),
color = getBackgroundColor()
) {
Log.e("TAG", "onCreate: ")
Greeting("Android")
}
}
}
}
}
#Composable
fun Greeting(name: String) {
Text(text = "Hello $name!")
}
#Preview(showBackground = true)
#Composable
fun DefaultPreview() {
SportsResultTheme {
Greeting("Android")
}
}
When I tried to use color = getBackgroundColor() it not changing text color but if use color = MaterialTheme.colorScheme.background it working correct on dark and white theme. I don't understand why? Can someone guide me on this?
Actual Output
Expected Output
When you use MaterialTheme.colorScheme.background it works correctly because in that case the Surface composable is able to determine the correct content color (that would be the contentColor parameter) based on the background color which you set (which is the color parameter).
However, when you use getBackgroundColor() in dark mode you get back Color.Black and since you are running your code on a device with Android 12 your theme is a dynamic color theme which was created here
val colorScheme = when {
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
val context = LocalContext.current
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
}
and apparently does not contain any color that is exactly Color.Black, i.e. that is exactly Color(0xFF000000), and thus the Surface composable is unable to determine the correct content color.
When a given color is not found in the theme then the contentColor is set to LocalContentColor.current and it just happens that in your case this results in a black text on a black background.
You have 2 options:
you can use MaterialTheme.colorScheme.background and let it determine the contentColor automatically based on your theme.
you can set both color and contentColor yourself like this
Surface(
modifier = Modifier.fillMaxSize(),
color = getBackgroundColor(),
contentColor = /* get and set some content color */,
)
This behavior is due to how Surface contentColor parameter default value is implemented by calling contentColorFor(color). See the Surface composable implementation and contentColorFor implementation (in Android Studio you Ctrl/Cmd+click on them, or open the context menu > Go To > Implementation).

Jetpack Compose Timing Issue with Stack of Cards

Actually I'm not even sure if this is a timing issue, but let's begin with the code first.
I start out in my MainActivity where I prepare a simple data structure containing letters from A to Z.
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
val model = mutableStateListOf<Char>()
model.addAll(('A'..'Z').toList())
val swipeComplete = {
model.removeFirst()
}
CardStack(elements = model, onSwipeComplete = { swipeComplete() })
}
}
}
Here I am calling CardStack, which looks like the following:
#Composable
fun CardStack(elements: List<Char>, onSwipeComplete: () -> Unit) {
elements.take(2).reversed().forEachIndexed { _, character ->
Box {
SwipeCard(
character.toString(),
onSwipeComplete = onSwipeComplete
)
}
}
}
When swiping a card, I want to view the card underneath it as well. Therefore I am only taking the two top-most cards and display them. Then comes the SwipeCard itself.
#OptIn(ExperimentalMaterialApi::class)
#Composable
fun SwipeCard(text: String, onSwipeComplete: () -> Unit) {
val color by remember {
val random = Random()
mutableStateOf(Color(random.nextInt(256), random.nextInt(256), random.nextInt(256)))
}
val screenWidth = LocalConfiguration.current.screenWidthDp.dp.value
val screenDensity = LocalConfiguration.current.densityDpi
var offsetXTarget by remember { mutableStateOf(0f) }
var offsetYTarget by remember { mutableStateOf(0f) }
val swipeThreshold = abs(screenWidth * screenDensity / 100)
var dragInProgress by remember {
mutableStateOf(false)
}
val offsetX by animateFloatAsState(
targetValue = offsetXTarget,
animationSpec = tween(
durationMillis = screenDensity / 3,
easing = LinearEasing
),
finishedListener = {
if (!dragInProgress) {
onSwipeComplete()
}
}
)
val offsetY by animateFloatAsState(
targetValue = offsetYTarget,
animationSpec = tween(
durationMillis = screenDensity / 3,
easing = LinearEasing
)
)
val rotationZ = (offsetX / 60).coerceIn(-40f, 40f) * -1
Card(
shape = RoundedCornerShape(20.dp),
elevation = 0.dp,
backgroundColor = color,
modifier = Modifier
.fillMaxSize()
.padding(50.dp)
.graphicsLayer(
translationX = offsetX,
translationY = offsetY,
rotationZ = rotationZ
)
.pointerInput(Unit) {
detectDragGestures(
onDrag = { change, dragAmount ->
dragInProgress = true
change.consumeAllChanges()
offsetXTarget += dragAmount.x
offsetYTarget += dragAmount.y
},
onDragEnd = {
if (abs(offsetX) < swipeThreshold / 20) {
offsetXTarget = 0f
offsetYTarget = 0f
} else {
offsetXTarget = swipeThreshold
offsetYTarget = swipeThreshold
if (offsetX < 0) {
offsetXTarget *= -1
}
}
dragInProgress = false
}
)
}
) {
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Text(
text = text,
style = TextStyle(
fontWeight = FontWeight.Bold,
fontSize = 52.sp
),
color = Color.White
)
}
}
}
This is how it looks in action:
A few key points, let's consider the initial state with all letters from A to Z:
When I start to drag the card with letter "A", I can see card with letter "B" underneath it.
When the drag motion ends the card for letter "A" shall be animated away to either the left or the right side, depending on what side the user has chosen.
When the animation has been finished, the onSwipeComplete shall be called in order to remove the top-most element, the letter "A", of my data model.
After the top-most element has been removed from the data model I expect the stack of cards to be recomposed with letters "B" and "C".
The problem is when the card "A" is animated away, then suddenly the letter "B" is drawn on this animated card and where "B" has been is now "C".
It seems the data model is already updated while the first card with letter "A" is still being animated away.
This leaves me with only one card for letter "C" left. Underneath "C" is no other card.
For me there seems something wrong with the timing, but I can't figure out what exactly.
Here are all the imports to add:
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween
import androidx.compose.foundation.gestures.detectDragGestures
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Card
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.pointer.consumeAllChanges
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.example.swipecard.ui.theme.SwipeCardTheme
import java.util.*
import kotlin.math.abs
This also requires the following dependencies:
implementation "androidx.compose.runtime:runtime:1.0.1"
implementation "androidx.compose.runtime:runtime-livedata:1.0.1"
When you delete an item from the array, from the Compose point of view it looks as if you deleted the last view and changed the data of the other views. The view that was view A is reused for content B, and because that view has non-zero offset values, it is not visible on the screen, so you only see view C.
Using key, you can tell Compose which view is associated with which data, so that they are reused correctly:
#Composable
fun CardStack(elements: List<Char>, onSwipeComplete: () -> Unit) {
elements.take(2).reversed().forEach { character ->
key(character) {
SwipeCard(
character.toString(),
onSwipeComplete = onSwipeComplete
)
}
}
}
p.s. some comments about your code:
It's quite strange that you pass screenDensity to durationMillis. It's pretty small value in terms of millis, which makes your animation almost instant, and looks kind of strange it terms of logic.
If you don't need index from forEachIndexed, just use forEach instead of specifying _ placeholder.
Using Box as you did here, when you only have a single child and don't specify any modifiers for Box has no effect.

How to set bottom navigation bar position fixed in compose?

I wanna the navigation bar to be at the bottom of the page using jet compose (material 3)
How to set the bottom navigation bar position fixed in compose
Here is my sample code for the navigation bar
import androidx.compose.foundation.background
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.Favorite
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.navigation.NavController
import io.material.compose.ui.theme.Purple40
#OptIn(ExperimentalMaterial3Api::class)
#Composable
fun NavigationBarDemoSample(navController: NavController) {
var selectedItem by remember { mutableStateOf(0) }
val items = listOf("Songs", "Artists", "Playlists")
Scaffold(
topBar = {
CenterAlignedTopAppBar(
title = { Text("Navigation Bar") },
Modifier.background(Purple40),
navigationIcon = {
IconButton(onClick = { /* doSomething() */
navController.navigateUp()
}) {
Icon(
imageVector = Icons.Filled.ArrowBack,
contentDescription = "Localized description"
)
}
},
)
},
content = {
NavigationBar {
items.forEachIndexed { index, item ->
NavigationBarItem(
icon = { Icon(Icons.Filled.Favorite, contentDescription = null) },
label = { Text(item) },
selected = selectedItem == index,
onClick = { selectedItem = index }
)
}
}
}
)
}
Here is my output:
Just change your Scaffold adding the NavigationBar in the bottomBar parameter instead of the content parameter:
Scaffold(
topBar = /* .. */ ,
bottomBar = {
NavigationBar {
items.forEachIndexed { index, item ->
NavigationBarItem(
/** ..... */
)
}
}
}
)
Scaffold
Compose provides convenient layouts for combining Material Components into common screen patterns. Composables such as Scaffold provide slots for various components and other screen elements.
Screen content
Scaffold has a generic content trailing lambda slot. The lambda receives an instance of PaddingValues that should be applied to the content root — for example, via Modifier.padding — to offset the top and bottom bars, if they exist.
Read more: https://developer.android.com/jetpack/compose/layouts/material#scaffold
package compose.material.theme
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Favorite
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.NavigationBar
import androidx.compose.material3.NavigationBarItem
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.*
#OptIn(ExperimentalMaterial3Api::class)
#Composable
fun NavigationBarDemoSample() {
var selectedItem by remember { mutableStateOf(0) }
val items = listOf("Songs", "Artists", "Playlists")
Scaffold(
bottomBar = {
NavigationBar {
items.forEachIndexed { index, item ->
NavigationBarItem(
icon = {
androidx.compose.material.Icon(
Icons.Filled.Favorite,
contentDescription = null
)
},
label = { androidx.compose.material.Text(item) },
selected = selectedItem == index,
onClick = { selectedItem = index }
)
}
}
}
) {
// Screen content
}
}

Jetpack compose animation with images weird issue

I am seeing a weird image flickering issue with Jetpack compose. It is a simple two card deck where the top image is animated off-screen to reveal the second card. The second card displays fine for a second and then the first image flashes on the screen. I have tried with Coil, Fresco and Glide and they all behave the same way.
import androidx.activity.compose.setContent
import androidx.activity.viewModels
import androidx.compose.animation.core.Animatable
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import coil.compose.rememberImagePainter
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlin.math.roundToInt
class MainViewModel : ViewModel() {
var images = MutableLiveData(listOf(
"https://images.pexels.com/photos/212286/pexels-photo-212286.jpeg?auto=compress&cs=tinysrgb&dpr=1&w=500",
"https://images.pexels.com/photos/163016/crash-test-collision-60-km-h-distraction-163016.jpeg?auto=compress&cs=tinysrgb&dpr=1&w=500",
"https://images.pexels.com/photos/1366944/pexels-photo-1366944.jpeg?auto=compress&cs=tinysrgb&dpr=1&w=500",
"https://images.pexels.com/photos/5878501/pexels-photo-5878501.jpeg?auto=compress&cs=tinysrgb&dpr=1&w=500",
"https://images.pexels.com/photos/3846022/pexels-photo-3846022.jpeg?auto=compress&cs=tinysrgb&dpr=1&w=500",
))
}
class MainActivity : ComponentActivity() {
private val model by viewModels<MainViewModel>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MainScreen(model)
}
}
}
#Composable
fun MainScreen(model: MainViewModel) {
val images: List<String> by model.images.observeAsState(listOf())
Box(
modifier = Modifier.fillMaxSize()
) {
images?.take(2).reversed().forEach {
Card(url = it) {
val d = model.images.value?.toMutableList()
d?.let {
it.removeFirst()
model.images.value = it
}
}
}
}
}
#Composable
fun Card(
url: String,
advance: ()-> Unit = {},
){
val coroutineScope = rememberCoroutineScope()
var offsetX = remember(url) { Animatable(0f) }
Box(
modifier = Modifier
.offset { IntOffset(offsetX.value.roundToInt(), 0) }
.fillMaxSize()
.background(color = Color.White)
.clickable {
coroutineScope.launch {
offsetX.animateTo(
targetValue = 3000F
)
}
coroutineScope.launch {
delay(400)
advance()
}
}
) {
Image(
painter = rememberImagePainter(
data = url,
),
contentDescription = null,
modifier = Modifier
.size(400.dp, 400.dp)
)
}
}
Also threw it on a github here in case anyone wants to try it out:
https://github.com/studentjet/learncompose
The problem is this: you have two card views. After deleting the top card, compose reuses them and updates them with the new data. And while the top card loads the second image, it still shows the first one.
You could disable caching, but in that case the image would still flash because it would show an empty space first. Instead, you need to have the second card's view reused for the first one.
Especially for such cases key Composable is made for: it'll reuse content for the same key between recompositions even if order changes.
val images = remember {
mutableStateListOf(
"https://images.pexels.com/photos/212286/pexels-photo-212286.jpeg?auto=compress&cs=tinysrgb&dpr=1&w=500",
"https://images.pexels.com/photos/163016/crash-test-collision-60-km-h-distraction-163016.jpeg?auto=compress&cs=tinysrgb&dpr=1&w=500",
"https://images.pexels.com/photos/1366944/pexels-photo-1366944.jpeg?auto=compress&cs=tinysrgb&dpr=1&w=500",
"https://images.pexels.com/photos/5878501/pexels-photo-5878501.jpeg?auto=compress&cs=tinysrgb&dpr=1&w=500",
"https://images.pexels.com/photos/3846022/pexels-photo-3846022.jpeg?auto=compress&cs=tinysrgb&dpr=1&w=500",
)
}
Box(
modifier = Modifier.fillMaxSize()
) {
images.take(2).reversed().forEach {
key(it) {
Card(url = it) {
images.add(
images.removeFirst()
)
}
}
}
}

LazyColumn is not keeping the state of items when scrolling

Following the Pathway code labs from Google about Jetpack compose, I was trying out this code
import android.os.Bundle
import androidx.activity.compose.setContent
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.animation.animateColorAsState
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material.Button
import androidx.compose.material.ButtonDefaults
import androidx.compose.material.Divider
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.codelab.basics.ui.BasicsCodelabTheme
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MyApp {
MyScreenContent()
}
}
}
}
#Composable
fun MyApp(content: #Composable () -> Unit) {
BasicsCodelabTheme {
Surface(color = Color.Yellow) {
content()
}
}
}
#Composable
fun MyScreenContent(names: List<String> = List(1000) { "Hello Android #$it" }) {
val counterState = remember { mutableStateOf(0) }
Column(modifier = Modifier.fillMaxHeight()) {
NameList(names, Modifier.weight(1f))
Counter(
count = counterState.value,
updateCount = { newCount ->
counterState.value = newCount
}
)
}
}
#Composable
fun NameList(names: List<String>, modifier: Modifier = Modifier) {
LazyColumn(modifier = modifier) {
items(items = names) { name ->
Greeting(name = name)
Divider(color = Color.Black)
}
}
}
#Composable
fun Greeting(name: String) {
var isSelected by remember { mutableStateOf(false) }
val backgroundColor by animateColorAsState(if (isSelected) Color.Red else Color.Transparent)
Text(
text = "Hello $name!",
modifier = Modifier
.padding(24.dp)
.background(color = backgroundColor)
.clickable(onClick = { isSelected = !isSelected })
)
}
#Composable
fun Counter(count: Int, updateCount: (Int) -> Unit) {
Button(
onClick = { updateCount(count + 1) },
colors = ButtonDefaults.buttonColors(
backgroundColor = if (count > 5) Color.Green else Color.White
)
) {
Text("I've been clicked $count times")
}
}
#Preview("MyScreen preview")
#Composable
fun DefaultPreview() {
MyApp {
MyScreenContent()
}
}
I have noticed that LazyColumn will recompose the items whenever they become Visible on the screen (intended behaviour!) however, the Local state of Greeting widget is completely lost!
I believe this is a bug in Compose, Ideally the composer should consider the remember cached state. Is there an elegant way to fix this?
Thanks in advance!
[Update]
Using rememberSaveable {...} gives the power to survive the android change of configuration as well as the scrollability
Docs
Remember the value produced by init.
It behaves similarly to remember, but the stored value will survive the activity or process recreation using the saved instance state mechanism (for example it happens when the screen is rotated in the Android application).
The code now is more elegant and shorter, we don't even need to hoist the state, it can be kept internal. The only thing I am not so sure of now with using rememberSaveable is if there will be any performance penalties when the list grows bigger and bigger, say 1000 items.
#Composable
fun Greeting(name: String) {
val isSelected = rememberSaveable { mutableStateOf(false) }
val backgroundColor by animateColorAsState(if (isSelected.value) Color.Red else Color.Transparent)
Text(
text = "Hello $name! selected: ${isSelected.value}",
modifier = Modifier
.padding(24.dp)
.background(color = backgroundColor)
.clickable(onClick = {
isSelected.value = !isSelected.value
})
)
}
[Original Answer]
Based on #CommonWare's answer The LazyColumn will dispose the composables along with their states when they are off-screen this means when LazyColumn recomposes the Compsoables again it will have fresh start state. To fix this issue all that has to be done is to hoist the state to the consumer's scope, LazyColumn in this case.
Also we need to use mutableStateMapOf() instead of MutableMapOf inside the remember { ... } lambda or Compose-core engine will not be aware of this change.
So far here is the code:
#Composable
fun NameList(names: List<String>, modifier: Modifier = Modifier) {
val selectedStates = remember {
mutableStateMapOf<Int, Boolean>().apply {
names.mapIndexed { index, _ ->
index to false
}.toMap().also {
putAll(it)
}
}
}
LazyColumn(modifier = modifier) {
itemsIndexed(items = names) { index, name ->
Greeting(
name = name,
isSelected = selectedStates[index] == true,
onSelected = {
selectedStates[index] = !it
}
)
Divider(color = Color.Black)
}
}
}
Happy composing!

Categories

Resources