Resaca
Compose Multiplatform library to scope ViewModels to a Composable, surviving configuration changes and navigation
Install / Use
/learn @sebaslogen/ResacaREADME
Article about this library: Every Composable deserves a ViewModel
Resaca 🍹
The right scope for objects and View Models in Android Compose.
Resaca provides a simple way to keep a Jetpack ViewModel (or any other object) in memory during the lifecycle of a @Composable function and automatically
clean it up when not needed anymore. This means, it retains your object or ViewModel across recompositions, during configuration changes, and also when the
container Fragment or Compose Navigation destination goes into the backstack.
With Resaca you can create fine grained ViewModels for fine grained Composables and finally have reusable components across screens.
Why
Compose allows the creation of fine-grained UI components that can be easily reused like Lego blocks 🧱. Well architected Android apps isolate functionality in small business logic components (like use cases, interactors, repositories, etc.) that are also reusable like Lego blocks 🧱.
Screens are built using Compose components together with business logic components, and the standard tool to connect these two types of components is a Jetpack ViewModel. Unfortunately, ViewModels can only be scoped to a whole screen (or larger scope), but not to smaller Compose components on the screen.
In practice, this means that we are gluing UI Lego blocks with business logic Lego blocks using a big glue class for the whole screen, the ViewModel 🗜.
Until now...
Installation
Just include the library (less than 5Kb):
<details open> <summary>Kotlin (KTS)</summary>// In module's build.gradle.kts
dependencies {
// The latest version of the lib is available in the badget at the top from Maven Central, replace X.X.X with that version
implementation("io.github.sebaslogen:resaca:X.X.X")
}
</details>
<details>
<summary>Groovy</summary>
dependencies {
// The latest version of the lib is available in the badget at the top from Maven Central, replace X.X.X with that version
implementation 'io.github.sebaslogen:resaca:X.X.X'
}
</details>
Usage
Inside your @Composable function create and retrieve an object using rememberScoped to remember any type of object (except ViewModels). For ViewModels use
viewModelScoped. That's all 🪄✨
Examples:
<details open> <summary>Scope an object to a Composable</summary>@Composable
fun DemoScopedObject() {
val myRepository: MyRepository = rememberScoped { MyRepository() }
DemoComposable(inputObject = myRepository)
}
</details>
<details open>
<summary>Scope a ViewModel to a Composable</summary>
@Composable
fun DemoScopedViewModel() {
val myScopedVM: MyViewModel = viewModelScoped()
DemoComposable(inputObject = myScopedVM)
}
</details>
<details>
<summary>Scope a ViewModel with a dependency to a Composable</summary>
@Composable
fun DemoScopedViewModelWithDependency() {
val myScopedVM: MyViewModelWithDependencies = viewModelScoped { MyViewModelWithDependencies(myDependency) }
DemoComposable(inputObject = myScopedVM)
}
</details>
<details>
<summary>Scope a ViewModel with a key to a Composable</summary>
@Composable
fun DemoViewModelWithKey() {
val scopedVMWithFirstKey: MyViewModel = viewModelScoped("myFirstKey") { MyViewModel("myFirstKey") }
val scopedVMWithSecondKey: MyViewModel = viewModelScoped("mySecondKey") { MyViewModel("mySecondKey") }
// We now have 2 ViewModels of the same type with different data inside the same Composable scope
DemoComposable(inputObject = scopedVMWithFirstKey)
DemoComposable(inputObject = scopedVMWithSecondKey)
}
</details>
<details>
<summary>Scope a ViewModel with a dependency injected with Koin to a Composable</summary>
@Composable
fun DemoKoinInjectedViewModelWithDependency() {
val myInjectedScopedVM: MyViewModelWithDependencies = viewModelScoped() { getKoin().get { parametersOf(myConstructorDependency) } }
DemoComposable(inputObject = myInjectedScopedVM)
}
</details>
<details>
<summary>Scope a ViewModel with a clear delay to a Composable</summary>
@Composable
fun DemoScopedViewModelWithClearDelay() {
// The ViewModel will be kept in memory for 5 seconds after the Composable is disposed,
// giving it a chance to be reused if the Composable returns to composition (e.g. quick navigation back and forth)
val myScopedVM: MyViewModel = viewModelScoped(clearDelay = 5.seconds)
DemoComposable(inputObject = myScopedVM)
}
</details>
<details>
<summary>Scope an object with a clear delay to a Composable</summary>
@Composable
fun DemoScopedObjectWithClearDelay() {
val myRepository: MyRepository = rememberScoped(clearDelay = 5.seconds) { MyRepository() }
DemoComposable(inputObject = myRepository)
}
</details>
<details>
<summary>Use a different ViewModel for each item in a LazyColumn and scope them to the Composable that contains the LazyColumn</summary>
@Composable
fun DemoManyViewModelsScopedOutsideTheLazyColumn(listItems: List<Int> = (1..1000).toList()) {
val keys = rememberKeysInScope(inputListOfKeys = listItems)
LazyColumn() {
items(items = listItems, key = { it }) { item ->
val myScopedVM: MyViewModel = viewModelScoped(key = item, keyInScopeResolver = keys)
DemoComposable(inputObject = myScopedVM)
}
}
}
</details>
Once you use the rememberScoped or viewModelScoped functions, the same object will be restored as long as the Composable is part of the composition, even if
it temporarily leaves composition on configuration change (e.g. screen rotation, change to dark mode, etc.) or while being in the backstack.
For ViewModels, in addition to being forgotten when they're really not needed anymore, their coroutineScope will also be automatically canceled because
ViewModel's onCleared method will be automatically called.
💡 Optional key: a key can be provided to the call,
rememberScoped(key) { ... }orviewModelScoped(key) { ... }. This makes possible to forget an old object when there is new input data during a recomposition (e.g. a new input id for your ViewModel).
💡 Optional clearDelay: a
clearDelaycan be provided to delay the disposal of the scoped object after the Composable is removed from composition. This is useful when a Composable might briefly leave composition and return (e.g. quick navigation back and forth), and you want to avoid recreating expensive objects. Usage:rememberScoped(clearDelay = 5.seconds) { ... }orviewModelScoped(clearDelay = 5.seconds). If the Composable returns to composition before the delay expires, the disposal is cancelled and the same object is reused.
⚠️ Note that ViewModels remembered with
viewModelScopedshould not be created using any of the ComposeviewModel()orViewModelProvidersfactories, otherwise they will be retained in the scope of the screen regardless ofviewModelScoped. Also, if a ViewModel is remembered withrememberScoped, instead ofviewModelScoped, then its clean-up method won't be called, so it's always better to useviewModelScopedfor ViewModels.
Sample use cases
Here are some sample use cases reported by the users of this library:
- ❤️ Isolated and stateful UI components like a favorite button that are widely used across the screens. This
FavoriteViewModelcan be very small, focused and only require an id to work without affecting the rest of the screen's UI and state. - 🗪 Dialog pop-ups can have their own business-logic with state that is better to isolate in a separate ViewModel but the lifespan of these dialogs might be short, so it's important to clean-up the ViewModel associated to a Dialog after it has been closed.
- 📃 A LazyColumn with a ViewModel per list item. Each item can have its own complex logic in an isolated ViewModel that will be lazily loaded when the item is visible for the first time. The ViewModel will cleared and destroyed when the item is not part of the list in the source data or the whole LazyColumn is removed.
- 📄📄 Multiple instances of the same type of ViewModel in a screen with a view-pager. This screen will have multiple sub-pages that use the same ViewModel class with different ids. For example, a screen of holiday destinations with multiple pages and each page with its own `HolidayD
