kontent
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.
Example and Usage
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.
What this library gives you
- Simplicity
- Confirguration changes are handled
- Extremely easy to test
- Increased seperation of concerns
- Increase code reusability
- Explicit state
- Code that is easy to reaad
What is MVI
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
Usage
A full example can be found in the app folder of this repository. Below is a simple example of the Activity, ViewModel and Reducer
MainActivity
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()
}
}
ViewModel
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 }
)
Reducer
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)
}
})
Other compoments
Things of note here
Dagger
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.
Intents
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)
Intent to Action (Intent Interpretor in the diagram above)
this is a function that converts the intents to actions, this adds a layer of abstraction so the ViewModel can be reused if nescessary
Action Processor (Processor in the diagram above)
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
Reducer
this is a function that takes the result, combines it with a stored previous state and emits a new view state
Default State
this is the starting state of the view, usually not loading, no models and no errors etc.
initial intent predicate
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)
Render
The view has one overriden function that is render. This takes a view state and works out how to presenter it to the user.
Base Classes
Master ActionProcess - Used to map actions to the action processor for the type of action.
Fair Warning
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
.