State management system for Android Application, inspired by Redux and MVI.
This library provides
- Redux like state management.
- Easy coroutine integration.
- Lifecycle aware view state stream with Android Architecture Component.
This is what it looks like:
sealed class CounterIntent : Intent {
object IncrementIntent : CounterIntent()
object DecrementIntent : CounterIntent()
}
sealed class CounterAction : Action {
data class UpdateCountAction(val count: Int) : CounterAction()
}
data class CounterState(val count: Int = 0) : State
class CounterViewModel : OwlViewModel<CounterIntent, CounterAction, CounterState>(initialState = CounterState()) {
override fun intentToAction(intent: CounterIntent, state: CounterState): CounterAction = when (intent) {
IncrementIntent -> UpdateCountAction(state.count + 1)
DecrementIntent -> UpdateCountAction(state.count - 1)
}
override fun reducer(state: CounterState, action: CounterAction): CounterState = when (action) {
is UpdateCountAction -> state.copy(count = action.count)
}
}
class MainActivity : AppCompatActivity() {
private val counterViewModel: CounterViewModel by lazy {
ViewModelProviders.of(this).get(CounterViewModel::class.java)
}
private val textCount: TextView by lazy { findViewById<TextView>(R.id.text_count) }
private val buttonIncrement: Button by lazy { findViewById<Button>(R.id.button_increment) }
private val buttonDecrement: Button by lazy { findViewById<Button>(R.id.button_decrement) }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
buttonIncrement.setOnClickListener {
counterViewModel.dispatch(IncrementIntent)
}
buttonDecrement.setOnClickListener {
counterViewModel.dispatch(DecrementIntent)
}
counterViewModel.state.observe(this, Observer { state ->
textCount.text = "count: ${state.count}"
})
}
}
The start point of changing state is OwlViewModel#dispatch
. When we dispatch Intent
, The intent is converted to Action
in OwlViewModel#intentToAction
and reach to OwlViewModel#reducer
where state actually changed.
All state change is notified to OwlViewModel#state
livedata, So we can observe it.
Owl has simple rule for its classes.
Intent
is the sealed class that indicates how we want to change the State
. We should not consider current State
when we dispatch Intent
.
Action
is the sealed class that have the data we actually want to apply to State
. We can access to Intent
dispatched and current State
when we create Action
.
State
is just a data class that contains view state. We should not write any logic in it.
Owl has Processor
class for coroutine. To connect processor with ViewModel, pass Processor
to OwlViewModel
constructor.
class CounterViewModel : OwlViewModel<CounterIntent, CounterAction, CounterState>(
initialState = CounterState(),
processor = CounterProcessor()
) {
...
}
Then OwlViewModel
notify Processor
when Action
dispatched. Processor itself is coroutine. So we can launch coroutine to do async programing. After we finish, we can notify Action
to OwlViewModel
by calling Processor#put
.
class CounterProcessor : Processor<CounterAction>() {
private fun processDelayedIncrementAction(action: DelayedIncrementAction) = launch {
delay(1000)
put(UpdateCountAction(action.count))
}
override fun processAction(action: CounterAction) {
when (action) {
is DelayedIncrementAction -> processDelayedIncrementAction(action)
}
}
}
Because OwlViewModel
is fully independent from Processor
, we don't have to consider about async job status in OwlViewModel
. All OwlViewModel
have to do is processing Action
in reducer
synchronously.
dependencies {
implementation "team.itome.owl:${latest_version}"
}
Pull requests are welcome! We'd love help improving this library. Feel free to browse through open issues to look for things that need work. If you have a feature request or bug, please open a new issue so we can track it.