1

So I have Two ViewModels in my Calculator App in which I all reference in my Compose NavGraph so I can use the same ViewModel instance. I set a Boolean State(historyCheck) in the first ViewModel and I set it too true to "Clear" the History I set which is a history of Calculations I am trying to retrieve from both ViewModels. The issue now is that the Boolean State "strCalcViewModel.historyCheck" changes before the variable above it get's assigned which then makes the 'if' statement I setup fail which in turns makes the whole Implementation also fail as it is always set to false.

This is my code Below... My Compose NavGraph.

@Composable
fun ComposeNavigation(
    navController: NavHostController,
) {
    /**
     *  Here We declare an Instance of our Two ViewModels, their states and History States. This is because we don't want to have the same States for the two Screens.
     */
    val strCalcViewModel = viewModel<CalculatorViewModel>()
    val sciCalcViewModel = viewModel<ScientificCalculatorViewModel>()

    val strCalcState = strCalcViewModel.strState
    val sciCalcState = sciCalcViewModel.sciState

    val strHistoryState = strCalcViewModel.historyState
    val sciHistoryState = sciCalcViewModel.historyState

    // This holds our current available 'HistoryState' based on where the Calculation was performed(Screens) by the USER.
    var currHistory by remember { mutableStateOf(CalculatorHistoryState()) }
    if(strCalcViewModel.historyCheck) {

        currHistory = strHistoryState 
        strCalcViewModel.historyCheck = false // this gets assigned before the 'currHistory' variable above thereBy making the the if to always be false

    } else {
        currHistory = sciHistoryState
    }

    NavHost(
        navController = navController,
        startDestination = "main_screen",
    ) {
    
        composable("main_screen") {
            MainScreen(
                navController = navController, state = strCalcState, viewModel = strCalcViewModel
            )
        }

        composable("first_screen") {
            FirstScreen(
                navController = navController, state = sciCalcState, viewModel = sciCalcViewModel
            )
        }

        composable("second_screen") {
            SecondScreen(
               navController = navController, historyState =  currHistory, viewModel = strCalcViewModel
            )
       }
   }
}

Then my ViewModel

private const val TAG = "CalculatorViewModel"

class CalculatorViewModel : ViewModel() {

    var strState by mutableStateOf(CalculatorState())
        // This makes our state accessible by outside classes but still readable
        private set

    var historyState by mutableStateOf(CalculatorHistoryState())
        private set

    private var leftBracket by mutableStateOf(true)
    private var check = 0

    var checkState by mutableStateOf(false)

    var historyCheck by mutableStateOf(false)

    // Function to Register our Click events
    fun onAction(action : CalculatorAction) {
        when(action) {
            is CalculatorAction.Number -> enterNumber(action.number)
            is CalculatorAction.Decimal -> enterDecimal()
            is CalculatorAction.Clear -> {
                strState = CalculatorState()
                check = 0
            }
            is CalculatorAction.ClearHistory -> checkState = true
            is CalculatorAction.Operation -> enterStandardOperations(action.operation)
            is CalculatorAction.Calculate -> performStandardCalculations()
            is CalculatorAction.Delete -> performDeletion()
            is CalculatorAction.Brackets -> enterBrackets()
        }
    }

    // We are Basically making the click events possible by modifying the 'state'
    private fun performStandardCalculations() {
        val primaryStateChar = strState.primaryTextState.last()
        val primaryState = strState.primaryTextState
        val secondaryState = strState.secondaryTextState

        if (!(primaryStateChar == '(' || primaryStateChar == '%')) {

            strState = strState.copy(
                primaryTextState = secondaryState
            )
           strState = strState.copy(secondaryTextState = "")

            // Below, we store our Calculated Values in the History Screen after it has been Calculated by the USER.
            historyState = historyState.copy(
                historySecondaryState = secondaryState
            )

            historyState = historyState.copy(
                historyPrimaryState = primaryState
            )

            historyCheck = true // this is where I assign it to true when I complete my Calculations and pass it to the history State
        } else {
            strState = strState.copy(
                secondaryTextState = "Format error"
            )

            strState = strState.copy(
                color = ferrari
            )
        }

    }
}
Daniel Iroka
  • 103
  • 8

2 Answers2

1

You're checking the if condition and assigning a new value to your viewModel variable in the Compose function, it's not correct! you should use side-effects

    LaunchedEffect(strCalcViewModel.historyCheck) {
     if(strCalcViewModel.historyCheck) {

        currHistory = strHistoryState 
        strCalcViewModel.historyCheck = false 
  
     } else {
        currHistory = sciHistoryState
    }}

Whenever there is a new change in strCalcViewModel.historyCheck this block will run you can check out here for more info Side-effects in Compose

Sadegh.t
  • 205
  • 3
  • 10
  • So basically, if I am trying to modify a state outside the scope of a composable function or an "Effect", a composable function that doesn't emit UI(Which is my NavGraph in this case), I should use Side-effects?? – Daniel Iroka Dec 26 '22 at 20:24
  • By the way, I tried this and it didn't work though. It still keeps showing me 'false' and the variable still doesn't get assigned. – Daniel Iroka Dec 26 '22 at 20:26
  • When you change the state inside a compose function you have no clue when it's going to run, every compose function can recompose anytime. By using an effect handler you make sure if this state changes this block will run. And using two ViewModel is not a good idea because you're not following the single source of truth principle anymore. – Sadegh.t Dec 28 '22 at 15:25
  • What is this single source of Truth Principle that you speak of? Because the reason for using Two different ViewModels is because I have two different composables that almost do the same things but must not use the same 'State'. if I use a single ViewModel, whatever I do in that one ViewModel will reflect on my other Screen which is not supposed to happen even if I use two states in that ViewModel but if you have any better idea, I can look into it. – Daniel Iroka Dec 28 '22 at 18:29
  • By the way, using a side-effect worked for me. Thank you very much and I've upvoted your answer but I wrote in a different way and used a different Implementation which I will post now. – Daniel Iroka Dec 28 '22 at 18:31
  • Happy to help. Now you're observing changes from the strCalcState it looks good. Also, You can use only one ViewModel and then have two different states. One state for the first Compose and another state for the second one in this case you don't have to implement two ViewModels! – Sadegh.t Dec 29 '22 at 09:47
  • And you can use "combine" to observe all changes from these two states! – Sadegh.t Dec 29 '22 at 09:56
  • My Two ViewModels are Identical. Which means that they almost do the same thing. If I am use two States in a single ViewModel and try to avoid any conflict that means I will have to create duplicate code in almost everything for each of the States and will make the whole code BoilerPlate. Is that Okay or is there another way?? because this was why I avoided that method and said I might as well just create Two separate ViewModels. These are the two ViewModels for reference [ViewModels](https://github.com/daniel-iroka/Calculator/tree/main/app/src/main/java/com/android/calculator/viewmodels) – Daniel Iroka Dec 29 '22 at 21:09
  • Maybe you can make a data class and define your attributes there. In this case, you can follow the MVI design pattern. You will have one ViewModel with one data class that represents all your variables and then you can use this data class as the state in your two views! I've made a sample app with this architecture you can check it to get some idea! https://github.com/Sadegh8/Earthquake-app – Sadegh.t Dec 30 '22 at 08:23
  • That is actually how my project is. I in fact have two data classes for each of this Composables that I use as 'States' in my ViewModel. The only issue is differentiating what should happen for each of this Views without any conflicts as they almost use the same Implementation and I will not be able to avoid any boilerplate code. But sure I will check out your project as well. – Daniel Iroka Dec 30 '22 at 19:38
  • This is my project as well for reference https://github.com/daniel-iroka/Calculator – Daniel Iroka Dec 30 '22 at 19:45
1

Based on Sadegh.t's Answer I got it working but didn't write it the exact same way and used a different Implementation which I will post now.

I Still used a side-effect but instead of checking for a change in the "historyCheck", I checked for a change in the 'State' itself and also instead of using a Boolean variable, I used the State itself for the basis of the Condition. So here is my answer based on Sadegh.t's original answer.

var currHistory by remember { mutableStateOf(CalculatorHistoryState()) }
LaunchedEffect(key1 = strCalcState) {
    if(strCalcState.secondaryTextState.isEmpty()) {
        currHistory = strHistoryState
    }
}

LaunchedEffect(key1 = sciCalcState) {
    if(sciCalcState.secondaryTextState.isEmpty()) {
        currHistory = sciHistoryState
    }
}
Daniel Iroka
  • 103
  • 8