This talk explains what problems exist in the context of Android application development, how Jetpack components such as SavedStateHandle help handle that, and how we can combine observable values to expose our state, to be observed only when it is needed.
3. • Save/restore minimal state across process
death
• Execute data loading asynchronously based on
state
– Should be kept across configuration changes
• Minimize „moving parts”
– Mutations should be controlled
– Mutations should be observed
4. • Save/restore minimal state across process
death
• Execute data loading asynchronously based
on state
– Should be kept across configuration changes
• Minimize „moving parts”
– Mutations should be controlled
– Mutations should be observed
5. • Save/restore minimal state across process
death
• Execute data loading asynchronously based on
state
– Should be kept across configuration changes
• Minimize „moving parts”
– Mutations should be controlled
– Mutations should be observed
10. Core App Quality Guidelines
From https://developer.android.com/docs/quality-guidelines/core-app-quality#fn
„When returning to the foreground, the app
must restore the preserved state and any
significant stateful transaction that was
pending, such as changes to editable fields,
game progress, menus, videos, and other
sections of the app.
11. How to induce process death?
• Step 1: put app in background with HOME
• Step 2: press „Terminate application”
• Step 3: restart app from launcher
12. For apps you don’t own
https://play.google.com/store/apps/details?id=me.empirical.android.application.fillme
mory&hl=en
13. Common mistakes
• Assuming that static variables initialized on one
screen (like data loading on a Splash screen) stay
set on the other screen (nope)
• Assuming that if(savedInstanceState == null) {
is true at least once (nope)
• Holding a reference to a Fragment instance
without using findFragmentByTag first (for
example ”ViewPagerAdapter.addFragment()”)
(don’t)
14. How to detect process death?
if (savedInstanceState != null
&& lastNonConfigurationInstance == null) {
}
Or in BaseActivity:
companion object {
var isRestored: Boolean = false
}
if (savedInstanceState !=null) {
if (!isRestored) {
isRestored = true
// here
}
}
16. What needs to be persisted across
process death?
• Navigation state is already managed by the
system on Android out of the box
– Empty ctor + using intent extras / fragment arguments
• Screen state is partially managed by the system
– Views with IDs have their state persisted
– Complex state (f.ex. RecyclerView selection) are not
persisted automatically
– Dynamically added views should be recreatable (!)
17. What SHOULDN’T be persisted?
• Data
– Bundle has a size limit
– Data should be fetched asynchronously, off the UI
thread
• Transient state
– „Loading” state: computed from progress of side-
effect („something is happening”, but is it really?)
18. Example for saving/restoring state
(the „old way”)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (savedInstanceState != null) {
selectedSportId = savedInstanceState.getLong("selectedSportId")
selectedPosition = savedInstanceState.getInt("selectedPosition")
selectedTags.clear()
selectedTags.addAll(
savedInstanceState.getStringArrayList("selectedTags"))
}
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putLong("selectedSportId", selectedSportId)
outState.putInt("selectedPosition", selectedPosition)
outState.putStringArrayList("selectedTags", ArrayList(selectedTags))
}
19. Loading data
• Asynchronous loading should either begin on
initialization, or when observed
• Data can be loaded via a transformation chain
from the observable source that stores the
state – changes trigger new data load
(switchMap, flatMapLatest)
22. • State restoration across process death
– SavedStateRegistry (savedstate)
– SavedStateHandle (viewmodel-savedstate)
• Connecting UI to state model
– Databinding
– Compose (beta)
• Scoping data/state between screens + argument
passing
– Navigation
• Piece together all the things
– Hilt
23. ViewModel
• Stored across configuration changes in a
ViewModelStore (ComponentActivity, Fragment,
and NavBackStackEntry are ViewModelStoreOwner)
• They have their own lifecycle for when the
ViewModelStore is destroyed (onCleared(),
viewModelScope)
• State and asynchronous operations go here
24. ViewModel
• Originally:
val viewModel = ViewModelProvider(
viewModelStoreOwner,
viewModelProviderFactory
).get(MyViewModel::class.java)
• KTX (fragment-ktx):
private val viewModel by viewModels<MyViewModel>(
ownerProducer = { viewModelStoreOwner },
factoryProducer = { viewModelProviderFactory },
)
28. Room
• Exposes observable queries
– if the database is written to, then the queries are
re-evaluated
• Integrations:
– LiveData (ComputableLiveData)
– Flowable/Observable (RxJava 2.x)
– Flow (kotlinx.coroutines)
29. WorkManager
• Schedule and eventually execute background jobs
– Constraint support: execute when network is available, etc
– OneTimeWorkRequest, unique work
– Can be used to download data for offline support
• Worker types:
– ListenableWorker
– CoroutineWorker
– RxWorker
30. SavedState
• SavedStateRegistry allows saving state into a Bundle
– Not commonly used directly, but heavily used by ViewModel-
SavedState (and Jetpack Compose’s
saveableStateHolder.SaveableStateProvider)
• Important methods:
– registerSavedStateProvider(String, SavedStateProvider)
– unregisterSavedStateProvider(String)
– consumeRestoredStateForKey(String): Bundle
31. ViewModel-SavedState
• SavedStateHandle allows saving ViewModel’s state into a
Bundle
– Provides get(), set(), and most importantly
getLiveData() (SavingStateLiveData)
– Custom types can be saved with setSavedStateProvider()
– Initialized with arguments by default
– Originally created by AbstractSavedStateViewModelFactory
32. Databinding
• (Prefer ViewBinding if possible)
• Allows directly binding against state via binding
expressions
– one-way bindings (=”@{}”) vs two-way bindings (=”@={}”)
– supports ObservableField / MutableLiveData /
MutableStateFlow
– Can be useful for forms
• Binding adapters to support custom observable
properties from XML in custom views
33. (Jetpack Compose)
• (Completely new UI framework, beta)
• (Rendering happens by invoking @Composable functions)
• (Changes in function arguments of @Composable functions
are tracked by Compose’s Kotlin compiler plugin)
• (When function arguments change, the functions that depend
on said state get re-evaluated and therefore re-rendered,
„recomposition”)
35. Navigation
• Created with the intention to simplify using 1 Activity for the
whole app
• Theoretically, the single Activity could be
class MainActivity: AppCompatActivity(R.layout.activity_main)
and no further code (see NavHostFragment)
• Track navigation state, handle argument passing
• Allow defining scopes shared between Fragments (see
NavBackStackEntry)
36. Hilt
• Dependency injection framework written on top of
Dagger
• Simplify injection of Android components via global
configuration and „automatic” injection
• Integration with ViewModel (+ SavedState) and
Navigation (by hiltNavGraphViewModels)
40. Reactive State Management
• Goal: Minimizing moving parts
– No mutation can happen without being notified
of it
• Data classes shouldn’t have var! (only val)
• Observable data holder shouldn’t have either mutable
classes or mutable collections!
– State synchronization is idempotent
• observe over a data holder should not execute one-off
actions that cause a different effect on re-execution
44. And
private val b1 = savedStateHandle.getLiveData("b1", 1)
private val b2 = savedStateHandle.getLiveData("b2", 2)
private val b3 = savedStateHandle.getLiveData("b3", 3)
private val b4 = savedStateHandle.getLiveData("b4", 4)
private val b5 = savedStateHandle.getLiveData("b5", 5)
private val c3 = combineTuple(
b1.asFlow(),
b2.asFlow(),
b3.asFlow(),
b4.asFlow(),
b5.asFlow(),
).map { (b1, b2, b3, b4, b5) ->
b1 * b2 * b3 * b4 * b5
}.stateIn(…)
45. Now you can do
// validation
val username: MutableLiveData<String> =
savedStateHandle.getLiveData("username", "")
val password: MutableLiveData<String> =
savedStateHandle.getLiveData("password", "")
val isRegisterAndLoginEnabled : LiveData<Boolean> =
validateBy(
username.map { it.isNotBlank() },
password.map { it.isNotBlank() },
)
46. Or
// state management
private val currentQuery = savedStateHandle.getLiveData("currentQuery", "")
private val isLoading = MutableStateFlow(false)
private val results: Flow<List<Sport>> = currentQuery.asFlow()
.distinctUntilChanged()
.debounce(125L)
.onEach { query ->
if (query.isNotEmpty()) {
isLoading.value = true
}
}
.flatMapLatest { query ->
when {
query.isEmpty() -> emptyFlow()
else -> sportDao.getSports(query)
}
}.onEach {
isLoading.value = false
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), listOf())
val state: Flow<ViewState> = combineTuple(
currentQuery,
isLoading,
results,
).map { (query, isLoading, results) ->
ViewState(query, isLoading, results) // no copying needed!
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
48. Try to avoid
abstract class BaseViewModel<S: BaseViewModel.State>( // do not trust
initialState: S
): ViewModel() {
abstract class State
private val flowState: MutableStateFlow<S> = // no
MutableStateFlow(initialState)
// or
private val liveDataState: MutableLiveData<S> = // no
MutableLiveData(initialState)
abstract val state: StateFlow<S> // can be ok with getter
// (but complicated and not needed)
}
class MyViewModel: BaseViewModel<MyViewModel.State>() {
@Parcelize
data class State( // no
val allData: List<T> = emptyList(), // no
val allFields: String = "",
val transientStates: Boolean = false, // no
): BaseViewModel.State()
// state.value = state.value.copy(someState = it.someState.copy(…))
}
49. Some reactive helpers you can use
• Rx-CombineTuple-Kt
• Rx-ValidateBy-Kt
• LiveData-CombineTuple-Kt
• LiveData-ValidateBy-Kt
• Flow-CombineTuple-Kt
• Flow-ValidateBy-Kt
• LiveData-CombineUtil-Java
50. Other resources
• Understand Kotlin Coroutines on Android
(Google I/O'19)
• LiveData with Coroutines and Flow (Android
Dev Summit '19)
• Android Coroutines: How to manage async
tasks in Kotlin - Manuel Vivo
• Building Reactive UIs with LiveData and
SavedStateHandle (or equivalent approaches like
Rx)