androidkotlinandroid-jetpack-composeviewmodelcompose-recomposition

Preserve the data of a ViewModel


For now, I have two view components: CharacterScreen and CharacterSkillScreen. I can navigate to CharacterSkillScreen from CharacterScreen.

On the CharacterSkillScreen view, as the name suggests, I can add skills (ItemSkillView). There are no issues with functionality at this level.

The problem arises when for some reason, I want to go back to the previous view by clicking the keyboard's back button and then decide to return to CharacterSkillScreen. Unfortunately, any possible added skills have disappeared. The page has returned to 0.

I tried quite a few things without really understanding what I was doing (passing the skill list as a navigation parameter using serialization (I didn't get it), using Bundle instead of serialization...)

The latest test to date: rememberSaveable. As you can imagine, no change. The problem persists.

I am providing you with the code hoping that you can help me.

THE CODE:

class SkillViewModel() : ViewModel() {

    private val _uiState = MutableStateFlow(SkillUiState())
    val uiState: StateFlow<SkillUiState> = _uiState.asStateFlow()

    // Mise à jour de l'état de l'interface utilisateur : Compétences
    private fun updateSkillState(update: (SkillUiState) -> SkillUiState) {
        _uiState.value = update(_uiState.value)
    }
    fun updateSkillTitle(id: String, text: String) {
        updateSkillState { state ->
            val updateSkillItem = state.skillItem.map {
                if (it.id == id) {
                    it.copy(title = text)
                } else {
                    it
                }
            }
            state.copy(skillItem = updateSkillItem)
        }
    }
    fun updateSkillDescribe(id: String, describe: String) {
        updateSkillState { state ->
            val updatedSkillItem = state.skillItem.map {
                if (it.id == id) {
                    it.copy(describe = describe)
                } else {
                    it
                }
            }
            state.copy(skillItem = updatedSkillItem)
        }
    }
    fun updateFirstCaracSelected(id: String, firstCarac: String) {
        updateSkillState { state ->
            val updatedSkillItem = state.skillItem.map {
                if (it.id == id) {
                    it.copy(firstCarac = firstCarac)
                } else {
                    it
                }
            }
            state.copy(skillItem = updatedSkillItem)
        }
    }
    fun updateSecondCaracSelected(id: String, secondCarac: String) {
        updateSkillState { state ->
            val updatedSkillItem = state.skillItem.map {
                if (it.id == id) {
                    it.copy(secondCarac = secondCarac)
                } else {
                    it
                }
            }
            state.copy(skillItem = updatedSkillItem)
        }
    }

    fun updateFirstParticularity(id: String, firstParticularity: String) {
        updateSkillState { state ->
            val updatedSkillItem = state.skillItem.map {
                if (it.id == id) {
                    it.copy(firstParticularity = firstParticularity)
                } else {
                    it
                }
            }
            state.copy(skillItem = updatedSkillItem)
        }
    }

    fun updateSecondParticularity(id: String, secondParticularity: String) {
        updateSkillState { state ->
            val updatedSkillItem = state.skillItem.map {
                if (it.id == id) {
                    it.copy(secondParticularity = secondParticularity)
                } else {
                    it
                }
            }
            state.copy(skillItem = updatedSkillItem)
        }
    }

    //Ajouter une compétence
    fun addSkillItem() {
        val id = UUID.randomUUID().toString()
        val newSkill = SkillModel(
            id,
            "",
            "",
            "",
            "",
            "",
            "")

        updateSkillState { state ->
            state.copy(skillItem = (state.skillItem + newSkill))
        }
    }

    //Calcul des points de compétences
    fun skillPointCalculated(firstCarac: String, secondCarac: String) : String {
        val points = "${(firstCarac.toIntOrNull() ?: 0) + (secondCarac.toIntOrNull() ?: 0)}"
        updateSkillState { it.copy(skillPoints = points) }

        return points
    }
}
@Composable
fun CharacterSkillScreen(
    arguments: Bundle? = null,
    skillViewModel: SkillViewModel = viewModel()
) {

    val rememberSkillList = rememberSaveable {
        mutableStateOf(emptyList<SkillModel>())
    }
    
    val skillUiState by skillViewModel.uiState.collectAsState()
    
    LaunchedEffect(key1 = skillUiState.skillItem) {
        rememberSkillList.value = skillUiState.skillItem
    }

    Column {

        val strengthValue = arguments?.getInt("strenght") ?: 0
        val dexterityValue = arguments?.getInt("dexterity") ?: 0
        val constituionValue = arguments?.getInt("constitution") ?: 0
        val intelligenceValue = arguments?.getInt("intelligence") ?: 0
        val wisdomValue = arguments?.getInt("wisdom") ?: 0
        val charismValue = arguments?.getInt("charism") ?: 0

        val statsFieldValue = mapOf(
            "FOR" to strengthValue,
            "DEX" to dexterityValue,
            "CON" to constituionValue,
            "INT" to intelligenceValue,
            "SAG" to wisdomValue,
            "CHA" to charismValue
        )

        Row(modifier = Modifier.padding(20.dp)) {

            statsFieldValue.forEach { (label, value) ->
                StatsField(
                    label = label,
                    value = value.toString(),
                    onValueChange = { },
                    modifier = Modifier.weight(1f)
                )
            }
        }
        Column(
            modifier = Modifier
                .verticalScroll(rememberScrollState())
                .fillMaxWidth(),
            horizontalAlignment = Alignment.CenterHorizontally
        ){
            rememberSkillList.value.forEach{ skillModel ->
                ItemSkillView(
                    skillModel = skillModel,
                    statsValue = statsFieldValue)
            }
            Log.d("Skills", "${rememberSkillList.value}")
            Divider(thickness = 1.dp)
            Button(
                onClick = {
                    skillViewModel.addSkillItem()
              },
                modifier = Modifier.padding(top = 10.dp)
            ) {
                Text(text = "Ajouter une compétence")
            }

        }
    }
}

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ItemSkillView(
    skillModel: SkillModel,
    statsValue: Map<String, Int>,
    skillViewModel: SkillViewModel = viewModel()
) {
    var isExpanded by remember { mutableStateOf(false) }

    val firstCarac = skillModel.firstCarac
    val secondCarac = skillModel.secondCarac
    val firstCaracValue = statsValue[firstCarac].toString()
    val secondCaracValue = statsValue[secondCarac].toString()
    val skillPoint = skillViewModel.skillPointCalculated(firstCaracValue, secondCaracValue)

    Column(
        modifier = Modifier
            .fillMaxWidth()
            .clickable { isExpanded = !isExpanded }
            .animateContentSize()
    ) {
        ConstraintLayout(modifier = Modifier.fillMaxWidth()) {

            val (icon, skill, resultSkill) = createRefs()

            Icon(
                painter = painterResource(id = R.drawable.expand_more),
                contentDescription = "Expanded button",
                tint = Color.Black,
                modifier = Modifier
                    .size(40.dp)
                    .constrainAs(icon) {
                        start.linkTo(parent.start, margin = 10.dp)
                        top.linkTo(skill.top)
                        bottom.linkTo(skill.bottom)
                    }
            )

            val title = skillModel.title
            TextField(
                value = title,
                onValueChange = {
                    skillViewModel.updateSkillTitle(skillModel.id, it)
                },
                label = { Text(text = "NOM DE LA COMPÉTENCE ") },
                colors = TextFieldDefaults.outlinedTextFieldColors(
                    focusedBorderColor = Color.Transparent,
                    unfocusedBorderColor = Color.Transparent
                ),
                modifier = Modifier.constrainAs(skill) {
                    start.linkTo(icon.end)
                    end.linkTo(resultSkill.start)
                }
            )
            Text(
                text = skillPoint,
                fontSize = 16.sp,
                textAlign = TextAlign.Center,
                modifier = Modifier
                    .border(1.dp, Color.Black)
                    .padding(top = 3.dp, bottom = 3.dp)
                    .constrainAs(resultSkill) {
                        start.linkTo(skill.end, margin = 18.dp)
                        end.linkTo(parent.end, margin = 18.dp)
                        top.linkTo(skill.top)
                        bottom.linkTo(skill.bottom)
                        width = Dimension.fillToConstraints
                    }
            )
            Divider(thickness = 1.dp)
        }
        if(isExpanded) {
            ExposedDropDownMenuSkill(skillModel = skillModel)
        }
    }
}

Solution

  • This can be solved in couple of ways :

    Approach 1 Shared ViewModel - Create a view model in your first fragment and share it with the second fragment. This way your view model will be destroyed only when first fragment is destroyed. You can have separate personal view models for each fragment for other logic. Here, first and second fragment are such that you navigate from first fragment to second fragment.

    import androidx.lifecycle.ViewModel
    
    class SharedViewModel : ViewModel() {
        val quantity = MutableLiveData<Int>(0)
    }
    
    // firstFragment.kt
    
    // shared view model
    private val sharedViewModel: OrderViewModel by activityViewModels()
    
    // personal view model of first fragment
    private val viewModel: ViewModel by activityViewModels()
    
    // secondFragment.kt
    
    // shared view model
    private val sharedViewModel: OrderViewModel by activityViewModels()
    
    // personal view model of second fragment
    private val viewModel: ViewModel by activityViewModels()
    

    Pros :

    • Easy to implement

    Cons :

    • Your logic view divided into two separate viewModels
    • Your data will be preserved back to first fragment only, if you go back from first fragment, the data will be lost.

    Approach 2 : Use a database using ROOM library. Since this approach has extra steps to setup database and entities. You can go through documentation here. Once you have database, update the values in database in secondFragment, and then it will preserved no matter what. When coming back to secondFragment, see if there is data already in database.

    Pros :

    • Your data will be preserved no matter what.
    • Easily scalable for future use cases, if you want to persist data permanently for some use case.

    Cons :

    • You will have to delete the data in database, if you want to use it for single app lifecycle. Otherwise, every time you open secondFragment, the data from database will be loaded, even if you want to start fresh.