10

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

double-beep
  • 5,031
  • 17
  • 33
  • 41
PandaPlaysAll
  • 927
  • 2
  • 9
  • 15

2 Answers2

18

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.

  • 1
    The problem with this approach is that the Bundle imposes 2 restrictions: the Object to store must be Serializable and it can't be large otherwise we get a TransactionTooLarge exception. I would like to store an object in memory during the lifecycle of the Composable (shorter than the lifecycle of the ViewModel) that survives config change. – Sebas LG Oct 08 '21 at 07:17
  • Remember to add configChanges tag for your activity in manifest too, for rememberSaveable to work. android:configChanges="orientation|screenSize|screenLayout|keyboardHidden" – Ajith M A Apr 23 '23 at 17:53
11

UPDATE:

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

}
sit
  • 360
  • 3
  • 9
Mohammad Sianaki
  • 1,425
  • 12
  • 20
  • 2
    This is definitely the preferred approach to persisting state in a way that survives configuration change. Please mark as correct if you can! : x – Nicolas Mage Sep 30 '21 at 23:01
  • The problem of this approach is that the ViewModel lives at the Activity/Fragment level and `remember` is local to a Composable. If we have Composable UI elements that come and go inside the same Activity I would like to have state that is read and disposed together with the lifecycle of the Composable not with the longer lifecycle of the Activity. Plus surviving config changes – Sebas LG Oct 08 '21 at 07:10
  • Remember to add configChanges tag for your activity in manifest too, for rememberSaveable to work. android:configChanges="orientation|screenSize|screenLayout|keyboardHidden" – Ajith M A Apr 23 '23 at 17:53