This small library is created to help reduce the boilerplate involved in using Model-View-Intent
This library of base components takes inspiration from Benoît Quenaudon’s excelent talk at Droidcon NYC 2017 and the Hannes Dorfmann’s series.
Small example - Most basic single page score counting application (more details about how it works in here
Large(ish) example - Multipage application with many dependencies published to the appstore.
- Simplicity
- Confirguration changes are handled
- Extremely easy to test
- Increased seperation of concerns
- Increase code reusability
- Explicit state
- Code that is easy to reaad
This little readme isn't enough to cover what mvi is. But in a nutshell it is a pattern that helps create a reactive functional architecture. There is lots of great articles but I recomend starting here
A full example can be found in the app folder of this repository. Below is a simple example of the Activity, ViewModel and Reducer
class MainActivity : KontentActivity<MainIntent, MainViewState>(), Injectable {
@Inject
lateinit var viewModelFactory: ViewModelProvider.Factory
private lateinit var progressDialog: ProgressDialog
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
progressDialog = ProgressDialog(this)
val viewModel = ViewModelProviders.of(this, viewModelFactory).get(MainViewModel::class.java)
super.setup(viewModel, { it.printStackTrace() })
super.attachIntents(intents())
}
private fun intents() = Observable.merge(incrementTeamA(), incrementTeamB(), initialIntent())
private fun initialIntent(): Observable<MainIntent> = Observable.just(MainIntent.LoadPreviousScore())
private fun incrementTeamA(): Observable<MainIntent> = RxView.clicks(teamAButton)
.map { MainIntent.IncrementTeamA() }
private fun incrementTeamB(): Observable<MainIntent> = RxView.clicks(teamBButton)
.map { MainIntent.IncrementTeamB() }
override fun render(state: MainViewState) {
teamAScore.text = state.teamAScore.toString()
teamBScore.text = state.teamBScore.toString()
if (state.loading) progressDialog.show()
else progressDialog.hide()
}
}
class MainViewModel @Inject constructor(scoreRepository: IScoreRepository) : KontentAndroidViewModel<MainIntent, MainActions, MainResults, MainViewState>(
intentToAction = { intent -> intentToAction(intent) },
actionProcessor = actionProcessor(scoreRepository),
reducer = reducer,
defaultState = MainViewState(),
initialIntentPredicate = { intent -> intent is MainIntent.LoadPreviousScore }
)
val reducer = KontentReducer<MainResults, MainViewState>({ result, previousState ->
when (result) {
is MainResults.IncrementTeamA -> previousState.copy(teamAScore = previousState.teamAScore + 1)
is MainResults.IncrementTeamB -> previousState.copy(teamBScore = previousState.teamBScore + 1)
is MainResults.LoadPreviousScoreLoading -> previousState.copy(loading = true, error = null)
is MainResults.LoadPreviousScoreError -> previousState.copy(loading = false, error = result.error)
is MainResults.LoadPreviousScoreSuccess -> previousState.copy(loading = false, error = null, teamAScore = result.teamAScore, teamBScore = result.teamBScore)
}
})
ViewModelFactory is injected into view, the viewmodel is then initialized from that factory (this is so they can be injected with dependencies, you can use default factory if no the ViewModel has no dependencies) Check here for more info.
when you call super.attachIntents(intent()) this is providing the list of actions you want to perform. Do start sending intents to the view model we cal super.attachIntent(intents()) (this must be called after super.setup(viewModel) so that the view model is attached to the view)
this is a function that converts the intents to actions, this adds a layer of abstraction so the ViewModel can be reused if nescessary
this is a function that processes the actions and does something, this could be going off to make a network reuqest or performing some kind of validation, this then produces a result
this is a function that takes the result, combines it with a stored previous state and emits a new view state
this is the starting state of the view, usually not loading, no models and no errors etc.
This is a function that checks to see if the intent was the initial intent sent. Not all views with have an initial intent. But most pages that load data on opening will have an Initial Intent of some kind (see initialIntent() function in the example above)
The view has one overriden function that is render. This takes a view state and works out how to presenter it to the user.
Master ActionProcess - Used to map actions to the action processor for the type of action.
This library is heavily dependant on the RxJava 2, Kotlin and Android's ViewModel.
This library uses inheritence, I would suggest adding an extra abstraction between kontent
classes and your implementations with something like a BaseActivity
that extends KontentActivty
.