Quantum is a general purpose state management library designed for building easy, stable and thread safe Android applications. It was inspired by AirBnb's MvRx and tailored for building reliable ViewModels.
dependencies {
implementation "io.sellmair:quantum:1.0.0"
// optional rx extensions
implementation "io.sellmair:quantum-rx:1.0.0"
// optional LiveData extensions
implementation "io.sellmair:quantum-livedata:1.0.0"
}
States should always be immutable. I highly recommend using kotlin data classes to make immutability easy 👍
Example:
data class MyState(
val isLoading: Boolean = false,
val error: Error? = null,
val content: Content? = null,
val userLocation: Location? = null)
A Quantum is the owner of your state. It applies all reducers, invokes actions and publishes new states.
Example:
// Create a new Quantum with initial state.
val quantum = Quantum.create(MyState())
Reducers are functions that take the current state and create a new state.
Reducers will always be called by a internal thread of the Quantum
.
Only one reducer will run at a time!
Reducers are allowed to return the same (untouched) instance to signal a no-operation.
A simple reducer that that says hello to a certain user.
data class SimpleState(val name: String, val message: String = "" )
val quantum = Quantum.create(SimpleState("Julian"))
fun sayHello() = quantum.setState {
copy(message = "Hello $name")
}
Unlike other "State Owner" concepts, Quantum allows reducers to dispatch async operations. This decision was made to give developers the option to handle side-effects inside a safer environment.
Much more complicated reducer problem:
We want to
- Load content from repository asyncronously
- Ensure that only one loading operation is running at a time
- Publish the content when fetched successfully
- Publish the error when an error occurred
// Reducer that fetches the content (if not currently loading)
fun loadContent() = quantum.setState {
// Do not try to load the content while currently loading
// Returning this (the current / input state) signals the quantum that
// this reducer was a NOOP
if(isLoading) return@setState this
// Dispatch a async loading operation with myRepository (exemplary)
myRepository.loadContent
.onSuccess(::onContentLoaded)
.onError(::onError)
.execute()
// Copy the current state but set loading flag
copy(isLoading = true)
}
fun onContentLoaded(content: Content) = setState {
// Content loaded:
// Copy current state and clear any error
copy(content = content, error = null, isLoading = false)
}
fun onError(error: Error) = setState {
// Copy current state but publish the error
copy(error = error, isLoading = false)
}
Actions are parts of your code that require the most recent state, but do not intend to change it.
Actions will always be called by a internal thread of the Quantum
and run after
all reducers are applied.
val quantum = Quantum.create(SimpleState(name = "Balazs"))
quantum.setState {
copy(name = "Paul")
}
quantum.withState {
// will print 'Hello Paul'
Log.i("Readme", "Hello $name")
}
Listeners are invoked by Android's main thread by default. It is possible to configure the thread which invokes listeners by specifying an Executor.
quantum.addStateListener { state -> print(state.message) }
fun onState(state: SimpleState){
// be awesome
}
fun onStart() {
quantum.addListener(::onState)
}
fun onStop() {
quantum.removeListener(::onState)
}
fun onStart() {
quantum.rx.subscribe { state -> /* be awesome */ }
}
It is possible to map a Quantum to create a 'Child-Quantum' which can enqueue reducers and actions as usual. The state of this child will be in sync with the parent Quantum.
data class ChildState(val name: String, val age: Int)
data class ParentState(val name: String, val age: Int, val children: List<ChildState>)
// Get the quantum instance of the parent state
val parentQuantum: Quantum<ParentState> = /* ... */
// Create the child state
val childQuantum = parentQuantum
.map { parentState -> parentState.children }
.connect { parentState, children -> parentState.copy(children = children) }
// Increase the age of all children
childQuantum.setState { children ->
children.map { child -> child.copy(age=child.age++) }
}
It is possible to record all states created in a Quantum
.
val quantum = Quantum.create(MyState()).apply {
history.enabled = true
}
fun debug(){
for(state in quantum.history){
print(state)
}
}
A Quantum
has to be stopped if it's no longer needed, in order to stop the internal background
thread and release all resources.
quantum.quit() // will quit as fast as possible
quantum.quitSafely() // will quit after all currently enqueued reducers / actions
I suggest having one 'ViewState' for each ViewModel. The ViewModel itself
might want to implement Quantum
itself.
data class LoginState(
val email: String = "",
val password: String = "",
val user: User? = null)
class LoginViewModel(private val loginService: LoginService):
ViewModel(),
Quantum<LoginState> by Quantum.create(LoginState()) {
fun setEmail(email: String) = setState {
copy(email = email)
}
fun setPassword(password: String) = setState {
copy(password = password)
}
fun login() = setState {
val user = loginService.login(email, password)
copy(user = user)
}
override fun onCleared() {
// Quit the quantum
quit()
}
}
It is possible to configure the defaults of Quantum for your whole application. For example: It is possible to specify the default threading mode, history settings, or even the thread pool that is shared for multiple Quantum instances.
// configure defaults
Quantum.configure {
// Quantum instances will use the given thread pool by default
this.threading.default.mode = Threading.Pool
// Listeners are now invoked by a new background thread
this.threading.default.callbackExecutor = Executors.newSingleThreadExecutor()
// Override the default shared thread pool
this.threading.pool = Executors.newCachedThreadPool()
// Set history default to enabled with limit of 100 states
this.history.default.enabled = true
this.history.default.limit = 100
// Get info's from quantum
this.logging.level = LogLevel.INFO
}
Quantum.create(
// initial state
initial = LoginState(),
// invoke listeners by background thread
callbackExecutor = Executors.newSingleThreadExecutor(),
// use thread pool
threading = Threading.Pool)