0

I am trying to understand SavedstateHandle and how I can create 1 viewModel that can store multiple Chips using savedstatehandle. At the moment, whenever the user launches an onClickEvent on one of the chips, all the remaining chips get activated as well. How can I create so that each chip has its seperate on click event but they all are saved in 1 viewModel with a savedStateHandle?

This is the current problem. Both chips activate when the onClick event is triggered on 1 of them: https://gyazo.com/c302b4ccb95fe150096ffc64ba5b3ab3

Appreciate the feedback!

Chips:

@Composable
fun SimpleChip(
    text: String,
    isSelected: Boolean,
    selectedColor: Color = Color.DarkGray,
    onChecked: (Boolean) -> Unit,
) {
    Surface(
        onClick = { onChecked(!isSelected) },
        modifier = Modifier.padding(4.dp)
            .wrapContentSize()
            .border(
                width = 1.dp,
                color = if (isSelected) selectedColor else Color.LightGray,
                shape = RoundedCornerShape(20.dp)
            )
        ,
        shape = RoundedCornerShape(16.dp),
        color = if (isSelected) LightGreen else Color.White,
    ) {
        Row(
            modifier = Modifier.padding(8.dp),
            horizontalArrangement = Arrangement.spacedBy(4.dp),
            verticalAlignment = Alignment.CenterVertically,
        ) {
            Text(
                text = text,
                fontSize = 18.sp,
                fontWeight = FontWeight.Bold,
                color = if (isSelected) Color.White else Color.Black,
            )
        }
    }
}

SavedStateHandled:

class SavedStateViewModel(private val state: SavedStateHandle) : ViewModel() {

    val checkedState = state.getStateFlow(key = CHECKED_STATE_KEY, initialValue = false)

    companion object {
        private const val CHECKED_STATE_KEY = "checkedState"

    }

    fun onCheckedChange(isSelected: Boolean) =
        state.set(key = CHECKED_STATE_KEY, value = isSelected)
    

}

Where the SimpleChip is called:


@SuppressLint("UnusedMaterialScaffoldPaddingParameter", "UnusedMaterial3ScaffoldPaddingParameter")
@Composable
fun AllAnimals(
    navController: NavController,
    viewModel: SavedStateViewModel = viewModel(),
) {

    val parentSavedState by viewModel.checkedState.collectAsState()

    val parent2 by viewModel.checkedState.collectAsState()

    Scaffold(
        topBar = {
            TopAppBarAnimalsActivities(
                title = "Animals",
                buttonIcon = Icons.Filled.ArrowBack,
                onButtonClicked = { navController.popBackStack() }
            )
        }
    ) {

        Column(
            verticalArrangement = Arrangement.Center,
            horizontalAlignment = Alignment.CenterHorizontally
        ) {

            Column(
                modifier = Modifier.fillMaxSize(),
                verticalArrangement = Arrangement.Center,
                horizontalAlignment = Alignment.CenterHorizontally
            ) {


                Text(
                    text = "What is your favourite animal?",
                    fontSize = 25.sp,
                    fontWeight = FontWeight.Bold,
                    fontFamily = FontFamily.Default,
                    color = DarkBlue
                )

                Box(
                    modifier = Modifier
                        .fillMaxWidth(0.8F)
                        .fillMaxHeight(0.93F),

                    ) {
                    Column(
                        modifier = Modifier
                            .fillMaxSize(),
                        verticalArrangement = Arrangement.Top,
                        horizontalAlignment = Alignment.Start

                    ) {

                        Spacer(modifier = Modifier.height(20.dp))
                        Row {

                            Spacer(modifier = Modifier.width(5.dp))
                            SimpleChip(
                                text = "Dog",
                                isSelected = parentSavedState,
                                onChecked = viewModel::onCheckedChange,
                            )

                            Spacer(modifier = Modifier.width(5.dp))
                            HorseChip()

                            Spacer(modifier = Modifier.width(5.dp))
                            GineaPigChip()

                        }

                        Spacer(modifier = Modifier.height(20.dp))
                        Row {

                            Spacer(modifier = Modifier.width(5.dp))
                            RabbitChip()

                            Spacer(modifier = Modifier.width(5.dp))
                            ReptilesChip()

                            Spacer(modifier = Modifier.width(5.dp))
                            CatsSimpleChips() // I added

                        }

                        Spacer(modifier = Modifier.height(20.dp))
                        Row {

                            Spacer(modifier = Modifier.width(5.dp))
                            OtherChip()
                        }

                    }
                }
            }
        }
    }
}

Possible Solution?: But how can I implement this with my SimpleChip?

class MyViewModel(private val savedStateHandle: SavedStateHandle) : ViewModel() {
    // Define your saved key variable
    val savedKey: MutableState<String?> = mutableStateOf(null)

    fun saveKey(key: String) {
        savedKey.value = key
        savedStateHandle.set(KEY_SAVED_KEY, key)
    }

    companion object {
        private const val KEY_SAVED_KEY = "saved_key"
    }
}

@Composable
fun MyComposable() {
    val viewModel: MyViewModel = viewModel()

    // Create a variable to hold the selected key
    val selectedKey by remember { viewModel.savedKey }

    // Example Chip click event
    Chip(
        onClick = {
            val clickedKey = "YourClickedKey"
            viewModel.saveKey(clickedKey)
        }
    ) {
        // Chip content
    }
}


Josef M
  • 412
  • 2
  • 5
  • 20
  • How is the `SimpleChip` used? The issue is likely in that place. – Abhimanyu Jun 25 '23 at 06:03
  • Have updated the thread with how the simpleChip is called as requested! @Abhimanyu – Josef M Jun 25 '23 at 11:02
  • You are storing a boolean in the saved state and updating it when the user clicks the chip, but that is not corresponding to any specific chip. A better approach is to store the id (or any unique attribute) of the selected chip and use it to denote the selected state. – Abhimanyu Jun 25 '23 at 13:58
  • For reference - https://stackoverflow.com/questions/69788366/create-toggle-button-group-in-jetpack-compose-without-radio-buttons/69788600#69788600 – Abhimanyu Jun 25 '23 at 13:58
  • So that would mean that each chips gets its unic Id/ boolean value and then I can call each chip seperate with each value? Just making sure I understood you correctly since the example you linked shows 4 types of chips but only 1 could be choosen at any given time. I want the possibility of all chips within a section, lets say 5 animal chips. The user can then choose whatever it wants between them lets say only 2/5 or 4/5. @Abhimanyu – Josef M Jun 25 '23 at 16:54
  • Then you can use a list to maintain the selected items. Yes, each chip would have a selection state. (Not ID). – Abhimanyu Jun 25 '23 at 16:58
  • Alright I will give it a try! also this would work with a savedstatehandle since the user will navigate away from that screen and the information needs to be saved for later use! – Josef M Jun 25 '23 at 17:01
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/254237/discussion-between-abhimanyu-and-josef-m). – Abhimanyu Jun 25 '23 at 17:02
  • How would I think to solve this ? @Abhimanyu – Josef M Jun 26 '23 at 18:12
  • Is this something to go after? https://stackoverflow.com/questions/61166786/how-to-save-livedata-into-savestatehandle – Josef M Jun 26 '23 at 18:17
  • You have to store only the id in the saved state instance. That doesn't have to be a LiveData. `Int`/`String` should work fine. – Abhimanyu Jun 26 '23 at 23:17
  • "So do I then need to create a public class with a serialized name for each chip?" I didn't get this part in the chat room. This is not required. – Abhimanyu Jun 26 '23 at 23:18
  • hmm so I need to find a way to extract the key from clickevent of the chip and store it in a viewmodel with the savedstatehandle?@Abhimanyu – Josef M Jun 27 '23 at 17:00
  • I have updated with a possible solution but how can I adapt it to my SimpleChip or your method: https://stackoverflow.com/questions/69788366/create-toggle-button-group-in-jetpack-compose-without-radio-buttons/69788600#69788600 @Abhimanyu – Josef M Jun 27 '23 at 18:23

1 Answers1

1

With your current approach you save the entire list's selection state.

val checkedState = state.getStateFlow(key = CHECKED_STATE_KEY, initialValue = false)

In order to have multiple item selection you need to change this to a list:

val checkedState = state.getStateFlow(key = CHECKED_STATE_KEY, initialValue = listOf<Int>())

The list will hold the indexes of the selected item, and you can get/set the value of the checkedState like this:

fun onCheckedChange(selectedIndex: Int) =
    state.set(key = CHECKED_STATE_KEY, value = toggleFromList(checkedState.value, selectedIndex))

private fun toggleFromList(list: List<Int>, selectedIndex: Int): List<Int>{
    val newList = list.toMutableList()
    if (newList.contains(selectedIndex)) {
        newList.remove(selectedIndex)
    }else {
        newList.add(selectedIndex)
    }
    return newList
}

And finally at the list you need to call the chips like this:

SimpleChip(
     text = "Dog",
     isSelected = parentSavedState.contains(0),
     onChecked = { viewModel.onCheckedChange(0) },
)
SimpleChip(
     text = "Horse",
     isSelected = parentSavedState.contains(1),
     onChecked = { viewModel.onCheckedChange(1) },
)

I'd try to use a list or at least a map for the indexes and the animals btw. The approach above seems very error prompt. :)

val animalMap = mapOf<String, Int>("Dog" to 0, "Horse" to 1, "Guinea pig" to 2, "Rabbit" to 3, "Reptile" to 4, "Cats" to 5)
Eliza Camber
  • 1,536
  • 1
  • 13
  • 21
  • hmm that actually makes more sense! And I can as you said use a list or map to make it produce less errors (I assume this is due to the composables being called everytime a user uses that screen compared to just displaying a list/map of items?). – Josef M Jun 28 '23 at 17:32
  • There is still 1 problem though. Whenever I navigate inside a composable and clicks the chip and popbackstack/ go back, it still resets the screen eventhough its inside a savedstatehandle. See video: https://gyazo.com/e5339a830fd4572689c04102c1ed2cd7 – Josef M Jun 28 '23 at 17:35
  • I found your answer here: https://stackoverflow.com/questions/69505729/savedstatehandle-does-not-persist-data#answer-69563397. I'd suggest using something simple like shared prefs – Eliza Camber Jun 29 '23 at 08:52
  • Its an interesting article, will read it a few times so that I understands it correctly but essentially I should be able to sync this with a database like mongoDb to store it even after process death occurs. – Josef M Jun 29 '23 at 17:45
  • Anyway appreciate the feedback and the very detalied explanation to how it works! – Josef M Jun 29 '23 at 17:46