The best way to survive configuration adjustments, survive course of loss of life, and scope ViewModels with out utilizing up Decompose stuff
Decompose model 1.0.0 latest 🙌. I feel it is a good alternative to share my particular use case with Decompose the place I exploit it to share navigation logic between Android and Desktop with out resorting to Decomposition substances pattern.
My app is focusing on Android, iOS, Desktop and Internet. Nonetheless, I simply wish to share navigation between my Android shopper and Desktop as they’re the one ones utilizing Compose Multiplatform. (on iOS, use SwiftUI and on the net, solely React with out enveloping response with simply plain previous vanilla typescript)
Listed here are some issues you’ll run into – when extracting navigate to shared code
- Basic navigation abstraction.
- Retain and localize your view fashions
- Restore view mannequin state after course of dies.
Would not Decompose clear up all these issues? Why not go all in?
That is actually only a interest. I would like make my very own The structure for my software and the inheritance-based part strategy usually are not my forte. Thankfully, Decompose lets you use elements of it and mix your navigation with enterprise logic (if you’ll). Decompose offers you in depth customization choices to tailor the library to your wants. That is the very last thing I did
That is what we can be working in direction of.
sealed class Display screen: Parcelable {
@Parcelize object Checklist : Display screen()
@Parcelize information class Particulars(val element: String) : Display screen()
}
@Composable
enjoyable ListDetailScreen() {
val router: Router = rememberRouter(listOf(Checklist))
RoutedContent(
router = router,
animation = stackAnimation(slide()),
) { display screen ->
when (display screen) {
Checklist -> ListScreen(onSelect { element -> router.push(element) } )
is Particulars -> DetailsScreen(display screen.element)
}
}
}
@Composable
enjoyable ListScreen(onSelect: (element: String) -> Unit) {
val viewModel: ListViewModel =
rememberViewModel { savedState -> ListViewModel(savedState) }
val state: ListState by viewModel.states.collectAsState()
LazyColumn {
objects(state.objects) { merchandise ->
TextButton(onClick = { onSelect(merchandise) } ) {
Textual content(textual content = merchandise)
}
}
}
}
@Composable
enjoyable DetailScreen(element: String) {
val viewModel: ListViewModel =
rememberViewModel(key = element) { DetailsViewModel(element) }
val state: DetailsState by viewModel.states.collectAsState()
Toolbar(title = element)
Textual content(state.descriptions)
}
There are 3 foremost elements to work with
- Router (instance:
Router
) preserve the display screen configuration (e.g.:Display screen
) contained in the FILO . stack - ONE
@Composable
for every configuration and prime stage@Composable
to modify between these (ListScreen
,DetailsScreen
AndListDetailsScreen
corresponding) - One
ViewModel
instance (instance:ListViewModel
,DetailsViewModel
) persists after configuration adjustments, is in scope of the router (deleted when the person leaves the display screen), and may restore its state after the method dies
This drawback was solved for me with Decompose within the first place. Creator of Decompose, Arkadi Ivanov completely summarizes the whole lot you want, beneath 1̶0̶0̶ 30 strains of code. I extremely suggest studying this primary – earlier than persevering with to learn mine.
There are three essential elements that we use from Decompose to make issues extra lovely for ourselves, they’re
-
StackNavigator
; permit youpush
orpop
completely different configuration -
ChildStack
&rememberChildStack
; the place all of your configurations are saved within the “FILO” stack -
@Composable Kids(stack: ChildStack, ..)
; One@Composable
perform that lets you elevate completely different screens for every profile (+ some fancy animations for transitions between screens)
Router API
I prolonged Arkadii’s implementation, by wrapping each StackNavigator
And State
in a single Router
. This Router
The title is impressed by Conductor
import com.arkivanov.essenty.parcelable.Parcelable
import com.arkivanov.decompose.ComponentContext
import com.arkivanov.decompose.router.stack.ChildStack
import com.arkivanov.decompose.router.stack.StackNavigation
class Router(
personal val navigator: StackNavigation,
val stack: State>,
) : StackNavigation by navigator
Be aware that the configuration (template kind C
) have to be a Parcelable
to be part of ChildStack
. That is the phrase nature (by the identical creator)
I would like one other wrapper perform to supply router for youths by wrapping them @Composable Kids(stack: ChildStack, ..)
with
import com.arkivanov.decompose.ExperimentalDecomposeApi
import com.arkivanov.decompose.extensions.compose.jetbrains.stack.Kids
import com.arkivanov.decompose.extensions.compose.jetbrains.stack.animation.StackAnimation
@OptIn(ExperimentalDecomposeApi::class)
@Composable
enjoyable RoutedContent(
router: Router,
modifier: Modifier = Modifier,
animation: StackAnimation? = null,
content material: @Composable (C) -> Unit,
) {
Kids(
stack = router.stack.worth,
modifier = modifier,
animation = animation,
) { little one ->
CompositionLocalProvider(LocalComponentContext gives little one.occasion) {
content material(little one.configuration)
}
}
}
Now now we have a Router
and a strategy to provision a router, then I would like a strategy to hook any new router Composables
. that I’ve Not but one other wrapper wrap round Decompose’s rememberChildStack
@Composable
inline enjoyable rememberRouter(
stack: Checklist,
handleBackButton: Boolean = true
): Router {
val navigator: StackNavigation = keep in mind { StackNavigation() }
val childStackState: State> = rememberChildStack(
supply = navigator,
initialStack = { stack },
key = C::class.getFullName(),
handleBackButton = handleBackButton
)
return keep in mind { Router(navigator = navigator, stack = childStackState) }
}
That is lots of wrapping paper 🍬🍬🍬. You might ask, how do all these packages make the location less complicated to make use of?
Utilizing Router API
Let’s check out a easy itemizing particulars display screen. I’ll use the identical instance display screen configurations from the unique publish.
sealed class Display screen: Parcelable {
@Parcelize object Checklist : Display screen()
@Parcelize information class Particulars(val element: String) : Display screen()
}
A easy itemizing particulars display screen now appears like this
@Composable
enjoyable ListDetailScreen() {
val router: Router = rememberRouter(listOf(Checklist))
RoutedContent(
router = router,
animation = stackAnimation(slide()),
) { display screen ->
when (display screen) {
is Checklist -> ListScreen(onSelect { element -> router.push(element) } )
is Particulars -> DetailsScreen(display screen.element)
}
}
}
Neat stuff 👍 Transfer on to the following drawback
Your Android exercise goes by a life cycle (and if you happen to used fragments, much more life cycles!). Typically you wish to retain an occasion (e.g. a view kind) by a number of lifecycle levels with out dropping arduous to search out standing. If the instances themselves can’t match a Bundle
or like a Parcelable
– you possibly can’t get away with simply utilizing rememberSavable
and that is normally the place you see most individuals utilizingandroidx.lifecycle.ViewModel
and let both Exercise
, Fragment
or Utility
maintain into these situations whereas the lifecycle does its job.
API ViewModels
Decomposition of use nature (from the identical creator) create a cross-platform abstraction to realize this and that is known as InstanceKeeper
. To ship our variations, all we have to do is deploy InstanceKeeper.Occasion
on the objects that you just wish to be saved.
import com.arkivanov.essenty.instancekeeper.InstanceKeeper
open class ViewModel() : InstanceKeeper.Occasion {
override enjoyable onDestroy() { // Clear up }
}
On condition that we’re not utilizing Decompose as supposed, we have to wire this up ourselves.
@Composable
inline enjoyable rememberViewModel(
key: Any = T::class,
crossinline block: @DisallowComposableCalls () -> T
): T {
val part: ComponentContext = LocalComponentContext.present
val packageName: String = T::class.getFullName()
val viewModelKey = "$packageName.view-model"
return keep in mind(key) {
part.instanceKeeper.getOrCreate(viewModelKey) { block() }
}
}
decompose ComponentContext
do all of the heavy lifting for us right here. It manages these variations by defining their scope for every little one part and eradicating these variations when they’re not on the stack.
Utilizing the ViewModels API
Declare the ViewModel of the display screen by extending the bottom ViewModel
class ListViewModel() : ViewModel()
Then use it with rememberViewModel
@Composable
enjoyable ListScreen() {
val viewModel: ListViewModel = rememberViewModel { ListViewModel() }
}
In the event you want reissued new case when the parameter adjustments you possibly can move in a key
@Composable
enjoyable DetailScreen(element: String) {
val viewModel = rememberViewModel(key = element) { ListViewModel(element) }
}
Fairly neat stuff 👍 Transfer on to the following drawback
You might die however that does not imply your opinion standing can be ☠️! Android can kill your course of if you happen to really feel prefer it – when nobody is watching. So you must cope with that every so often. That is normally the place you see most individuals utilizing androidx.lifecycle.SavedStateHandle
to avoid wasting state earlier than loss of life and restore state after course of restart. We’d like an identical abstraction on the cross-platform aspect to make this occur.
Saved State API
To begin with, we have to create a wrapper to wrap our display screen state. i’ll name this SavedState
. Be aware that we will retain the state throughout loss of life just one if your standing is Parcelable
.
import com.arkivanov.essenty.parcelable.Parcelable
import com.arkivanov.essenty.parcelable.Parcelize
@Parcelize
information class SavedState(val worth: Parcelable): Parcelable
We additionally want a deal with to carry a single occasion of this wrapped state to persist upon configuration adjustments. I’ll name this deal with SavedStateHandle
@Parcelize
information class SavedState(val worth: Parcelable): Parcelable
class SavedStateHandle(default: SavedState?): InstanceKeeper.Occasion {
personal var savedState: SavedState? = default
val worth: Parcelable? get() = savedState
enjoyable get(): T? = savedState?.worth as? T?
enjoyable set(worth: Parcelable) { this.savedState = SavedState(worth) }
override enjoyable onDestroy() { savedState = null }
}
nature ofStateKeeper
is the cross-platform abstraction of androidx.lifecycle.SavedStateHandle
and that is accessible to us by ComponentContext
. Incorporating that into our little API requires just some extra modifications. We have to modify the rememberViewModel
hook which now we have executed earlier than to move on this SavedStateHandle
@Composable
inline enjoyable rememberViewModel(
key: Any = T::class,
crossinline block: @DisallowComposableCalls (savedState: SavedStateHandle) -> T
): T {
val part: ComponentContext = LocalComponentContext.present
val stateKeeper: StateKeeper = part.stateKeeper
val instanceKeeper: InstanceKeeper = part.instanceKeeper
val packageName: String = T::class.getFullName()
val viewModelKey = "$packageName.viewModel"
val stateKey = "$packageName.savedState"
val (viewModel, savedState) = keep in mind(key) {
val savedState: SavedStateHandle = instanceKeeper
.getOrCreate(stateKey) { SavedStateHandle(stateKeeper.devour(stateKey, SavedState::class)) }
val viewModel: T = instanceKeeper.getOrCreate(viewModelKey) { block(savedState) }
viewModel to savedState
}
LaunchedEffect(Unit) {
if (!stateKeeper.isRegistered(stateKey))
stateKeeper.register(stateKey) { savedState.worth }
}
return viewModel
}
decompose And nature StateKeeper
do all of the heavy lifting for us right here. It calls the StateKeeper
to get the state saved in SavedStateHandle
and provides again to SavedStateHandle
(we’ll get this from the state controller to revive the default state in ViewModel
Subsequent).
Perfection! 👌
Utilizing the SavingState API
If you need your state to persist after course of loss of life ️ simply use StateKeeper
(or depart it if you do not need that)
@Composable
enjoyable ListScreen() {
val viewModel: ListViewModel =
rememberViewModel { savedState -> ListViewModel(savedState) }
}
Then we have to use it after we instantiate our view mannequin, which we will do by passing it by the constructor as a parameter
class ListViewModel(savedState: SavedStateHandle) : ViewModel() {
personal val defaultState: ListState = savedState.get() ?: ListState()
val states: StateFlow by lazy {
moleculeFlow() { .. } // or nonetheless you wish to handle your state
.onEach { state -> savedState.set(state) }
.stateIn(this, Lazily, defaultState)
}
}
Fairly neat stuff 👍
That is how I exploit Decompose in my software 😎. I am positive there are issues that I have never considered that would break my plans — however this appears to work and solves all the standard issues you’d encounter when attempting to place logic in issues. oriented to the shared class in Compose Multiplatform functions.
Try pattern apps right here