androidkotlinandroid-jetpack-composeandroid-chipsviewmodel-savedstate

How to create 1 viewmodel with savedstatehandle for multiple chips?


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



Solution

  • 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)