Reputation: 1367
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
Reputation: 1731
There are 2 built-in ways for persisting state in Compose:
remember
: exists to save state in Composable functions between recompositions.
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:
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,
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:
setting android:configChanges
in 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)
Using a combination of ViewModel
+ remeberSaveable
+ data persistence in storage
=======================================================
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
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 userememberSaveable
.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