Ballast Undo
Overview
Undo/Redo functionality can be very handy in situations where the user is interacting with larger amounts of data over a longer period of time. This module implements standard state-based undo/redo behavior making it simple to navigate through the history of a user's changes over time.
Note that the default functionality is strictly state-based, and it works by observing States emitted from the ViewModel and restoring captured State when requested, irrespective of any particular Inputs that changed the State. It does not attempt to undo specific Inputs, which may have performed other actions like emitting Events, starting Side Jobs, or other "side effects" which cannot be so easily tracked and undone.
Usage
Start by creating a UndoController
for your ViewModel. This controller includes functions to undo()
and redo()
which should be called from the UI, as well as corresponding Flows
which notify whether such actions are can be used.
A default implementation, DefaultUndoController
may be used, but for advanced use-cases such as persisting the
undo/redo state across application restarts, you may implement your own.
Then, set up your ViewModel with the BallastUndoInterceptor
added, which needs that Controller we just created.
class ExampleViewModel(
coroutineScope: CoroutineScope,
controller: UndoController<
ExampleContract.Inputs,
ExampleContract.Events,
ExampleContract.State>,
) : BasicViewModel<
ExampleContract.Inputs,
ExampleContract.Events,
ExampleContract.State>(
coroutineScope = coroutineScope,
config = BallastViewModelConfiguration.Builder()
.apply {
this += BallastUndoInterceptor(controller)
}
.withViewModel(
initialState = ExampleContract.State(),
inputHandler = ExampleInputHandler(),
name = "Example",
)
.build(),
)
@Composable
fun mainUi() {
val controller = remember { DefaultUndoController<ExampleContract.Inputs, ExampleContract.Events, ExampleContract.State>() }
val applicationCoroutineScope = rememberCoroutineScope()
val viewModel = remember(applicationCoroutineScope, controller) { ExampleViewModel(applicationCoroutineScope, controller) }
val uiState by viewModel.observeStates().collectAsState()
// buttons to undo/redo
val isUndoAvailable by undoController.isUndoAvailable.collectAsState(false)
val isRedoAvailable by undoController.isRedoAvailable.collectAsState(false)
Button(onClick = { controller.undo() }, enabled = isUndoAvailable) { Text("Undo") }
Button(onClick = { controller.redo() }, enabled = isRedoAvailable) { Text("Redo") }
// the normal content for this screen, which will be updated via undo/redo as prompted by the user
Content(uiState) {
viewModel.trySend(it)
}
}
Installation
repositories {
mavenCentral()
}
// for plain JVM or Android projects
dependencies {
implementation("io.github.copper-leaf:ballast-undo:4.2.1")
}
// for multiplatform projects
kotlin {
sourceSets {
val commonMain by getting {
dependencies {
implementation("io.github.copper-leaf:ballast-undo:4.2.1")
}
}
}
}