Use of State hosting to change variable in jetpack compose - android

I want to change the value of variable in jetpack compose. I am trying to use Stateful and Stateless with some code, but it have some problem to increment the value. Can you guys guide me on this.
ItemColorStateful
#Composable
fun ItemColorStateful() {
var index by remember { mutableStateOf(-1) }
Column(modifier = Modifier.fillMaxSize()) {
Text(text = "Different Color")
ButtonScopeStateless(
index = { index },
onIndexChange = {
index = it
}
)
}
}
ButtonScopeStateless
#Composable
fun ButtonScopeStateless(
index: () -> Int,
onIndexChange: (Int) -> Unit,
) {
Button(onClick = { onIndexChange(index()++) }) {
Text(text = "Click Me $index")
}
}
I am getting error on index()++.

Using the general pattern for state hoisting your stateless composable should have two parameters:
value: T: the current value to display
onValueChange: (T) -> Unit: an event that requests the value to change, where T is the proposed new value
In your case:
index: Int,
onIndexChange: (Int) -> Unit
Also you should respect the Encapsulated properties: Only stateful composables can modify their state. It's completely internal.
Use onIndexChange(index+1) instead of onIndexChange(index()++). In this way the state is modified by the ItemColorStateful.
You can use:
#Composable
fun ItemColorStateful() {
var index by remember { mutableStateOf(-1) }
Column(modifier = Modifier.fillMaxSize()) {
Text(text = "Different Color")
ButtonScopeStateless(
index = index ,
onIndexChange = {
index = it
}
)
}
}
#Composable
fun ButtonScopeStateless(
index: Int, //value=the current value to display
onIndexChange: (Int) -> Unit //an event that requests the value to change, where Int is the proposed new value
) {
Button(onClick = { onIndexChange(index+1) }) {
Text(text = "Click Me $index")
}
}

ItemColorStateful
#Composable
fun ItemColorStateful() {
var index by remember { mutableStateOf(-1) }
Column(modifier = Modifier.fillMaxSize()) {
Text(text = "Different Color")
ButtonScopeStateless(
index = index ,
onIndexChange = {
index++
}
)
}
}
ButtonScopeStateless
#Composable
fun ButtonScopeStateless(
index: Int,
onIndexChange: () -> Unit,
) {
Button(onClick = {
onIndexChange()
}) {
Text(text = "Click Me $index")
}
}

Related

Change background color of surface item when click in jetpack compose

I want to make a background color change when user select a item on Surface. I am using Column in my parent, so I can't make LazyColumn. So I am using Foreach to make a list of view. By default no view will be selected, when user click on item then I want change the color. Note: Only one item will select at a time.
ScanDeviceList
#Composable
fun ColumnScope.ScanDeviceList(
scanDeviceList: List<ScanResult>,
modifier: Modifier = Modifier,
pairSelectedDevice: () -> Unit
) {
Spacer()
AnimatedVisibility() {
Column {
Text()
Spacer()
scanDeviceList.forEachIndexed { index, scanResult ->
ClickableItemContainer(
rippleColor = AquaLightOpacity10,
content = {
ScanDeviceItem(index, scanResult, scanDeviceList)
}
){}
}
AvailableWarningText()
PairSelectedDevice(pairSelectedDevice)
}
}
}
ScanDeviceItem
#Composable
fun ScanDeviceItem(
index: Int,
scanResult: ScanResult,
scanDeviceList: List<ScanResult>
) {
Column {
if (index == 0) {
Divider(color = Cloudy, thickness = 1.dp)
}
Text(
text = scanResult.device.name,
modifier = Modifier.padding(vertical = 10.dp)
)
if (index <= scanDeviceList.lastIndex) {
Divider(color = Cloudy, thickness = 1.dp)
}
}
}
ClickableItemContainer
#OptIn(ExperimentalMaterialApi::class)
#Composable
fun ClickableItemContainer(
rippleColor: Color = TealLight,
content: #Composable (MutableInteractionSource) -> Unit,
clickAction: () -> Unit
) {
val interactionSource = remember { MutableInteractionSource() }
CompositionLocalProvider(
LocalRippleTheme provides AbcRippleTheme(rippleColor),
content = {
Surface(
onClick = { clickAction() },
interactionSource = interactionSource,
color = White
) {
content(interactionSource)
}
}
)
}
I want to something like this
Now above solution is only working on ripple effect, Now I want to extend my function to select a single item at a time. Many Thanks
You can store the selected index and use the clickAction to update its value when the user clicks on each item.
Something like:
#Composable
fun MyList(
var selectedIndex by remember { mutableStateOf(-1) }
Column() {
itemsList.forEachIndexed() { index, item ->
MyItem(
selected = selectedIndex == index,
clickAction = { selectedIndex = index }
)
}
}
#Composable
fun MyItem(
selected : Boolean = false,
clickAction: () -> Unit
){
Surface(
onClick = { clickAction() },
color = if (selected) Color.Red else Color.Yellow
) {
Text("Item...")
}
}
To have only item selected at a time, you can follow a behavior that is similar. You save your item selected in a mutableState variable, like:
var itemSelected by remember { mutableState(ScanDeviceResult()) }
After that, when you click the item, you update the itemSelected, something like:
ClickableItemContainer(
rippleColor = AquaLightOpacity10,
content = {
ScanDeviceItem(index, scanResult, scanDeviceList)
}
){ newItemSelected ->
itemSelected = newItemSelected
}
Now, each ClickableItemContainer needs to have an associated ScanResult.
#OptIn(ExperimentalMaterialApi::class)
#Composable
fun ClickableItemContainer(
itemSelected: ScanResult,
scanResult: ScanResult,
rippleColor: Color = TealLight,
content: #Composable (MutableInteractionSource) -> Unit,
clickAction: (ScanResult) -> Unit
) {
val interactionSource = remember { MutableInteractionSource() }
CompositionLocalProvider(
LocalRippleTheme provides AbcRippleTheme(rippleColor),
content = {
Surface(
onClick = { clickAction.invoke(scanResult) },
interactionSource = interactionSource,
color = if (itemSelected == scanResult) YOUR_BACKGROUND-SELECTED_COLOR else White
) {
content(interactionSource)
}
}
)
}
Now, on your ScanDeviceList method:
#Composable
fun ColumnScope.ScanDeviceList(
itemSelected: MutableState<ScanResult>,
scanDeviceList: List<ScanResult>,
modifier: Modifier = Modifier,
pairSelectedDevice: () -> Unit
) {
Spacer()
AnimatedVisibility() {
Column {
Text()
Spacer()
scanDeviceList.forEachIndexed { index, scanResult ->
ClickableItemContainer(
itemSelected = itemSelected,
scanResult = scanResult,
rippleColor = AquaLightOpacity10,
content = {
ScanDeviceItem(index, scanResult, scanDeviceList)
}
){ scanResultToSelect ->
itemSelected = scanResultToSelect
}
}
AvailableWarningText()
PairSelectedDevice(pairSelectedDevice)
}
}
}

How to use sealed class data to display alphabet index scroller

After creating a sealed class for my LazyColumn, how can I use the inital of every item for an alphabet scroller? it.? is where my problem occurs as for some reason, it does not let me accesss my sealed class and use it, i.e. itemName.
sealed class Clothes {
data class FixedSizeClothing(val itemName: Int, val sizePlaceholder: Int): Clothes()
data class MultiSizeClothing(val itemName: Int, val sizePlaceholders:
List<Int>): Clothes()
}
val clothingItems = remember { listOf(
Clothes.FixedSizeClothing(itemName = R.string.jumper, itemPlaceholder = 8),
Clothes.MultiSizeClothing(itemName = R.string.dress, itemPlaceholders = listOf(0, 2))
)
}
val headers = remember { clothingItems.map { getString(it.?).first().uppercase() }.toSet().toList() }
val listState = rememberLazyListState()
LazyColumn(
state = listState,
modifier = Modifier
.weight(1f)
.padding(it)
) {
items(clothingItems) {
val text1 = when (it) {
is Clothes.FixedSizeClothing ->
stringResource(id = it.itemName)
is Clothes.MultiSizeClothing ->
stringResource(id = it.itemName)
}
val text2 = when (it) {
is Clothes.FixedSizeClothing ->
stringResource(id = R.string.size_placeholder, it.sizePlaceholder)
is Clothes.MultiSizeClothing ->
stringResource(id = R.string.size_placeholder_and_placeholder, it.itemPlaceholders[0], it.itemPlaceholders[1])
}
Column(modifier = Modifier
.fillMaxWidth()
.clickable {}) {...}
}
}
val offsets = remember { mutableStateMapOf<Int, Float>() }
var selectedHeaderIndex by remember { mutableStateOf(0) }
val scope = rememberCoroutineScope()
fun updateSelectedIndexIfNeeded(offset: Float) {
val index = offsets
.mapValues { abs(it.value - offset) }
.entries
.minByOrNull { it.value }
?.key ?: return
if (selectedHeaderIndex == index) return
selectedHeaderIndex = index
val selectedItemIndex = clothingItems.indexOfFirst { getString(it.?).first().uppercase() == headers[selectedHeaderIndex] }
scope.launch {
listState.scrollToItem(selectedItemIndex)
}
}
Column(
verticalArrangement = Arrangement.SpaceEvenly,
modifier = Modifier
.fillMaxHeight()
.background(Color.Gray)
.pointerInput(Unit) {
detectTapGestures {
updateSelectedIndexIfNeeded(it.y)
}
}
.pointerInput(Unit) {
detectVerticalDragGestures { change, _ ->
updateSelectedIndexIfNeeded(change.position.y)
}
}
) {
headers.forEachIndexed { i, header ->
Text(
header,
modifier = Modifier
.onGloballyPositioned {
offsets[i] = it.boundsInParent().center.y
},
color = Color.White
)
}
}
You can define the val itemName: Int in the parent Clothes class and override it in you other subclasses. If you do that, you then do not need to use a when expression if you just want to access the itemName property.
The parent class can be a sealed interface instead of a sealed class. That way it is a bit more flexible and a bit less verbose when overriding its properties. Also : Clothes() then becomes just : Clothes
sealed interface Clothes {
val itemName: Int
data class FixedSizeClothing(override val itemName: Int, val sizePlaceholder: Int): Clothes
data class MultiSizeClothing(override val itemName: Int, val sizePlaceholders: List<Int>): Clothes
}
And the line where you create the headers becomes
val headers = clothingItems.map { stringResource(it.itemName).first().uppercase() }.toSet().toList()
The remember {} does not make much sense because if your data changes so can the set of initial letters. You also cannot use the stringResouce() function inside remember {}, because stringResouce() has to be used inside a #Composable function.
There is a different way of obtaining Resources and then using resources.getString(...) if you would like to retrieve resource strings inside a remember {} block. But in this case the remember {} block does not make sense due to the data potentially changing. The optimization to cache initial letters would have to be done in a different way.

Jetpack Compose LazyColumn recomposition with remember()

Ive been trying out Jetpack Compose and ran into something with the LazyColumn list and remember().
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MyApp{
MyScreen()
}
}
}
}
#Composable
fun MyApp(content: #Composable () -> Unit){
ComposeTestTheme {
// A surface container using the 'background' color from the theme
Surface(color = MaterialTheme.colors.background) {
content()
}
}
}
#Composable
fun MyScreen( names: List<String> = List(1000) {"Poofy #$it"}) {
NameList( names, Modifier.fillMaxHeight())
}
#Composable
fun NameList( names: List<String>, modifier: Modifier = Modifier ){
LazyColumn( modifier = modifier ){
items( items = names ) { name ->
val counter = remember{ mutableStateOf(0) }
Row(){
Text(text = "Hello $name")
Counter(
count = counter.value,
updateCount = { newCount -> counter.value = newCount } )
}
Divider(color = Color.Black)
}
}
}
#Composable
fun Counter(count: Int, updateCount: (Int) -> Unit) {
Button( onClick = {updateCount(count+1)} ){
Text("Clicked $count times")
}
}
This runs and creates a list of 1000 rows where each row says "Hello Poofy #N" followed by a button that says "Clicked N times".
It all works fine but if I click a button to update its count that count will not persist when it is scrolled offscreen and back on.
The LazyColumn "recycling" recomposes the row and the count. In the above sample the counter is hoisted up into NameList() but I have tried it unhoisted in Counter(). Neither works.
What is the proper way to remember the count? Must I store it in an array in the activity or something?
The representations for items are recycled, and with the new index the value of remember is reset. This is expected behavior, and you should not expect this value to persist.
You don't need to keep it in the activity, you just need to move it out of the LazyColumn. For example, you can store it in a mutable state list, as shown here:
val counters = remember { names.map { 0 }.toMutableStateList() }
LazyColumn( modifier = modifier ){
itemsIndexed(items = names) { i, name ->
Row(){
Text(text = "Hello $name")
Counter(
count = counters[i],
updateCount = { newCount -> counters[i] = newCount } )
}
Divider(color = Color.Black)
}
}
Or in a mutable state map:
val counters = remember { mutableStateMapOf<Int, Int>() }
LazyColumn( modifier = modifier ){
itemsIndexed(items = names) { i, name ->
Row(){
Text(text = "Hello $name")
Counter(
count = counters[i] ?: 0,
updateCount = { newCount -> counters[i] = newCount } )
}
Divider(color = Color.Black)
}
}
Note that remember will also be reset when screen rotates, consider using rememberSaveable instead of storing the data inside a view model.
Read more about state in Compose in documentation

How to use by delegate for mutableState yet make it passable to another function?

I have this composable function that a button will toggle show the text and hide it
#Composable
fun Greeting() {
Column {
val toggleState = remember {
mutableStateOf(false)
}
AnimatedVisibility(visible = toggleState.value) {
Text(text = "Edit", fontSize = 64.sp)
}
ToggleButton(toggleState = toggleState) {}
}
}
#Composable
fun ToggleButton(modifier: Modifier = Modifier,
toggleState: MutableState<Boolean>,
onToggle: (Boolean) -> Unit) {
TextButton(
modifier = modifier,
onClick = {
toggleState.value = !toggleState.value
onToggle(toggleState.value)
})
{ Text(text = if (toggleState.value) "Stop" else "Start") }
}
One thing I didn't like the code is val toggleState = remember { ... }.
I prefer val toggleState by remember {...}
However, if I do that, as shown below, I cannot pass the toggleState over to ToggleButton, as ToggleButton wanted mutableState<Boolean> and not Boolean. Hence it will error out.
#Composable
fun Greeting() {
Column {
val toggleState by remember {
mutableStateOf(false)
}
AnimatedVisibility(visible = toggleState) {
Text(text = "Edit", fontSize = 64.sp)
}
ToggleButton(toggleState = toggleState) {} // Here will have error
}
}
#Composable
fun ToggleButton(modifier: Modifier = Modifier,
toggleState: MutableState<Boolean>,
onToggle: (Boolean) -> Unit) {
TextButton(
modifier = modifier,
onClick = {
toggleState.value = !toggleState.value
onToggle(toggleState.value)
})
{ Text(text = if (toggleState.value) "Stop" else "Start") }
}
How can I fix the above error while still using val toggleState by remember {...}?
State hoisting in Compose is a pattern of moving state to a composable's caller to make a composable stateless. The general pattern for state hoisting in Jetpack Compose is to replace the state variable with two parameters:
value: T: the current value to display
onValueChange: (T) -> Unit: an event that requests the value to change, where T is the proposed new value
You can do something like
// stateless composable is responsible
#Composable
fun ToggleButton(modifier: Modifier = Modifier,
toggle: Boolean,
onToggleChange: () -> Unit) {
TextButton(
onClick = onToggleChange,
modifier = modifier
)
{ Text(text = if (toggle) "Stop" else "Start") }
}
and
#Composable
fun Greeting() {
var toggleState by remember { mutableStateOf(false) }
AnimatedVisibility(visible = toggleState) {
Text(text = "Edit", fontSize = 64.sp)
}
ToggleButton(toggle = toggleState,
onToggleChange = { toggleState = !toggleState }
)
}
You can also add the same stateful composable which is only responsible for holding internal state:
#Composable
fun ToggleButton(modifier: Modifier = Modifier) {
var toggleState by remember { mutableStateOf(false) }
ToggleButton(modifier,
toggleState,
onToggleChange = {
toggleState = !toggleState
},
)
}

How to update composable state from outside?

For example it working with state{} within composable function.
#Composable
fun CounterButton(text: String) {
val count = state { 0 }
Button(
modifier = Modifier.padding(8.dp),
onClick = { count.value++ }) {
Text(text = "$text ${count.value}")
}
}
But how to update value outside of composable function?
I found a mutableStateOf() function and MutableState interface which can be created outside composable function, but no effect after value update.
Not working example of my attempts:
private val timerState = mutableStateOf("no value", StructurallyEqual)
#Composable
private fun TimerUi() {
Button(onClick = presenter::onTimerButtonClick) {
Text(text = "Timer Button")
}
Text(text = timerState.value)
}
override fun updateTime(time: String) {
timerState.value = time
}
Compose version is 0.1.0-dev14
You can do it by exposing the state through a parameter of the controlled composable function and instantiate it externally from the controlling composable.
Instead of:
#Composable
fun CounterButton(text: String) {
val count = remember { mutableStateOf(0) }
//....
}
You can do something like:
#Composable
fun screen() {
val counter = remember { mutableStateOf(0) }
Column {
CounterButton(
text = "Click count:",
count = counter.value,
updateCount = { newCount ->
counter.value = newCount
}
)
}
}
#Composable
fun CounterButton(text: String, count: Int, updateCount: (Int) -> Unit) {
Button(onClick = { updateCount(count+1) }) {
Text(text = "$text ${count}")
}
}
In this way you can make internal state controllable by the function that called it.
It is called state hoisting.

Categories

Resources