Choosing between DexGuard and ProGuard in Android development
Discover the nuances between DexGuard and ProGuard – two powerful libraries that can fortify your app against security threats.
Financial services
Expertise in core banking, BaaS integrations, payments, and GenAI-enhanced financial solutions.
Healthcare
People-centric healthcare design and solutions, from virtual care, integrations, to smart devices.
Insurance
Modern solutions including self-service, on-demand, and algorithm-driven personalization.
We’re purposefully shaping the digital future across a range of industries.
Discover some of our specific industry services.
Discover moreJune 7, 2023
Check out some tips on how to save the UI state during Android development.
This year’s Google I/O event was an exciting showcase of new details and announcements that are crucial for Android developers to stay informed about.
To assist the developer community, our App Solutions Studio has reviewed the various talks, and we are thrilled to share a series of blog posts highlighting the most captivating updates.
Our first blog post revolves around the topic of managing UI state in apps. We delve into the best practices for avoiding UI state loss and implementing effective strategies for saving the UI state. For a comprehensive understanding, here is the link to the full video of the talk on YouTube.
There are several ways in which this can happen.
class ChatBubbleView(context: Context, ...) : View(context, ...) {
private var isExpanded = false
override fun onSaveInstanceState(): Parcelable {
super.onSaveInstanceState()
return bundleOf(IS_EXPANDED to isExpanded)
}
override fun onRestoreInstanceState(state: Parcelable) {
isExpanded = (state as Bundle).getBoolean(IS_EXPANDED)
super.onRestoreInstanceState(null)
}
companion object {
private const val IS_EXPANDED = "is_expanded"
}
}
@Composable
fun ChatBubble(
message: Message
) {
var showDetails by rememberSaveable { mutableStateOf(false) }
ClickableText(
text = AnnotatedString(message.content),
onClick = { showDetails = !showDetails }
)
if (showDetails) {
Text(message.timestamp)
}
}
class ConversationViewModel (
savedStateHandle: SavedStateHandle
) : ViewModel() {
var message by savedStateHandle.saveable(stateSaver = TextFieldValue.Saver) {
mutableStateOf(TextFieldValue(""))
}
private set
fun update(newMessage: TextFieldValue) {
message = newMessage
}
fun send() { /* Send current message to the data layer */ }
/*...*/
}
For more information on ViewModel and SavedStateHandle, refer to the Android Developer documentation.
First use case: Contribute to saved state from your own classes
Imagine you have a NewsSearchState with a mutable state searchInput:
class NewsSearchState(
private val newsRepository: NewsRepository,
initialSearchInput: String
) {
var searchInput = mutableStateOf(TextFieldValue(initialSearchInput))
private set
}
You can create a new composable rememberNewsSearchState which is kind of a wrapper that uses rememberSavable under the hood, but with a custom saver.
@Composable
fun rememberNewsSearchState(
newsRepository: NewsRepository,
initialSearchInput: String = ""
) {
return rememberSaveable(
newsRepository, initialSearchInput,
saver = // TODO: Custom Saver
) {
NewsSearchState(newsRepository, initialSearchInput)
}
}
Here is the NewsSearchState modified with the new saver function. Note that it creates a Saver object with a save and restore properties.
class NewsSearchState(
private val newsRepository: NewsRepository,
initialSearchInput: String
) {
var searchInput = mutableStateOf(TextFieldvalue(initialSearchInput))
private set
companion object {
fun saver (newsRepository: NewsRepository): Saver<NewsSearchState, *> = Saver(
save = {
with(TextFieldValue.Saver) { save(it.searchInput)}
},
restore = {
TextFieldValue.Saver.restore(it)?.let { searchInput ->
NewsSearchState(newsRepository, searchInput)
}
}
)
}
}
And the usage is the following.
@Composable
fun rememberNewsSearchState(
newsRepository: NewsRepository,
initialSearchInput: String = ""
) {
return rememberSaveable(
newsRepository, initialSearchInput,
saver = NewsSearchState.saver(newsRepository)
) {
NewsSearchState(newsRepository, initialSearchInput)
}
}
Here we have the same situation. Suppose that we have a NewsSearchState.
class NewsSearchState(
private val newsRepository: NewsRepository,
private val initialSearchInput: String,
) {
private var currentQuery: String = initialSearchInput
// ... Rest of business logic ...
}
As we are not in a ViewModel we cannot use SaveStateHandler and as we are not in a fragment or in an Activity we cannot use onSaveinstanceState/onRestoreInstancesState. We have to make use of SavedStateRegistry. By overriding the saveState method in our class would leave something like:
class NewsSearchState(
private val newsRepository: NewsRepository,
private val initialSearchInput: String,
) : SavedStateRegistry.SavedStateProvider {
private var currentQuery: String = initialSearchInput
// ... Rest of business logic ...
override fun saveState(): Bundle {
return bundleOf(QUERY to currentQuery)
}
companion object {
private const val QUERY = "current_query"
private const val PROVIDER = "news_search_state"
}
}
Now we have to connect this with a registry owner that we pass a parameter in the constructor.
class NewsSearchState(
private val newsRepository: NewsRepository,
private val initialSearchInput: String,
registryOwner: SavedStateRegistryOwner
) : SavedStateRegistry.SavedStateProvider {
private var currentQuery: String = initialSearchInput
init {
registryOwner.lifecycle.addObserver(LifecycleEventObserver { _, event ->
if (event == Lifecycle.Event.ON_CREATE) {
val registry = registryOwner.savedStateRegistry
if (registry.getSavedStateProvider(PROVIDER) == null ) {
registry.registerSavedStateProvider(PROVIDER, this)
}
val savedState = registry.consumeRestoredStateForKey(PROVIDER)
currentQuery = savedState?.getString(QUERY) ?: initialSearchInput
}
})
}
}
The usage of this State in a fragment is as simple as:
class NewsFragment : Fragment() {
private var newsSearchState = NewsSearchState(this)
...
}
Note of caution: We took these images from the video, but this example is missing the passing of the repository and search input as parameters. The search input can also be handle as a property, for more information: Android Developer documentation.
First remember the Composable lifecycle. The composable enters the composition, can recompose 0 or more times and finally leave the composition.
It means that when the UI enters the composition the rememberSavable values are stored in Saved State. If a configuration change happens and the activity is recreated, the old composition is destroyed, a new composition is created and rememberSavable values are restored.
Note that rememberSavableValues are restored, but values using the remember API won’t. They are lost after activity is recreated.
Lastly when the composable leaves the composition the rememberSavable values are removed from Saved State.
This is the default behavior. But how can we modify this? This can also be done with SavableStateRegistry (remember that we saw it when we talked about saving state in View system).
Take a look at the current existing rememberSavable composable.
// androidx/compose/runtime/saveable/RememberSaveable.kt
@Composable
fun <T : Any> rememberSaveable(
vararg inputs: Any?,
saver: Saver<T, out Any> = autoSaver(),
key: String? = null,
init: () -> T
): T {
// ...
val registry = LocalSaveableStateRegistry.current
val value = remember (*inputs) {
val restored = registry?.consumeRestored(finalKey)?.let {
saver.restore(it)
}
restored ?: init()
}
// ...
}
It accesses the current SavableState registry and is initialized by calling consumeRestored from it. If there was no value previously stored it is initialized with the init LaMDA. So, if we define a new SavableStateRegistry we can control for how long rememberSavable stores their values. This is what Jetpack’s compose navigation library does. Check out the following part of the video to see the explanation.
In conclusion, when developing Android apps, it’s crucial to understand how the UI state can be lost, and implement effective strategies to save and restore it. By following best practices, developers can ensure a much improved user experience and prevent data loss.
Mobile Developer
Receive regular updates about our latest work
Discover the nuances between DexGuard and ProGuard – two powerful libraries that can fortify your app against security threats.
Check out what is new in Kotlin, a widely used programming language for Android.
What are the differences between Jetpack Compose and XML? Read more to find out!
Receive regular updates about our latest work
Get in touch with our experts to review your idea or product, and discuss options for the best approach
Get in touchProduct Design Solutions
Artificial Intelligence Services
Healthcare Solutions
Data
App Solutions
Platform engineering
Cybersecurity
SRE & Cloud Services
Quality Assurance
Blockchain
Firmware & IoT Development
Product Management
Financial Services Technology
Insurance