PandaPlaysAll
PandaPlaysAll

Reputation: 1367

Jetpack Compose saving state on orientation change

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

Upvotes: 17

Views: 22527

Answers (2)

Mohammad Sianaki
Mohammad Sianaki

Reputation: 1731

Updated answer

There are 2 built-in ways for persisting state in Compose:

  1. remember: exists to save state in Composable functions between recompositions.

  2. rememberSaveable: remember only save state across recompositions and doesn't handle configuration changes and process death, so to survive configuration changes and process death you should use rememberSaveable instead.

But there are some problems with rememberSaveable too:

  1. It supports primitive types out of the box, but for more complex data, like data class, you must create a Saver to explain how to persist state into bundle,

  2. rememberSaveable uses Bundle under the hood, so there is a limit of how much data you can persist in it, if data is too large you will face TransactionTooLarge exception.

with above said, below solutions are available:

  1. setting android:configChangesin Manifest to avoid activity recreation in configuration changes. (not useful in process death, also doesn't save you from being recreated in Wallpaper changes in Android 12)

  2. Using a combination of ViewModel + remeberSaveable + data persistence in storage

=======================================================

Old answer

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

}

Upvotes: 17

Matheus Ribeiro Lima
Matheus Ribeiro Lima

Reputation: 251

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.

Upvotes: 25

Related Questions