Gabor Varadi - Reactive State Management with Jetpack Components

GaborVaradi3 75 views 51 slides Sep 25, 2024
Slide 1
Slide 1 of 51
Slide 1
1
Slide 2
2
Slide 3
3
Slide 4
4
Slide 5
5
Slide 6
6
Slide 7
7
Slide 8
8
Slide 9
9
Slide 10
10
Slide 11
11
Slide 12
12
Slide 13
13
Slide 14
14
Slide 15
15
Slide 16
16
Slide 17
17
Slide 18
18
Slide 19
19
Slide 20
20
Slide 21
21
Slide 22
22
Slide 23
23
Slide 24
24
Slide 25
25
Slide 26
26
Slide 27
27
Slide 28
28
Slide 29
29
Slide 30
30
Slide 31
31
Slide 32
32
Slide 33
33
Slide 34
34
Slide 35
35
Slide 36
36
Slide 37
37
Slide 38
38
Slide 39
39
Slide 40
40
Slide 41
41
Slide 42
42
Slide 43
43
Slide 44
44
Slide 45
45
Slide 46
46
Slide 47
47
Slide 48
48
Slide 49
49
Slide 50
50
Slide 51
51

About This Presentation

Gabor Varadi - Reactive State Management with Jetpack Components (2020)


Slide Content

ReactiveStateManagement
UsingJetpackComponents
Gabor Varadi
@zhuinden

What is theproblemwearetryingto
solve?

•Save/restoreminimalstateacrossprocess
death
•Executedataloadingasynchronouslybasedon
state
–Shouldbe keptacrossconfigurationchanges
•Minimize„movingparts”
–Mutationsshouldbe controlled
–Mutationsshouldbe observed

•Save/restoreminimalstateacrossprocess
death
•Executedataloadingasynchronouslybased
onstate
–Shouldbe keptacrossconfigurationchanges
•Minimize„movingparts”
–Mutationsshouldbe controlled
–Mutationsshouldbe observed

•Save/restoreminimalstateacrossprocess
death
•Executedataloadingasynchronouslybasedon
state
–Shouldbe keptacrossconfigurationchanges
•Minimize„movingparts”
–Mutationsshouldbe controlled
–Mutationsshouldbe observed

Processdeath
(aka: howAndroid appsactuallywork)

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.

How to induce process death?
•Step 1: put app in background with HOME
•Step 2: press „Terminate application”
•Step 3: restart app from launcher

For apps you don’t own
https://play.google.com/store/apps/details?id=me.empirical.android.application.fillme
mory&hl=en

Commonmistakes
•Assumingthatstaticvariablesinitializedonone
screen(like dataloadingona Splashscreen) stay
setontheotherscreen(nope)
•Assumingthatif(savedInstanceState == null) {
is trueatleastonce(nope)
•Holding a referencetoa Fragmentinstance
withoutusingfindFragmentByTagfirst(for
example”ViewPagerAdapter.addFragment ()”)
(don’t)

Howtodetectprocessdeath?
if(savedInstanceState != null
&& lastNonConfigurationInstance == null) {
}
Orin BaseActivity:
companionobject{
var isRestored: Boolean = false
}
if(savedInstanceState !=null) {
if(!isRestored) {
isRestored= true
//here
}
}

Saving UI states

What needs to be persistedacross
processdeath?
•Navigation state is already managed by the
system on Android out of thebox
–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(!)

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

Example for saving/restoringstate
(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))
}

Loading data
•Asynchronous loading should either begin on
initialization, or when observed
•Data can be loaded via a transformationchain
fromtheobservablesourcethatstores the
state –changes trigger new dataload
(switchMap, flatMapLatest)

HowcanJetpackhelp?

•Survivingconfigurationchanges
–ViewModel
•Lifecycle-awaredataloading
–Lifecycle(livedata/ lifecycle-ktx)
•Persistentstorage(observablequeries)
–Room
–DataStore(beta)
–*Paging(beta)
•Backgroundjobprocessing
–WorkManager

•Staterestorationacrossprocessdeath
–SavedStateRegistry(savedstate)
–SavedStateHandle(viewmodel-savedstate)
•ConnectingUI tostatemodel
–Databinding
–Compose(beta)
•Scopingdata/statebetweenscreens+ argument
passing
–Navigation
•Piecetogetherallthethings
–Hilt

ViewModel
•Storedacrossconfigurationchangesin a
ViewModelStore (ComponentActivity, Fragment,
and NavBackStackEntryareViewModelStoreOwner)
•Theyhavetheirownlifecycleforwhenthe
ViewModelStoreis destroyed(onCleared(),
viewModelScope)
•Stateand asynchronousoperationsgo here

ViewModel
•Originally:
valviewModel= ViewModelProvider(
viewModelStoreOwner,
viewModelProviderFactory
).get(MyViewModel::class.java)
•KTX (fragment-ktx):
privatevalviewModelbyviewModels<MyViewModel>(
ownerProducer= { viewModelStoreOwner },
factoryProducer = { viewModelProviderFactory },
)

Lifecycle-ktx
•LiveData/ MutableLiveData/
MediatorLiveData
•liveData(coroutineContext, timeoutMs) {coroutine
builder(CoroutineLiveData)
•liveData.asFlow() (FlowLiveData)

AlsoLifecycle-ktx
•LifecycleOwner.lifecycleScope
(fragment.viewLifecycleOwner.lifecycle
Scope)
•ViewModel.viewModelScope
•LifecycleCoroutineScope.launchWhenStarted ,
Lifecycle.whenStateAtLeast
•LifecycleOwner.addRepeatingJob ,
Lifecycle.repeatOnLifecycle (alpha)

Room
•Exposesobservablequeries
–ifthedatabaseis writtento, thenthequeriesare
re-evaluated
•Integrations:
–LiveData(ComputableLiveData)
–Flowable/Observable(RxJava2.x)
–Flow (kotlinx.coroutines)

WorkManager
•Schedule and eventuallyexecutebackgroundjobs
–Constraintsupport: executewhennetworkis available, etc
–OneTimeWorkRequest, uniquework
–Canbe usedtodownloaddataforoffline support
•Workertypes:
–ListenableWorker
–CoroutineWorker
–RxWorker

SavedState
•SavedStateRegistryallowssaving stateintoa Bundle
–Notcommonlyuseddirectly, butheavilyusedbyViewModel-
SavedState(and JetpackCompose’s
saveableStateHolder.SaveableStateProvider )
•Importantmethods:
–registerSavedStateProvider (String, SavedStateProvider)
–unregisterSavedStateProvider (String)
–consumeRestoredStateForKey (String): Bundle

ViewModel-SavedState
•SavedStateHandleallowssaving ViewModel’sstateintoa
Bundle
–Providesget(), set(), and most importantly
getLiveData() (SavingStateLiveData)
–Customtypescanbe savedwithsetSavedStateProvider ()
–Initializedwithargumentsbydefault
–OriginallycreatedbyAbstractSavedStateViewModelFactory

Databinding
•(PreferViewBindingifpossible)
•Allowsdirectlybindingagainststateviabinding
expressions
–one-waybindings(=”@{}”) vstwo-waybindings(=”@={}”)
–supportsObservableField / MutableLiveData /
MutableStateFlow
–Canbe usefulforforms
•Bindingadapterstosupportcustomobservable
propertiesfromXML in customviews

(JetpackCompose)
•(CompletelynewUI framework, beta)
•(Renderinghappensbyinvoking@Composable functions)
•(Changesin functionargumentsof @Composablefunctions
aretrackedbyCompose’sKotlincompilerplugin)
•(Whenfunctionargumentschange, thefunctionsthatdepend
onsaidstategetre-evaluatedand thereforere-rendered,
„recomposition”)

(JetpackCompose)

Navigation
•Createdwiththeintentiontosimplifyusing1 Activityforthe
wholeapp
•Theoretically, thesingleActivitycouldbe
classMainActivity: AppCompatActivity(R.layout.activity_main )
and no furthercode(seeNavHostFragment)
•Tracknavigationstate, handleargumentpassing
•AllowdefiningscopessharedbetweenFragments(see
NavBackStackEntry)

Hilt
•Dependencyinjectionframeworkwrittenontop of
Dagger
•Simplifyinjectionof Android componentsviaglobal
configurationand „automatic” injection
•IntegrationwithViewModel(+ SavedState) and
Navigation(byhiltNavGraphViewModels)

@HiltAndroidApp
classCustomApplication: Application()
@AndroidEntryPoint
classMainActivity: AppCompatActivity(R.layout.main_activity )
<?xmlversion="1.0" encoding="utf-8"?>
<FrameLayoutxmlns:android="http://schemas.android.com/ apk/res/android"
xmlns:app="http://schemas.android.com/ apk/res-auto"
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.fragment.app.FragmentContainerView
android:id="@+id/nav_host"
android:name="androidx.navigation.fragment.NavHostFragment "
android:layout_width="match_parent"
android:layout_height="match_parent"
app:defaultNavHost="true"
app:navGraph="@navigation/navigation_graph"/>
</FrameLayout>

@AndroidEntryPoint
classMyFragment: Fragment(R.layout.my_fragment) {
privatevalviewModelbyviewModels<MainViewModel>()
overridefunonViewCreated(view: View, savedInstanceState: Bundle) {
super.onViewCreated(view, savedInstanceState)
valbinding= MyFragmentBinding.bind (view)
valviewModel= viewModel
viewLifecycleOwner.lifecycleScope.launchWhenStarted {
viewModel.sport.collectLatest { sport ->
// bindviews
}
}
}
}
@HiltViewModel
classMyViewModel@Inject constructor(
privatevalsportDao: SportDao,
privatevalhandle: SavedStateHandle
): ViewModel() {
privatevalselectedSportId: MutableLiveData<String> =
handle.getLiveData("selectedSportId")
valsport: Flow<Sport> = selectedSportId.asFlow ().flatMapLatest{ id->
sportDao.getSport(id)
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed (), null)
}

ReactiveStateManagement

ReactiveStateManagement
•Goal: Minimizingmovingparts
–No mutationcanhappenwithoutbeingnotified
of it
•Data classesshouldn’thavevar! (onlyval)
•Observabledataholdershouldn’thaveeithermutable
classesormutablecollections!
–Statesynchronizationis idempotent
•observeover a dataholdershouldnotexecuteone-off
actionsthatcausea differenteffectonre-execution

Don’ts
MutableLiveData<ArrayList<T>>
Flow<ArrayList<T>>
publicfunsomeMethod(): ArrayList<T>
@Parcelize dataclassSomeClass(
var prop: String
): Parcelable

Do
MutableLiveData<List<T>>,
Flow<List<T>>
publicList<T> doSomething() {
ArrayList<T> copy= newArrayList<T>(original);

returnCollections.unmodifiableList (copy);
}
@Parcelize dataclassSomeClass(
valprop: String
): Parcelable

Then
Imaginea spreadsheetwithcellsof data,
and formulastocombinethem

And
privatevalb1 = savedStateHandle.getLiveData ("b1", 1)
privatevalb2 = savedStateHandle.getLiveData ("b2", 2)
privatevalb3 = savedStateHandle.getLiveData ("b3", 3)
privatevalb4 = savedStateHandle.getLiveData ("b4", 4)
privatevalb5 = savedStateHandle.getLiveData ("b5", 5)
privatevalc3 = combineTuple(
b1.asFlow(),
b2.asFlow(),
b3.asFlow(),
b4.asFlow(),
b5.asFlow(),
).map { (b1, b2, b3, b4, b5) ->
b1 * b2 * b3 * b4 * b5
}.stateIn(…)

Nowyoucando
// validation
valusername: MutableLiveData<String> =
savedStateHandle.getLiveData ("username", "")
valpassword: MutableLiveData<String> =
savedStateHandle.getLiveData ("password", "")
valisRegisterAndLoginEnabled : LiveData<Boolean> =
validateBy(
username.map{ it.isNotBlank() },
password.map{ it.isNotBlank() },
)

Or
// statemanagement
privatevalcurrentQuery= savedStateHandle.getLiveData ("currentQuery", "")
privatevalisLoading= MutableStateFlow(false)
privatevalresults: 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())
valstate: Flow<ViewState> = combineTuple(
currentQuery,
isLoading,
results,
).map { (query, isLoading, results) ->
ViewState(query, isLoading, results) // no copyingneeded!
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed (), null)

But

Trytoavoid
abstractclassBaseViewModel<S: BaseViewModel.State>( // donottrust
initialState: S
): ViewModel() {
abstractclassState
privatevalflowState: MutableStateFlow<S> = // no
MutableStateFlow(initialState)
// or
privatevalliveDataState: MutableLiveData<S> = // no
MutableLiveData(initialState)
abstractvalstate: StateFlow<S> // canbe ok withgetter
// (butcomplicatedand notneeded)
}
classMyViewModel: BaseViewModel<MyViewModel.State>() {
@Parcelize
dataclassState( // no
valallData: List<T> = emptyList(), // no
valallFields: String= "",
valtransientStates: Boolean = false, // no
): BaseViewModel.State()
// state.value= state.value.copy(someState= it.someState.copy(…))
}

Somereactivehelpersyoucanuse
•Rx-CombineTuple-Kt
•Rx-ValidateBy-Kt
•LiveData-CombineTuple-Kt
•LiveData-ValidateBy-Kt
•Flow-CombineTuple-Kt
•Flow-ValidateBy-Kt
•LiveData-CombineUtil-Java

Otherresources
•Understand Kotlin Coroutines on Android (Google
I/O'19)
•LiveDatawith Coroutines and Flow (Android Dev
Summit '19)
•Android Coroutines: How to manage async tasks in
Kotlin -Manuel Vivo
•Building Reactive UIs with LiveDataand
SavedStateHandle(or equivalent approaches like Rx)

Thank you for your attention!
Q/A?