1
votes

I am using Android Jetpack's Compose and have been trying to figure out how to save state for orientation changes.

My train of thought was making a class a ViewModel. As that generally worked when I would work with Android's traditional API.

I have used remember {} and mutableState {} to update the UI when information has been changed. Please validate if my understanding is correct...

remember = Saves the variable and allows access via .value, this allows values to be cache. But its main use is to not reassign the variable on changes.

mutableState = Updates the variable when something is changed.

Many blog posts say to use @Model, however, the import gives errors when trying that method. So, I added a : ViewModel()

However, I believe my remember {} is preventing this from working as intended?

Can I get a point in the right direction?

@Composable
fun DefaultFlashCard() {

    val flashCards = remember { mutableStateOf(FlashCards())}
    

    Spacer(modifier = Modifier.height(30.dp))

    MaterialTheme {


        val typography = MaterialTheme.typography
        var question = remember { mutableStateOf(flashCards.value.currentFlashCards.question) }



        Column(modifier = Modifier.padding(30.dp).then(Modifier.fillMaxWidth())
                .then(Modifier.wrapContentSize(Alignment.Center))
                .clip(shape = RoundedCornerShape(16.dp))) {
            Box(modifier = Modifier.preferredSize(350.dp)
                    .border(width = 4.dp,
                            color = Gray,
                            shape = RoundedCornerShape(16.dp))
                    .clickable(
                            onClick = {
                                question.value = flashCards.value.currentFlashCards.answer })
                    .gravity(align = Alignment.CenterHorizontally),
                    shape = RoundedCornerShape(2.dp),
                    backgroundColor = DarkGray,
                    gravity = Alignment.Center) {
                Text("${question.value}",
                        style = typography.h4, textAlign = TextAlign.Center, color = White
                )
            }
        }

        Column(modifier = Modifier.padding(16.dp),
                horizontalGravity = Alignment.CenterHorizontally) {

            Text("Flash Card application",
                    style = typography.h6,
                    color = Black)

            Text("The following is a demonstration of using " +
                    "Android Compose to create a Flash Card",
                    style = typography.body2,
                    color = Black,
                    textAlign = TextAlign.Center)

            Spacer(modifier = Modifier.height(30.dp))
            Button(onClick = {
                flashCards.value.incrementQuestion();
                question.value = flashCards.value.currentFlashCards.question },
                    shape = RoundedCornerShape(10.dp),
                    content = { Text("Next Card") },
                    backgroundColor = Cyan)
        }
    }
}


data class Question(val question: String, val answer: String) {
}


class FlashCards: ViewModel() {

    var flashCards = mutableStateOf( listOf(
            Question("How many Bananas should go in a Smoothie?", "3 Bananas"),
            Question("How many Eggs does it take to make an Omellete?", "8 Eggs"),
            Question("How do you say Hello in Japenese?", "Konichiwa"),
            Question("What is Korea's currency?", "Won")
    ))

    var currentQuestion = 0

    val currentFlashCards
        get() = flashCards.value[currentQuestion]

    fun incrementQuestion() {
        if (currentQuestion + 1 >= flashCards.value.size) currentQuestion = 0 else currentQuestion++
    }
}

2

2 Answers

7
votes

There is another approach to handle config changes in Compose, it is rememberSaveable. As docs says:

While remember helps you retain state across recompositions, the state is not retained across configuration changes. For this, you must use rememberSaveable. rememberSaveable automatically saves any value that can be saved in a Bundle. For other values, you can pass in a custom saver object.

It seems that Mohammad's solution is more robust, but this one seems simpler.

2
votes

Same as before, you can use Architecture Component ViewModel to survive configuration changes.

You should initialize your ViewModel in Activity/Fragment and then pass it to Composable functions.

class UserDetailFragment : Fragment() {

    private val viewModel: UserDetailViewModel by viewModels()

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {

        return ComposeView(context = requireContext()).apply {
            setContent {
                AppTheme {
                    UserDetailScreen(
                        viewModel = viewModel
                    )
                }
            }
        }
    }
}

Then your ViewModel should expose the ViewState by something like LiveData or Flow

UserDetailViewModel:

class UserDetailViewModel : ViewModel() {
    private val _userData = MutableLiveData<UserDetailViewState>()
    val userData: LiveData<UserDetailViewState> = _userData


    // or

    private val _state = MutableStateFlow<UserDetailViewState>()
    val state: StateFlow<UserDetailViewState>
        get() = _state

}

Now you can observe this state in your composable function:

@Composable
fun UserDetailScreen(
    viewModel:UserDetailViewModel
) {
    val state by viewModel.userData.observeAsState()
    // or
    val viewState by viewModel.state.collectAsState()

}