The document describes an implementation of the Model-View-Intent (MVI) architecture pattern for Android applications. It explains the key components of MVI - the Intent, Model, and View. The Intent interprets user interactions and outputs actions. The Model manages application state from actions and outputs state changes. The View renders state. It then provides code examples of an Android drawing app implemented using MVI, including classes for the Intent, Model, View, and drawing state data.
1. Model View Intent on
Android
Cody Engel
Senior Android Engineer @ Yello
2. What is Model View Intent?
• Coined by André Staltz, Cycle.js creator
• Split main into multiple parts
• Similar idea to MVP, MVVM, MVC
3. Why Am I Here?
• Let’s Try Model-View-Intent With Android
goo.gl/3fPSWV
• Flax For Android - Reactive Concept
goo.gl/1JjSpN
• Hello Flax - A Reactive Architecture For
Android
goo.gl/rD2ZVw
4. The Intent
• Interpret user interactions
• Input: a view which exposes streams of
events
• Output: stream of actions
5. The Model
• Manage the state of the application
• Input: stream of actions
• Output: stream of state
6. The View
• Visually represent state from the Model
• Input: stream of state
• Output: stream of events
11. interface DrawingIntent {
fun getTouches(): Observable<DrawingPoint>
fun getColorClicks(): Observable<Boolean>
fun getResetClicks(): Observable<Boolean>
}
Emits DrawingPoint
Whenever Touch
Event TriggeredEmits Boolean
Whenever Color Is
ClickedEmits Boolean
Whenever Reset Is
Clicked
12. class DrawingIntentImpl(private val drawingView: DrawingView): DrawingIntent {
override fun getTouches(): Observable<DrawingPoint> {
return drawingView.getMotionEvents()
.filter { it.action != MotionEvent.ACTION_UP }
.map { DrawingPoint(it.x, it.y, it.size, it.eventTime) }
}
override fun getColorClicks(): Observable<Boolean> = drawingView.getChangeColorClicks().map { true }
override fun getResetClicks(): Observable<Boolean> = drawingView.getResetClicks().map { true }
}
Maps Reset Clicks
To Boolean
Click Events Come
From DrawingView
Maps Motion Events
To DrawingPoint
Only Emit When
MotionEvent Is Not
ACTION_UP
Emits A New
DrawingPoint
Intent Takes A
DrawingView
13. class DrawingModel(drawingIntent: DrawingIntent) {
private val randomColor = RandomColor()
private val collectedDrawingPoints: ArrayList<DrawingPoint> = ArrayList()
private val defaultColor = "#607D8B"
private var previousState = DrawingState(emptyList(), defaultColor, false)
private val subject: Subject<DrawingState> = ReplaySubject.create()
init {
Observable.merge(
transformTouchesToState(drawingIntent.getTouches()),
transformColorClicksToState(drawingIntent.getColorClicks()),
transformResetClicksToState(drawingIntent.getResetClicks())
).subscribe {
if (previousState != it) {
previousState = it
subject.onNext(it)
}
}
}
fun getObservable(): Observable<DrawingState> = subject
private fun transformColorClicksToState(clicks: Observable<Boolean>): Observable<DrawingState> {
// Transforms Color Clicks Into DrawingState
}
private fun transformResetClicksToState(clicks: Observable<Boolean>): Observable<DrawingState> {
// Transforms Reset Clicks Into DrawingState
}
private fun transformTouchesToState(touches: Observable<DrawingPoint>): Observable<DrawingState> {
// Transforms Touch Events Into DrawingState
}
}
Model Takes A
DrawingIntent
Transform Intent
Actions To State
Transformations
Return DrawingState
Handles Emission Of
New State
Our Activity
Subscribes To This
14. data class DrawingState(
val drawingPoints: List<DrawingPoint>,
val drawingColor: String,
val redraw: Boolean
)
Collection Of
DrawingPoints To Draw
What Color Our Line
Should Be
Whether Or Not We Need
To Redraw The Drawing
15. private fun transformColorClicksToState(clicks: Observable<Boolean>): Observable<DrawingState> {
return clicks.map {
DrawingState(collectedDrawingPoints, randomColor.get(previousState.drawingColor), true)
}
}
private fun transformResetClicksToState(clicks: Observable<Boolean>): Observable<DrawingState> {
return clicks.map {
collectedDrawingPoints.clear()
DrawingState(collectedDrawingPoints, defaultColor, true)
}
}
Emits New Drawing State
With Random Color
Emits New DrawingState
With No DrawingPoints
16. private fun transformTouchesToState(touches: Observable<DrawingPoint>): Observable<DrawingState> {
return touches.map {
var emitState = previousState
collectedDrawingPoints.add(it)
if (collectedDrawingPoints.size >= 2) {
val currentDrawingPoint = collectedDrawingPoints.get(collectedDrawingPoints.size - 1)
val previousDrawingPoint = collectedDrawingPoints.get(collectedDrawingPoints.size - 2)
emitState = previousState.copy(
drawingPoints = listOf(previousDrawingPoint, currentDrawingPoint),
redraw = false
)
}
emitState
}
}
Mutates Our Previous
State To New State
Return Our
emitState
Only Emit New State If
More Than Two
DrawingPoints Exist
17. interface DrawingView {
fun getMotionEvents(): Observable<MotionEvent>
fun getChangeColorClicks(): Observable<Any>
fun getResetClicks(): Observable<Any>
}
Emits MotionEvent
Whenever User
Touches ScreenEmits Anytime The
User Clicks Change
Color
Emits Anytime The
User Clicks Reset
18. class DrawingViewImpl(context: Context?, attrs: AttributeSet?, defStyleAttr: Int, defStyleRes: Int) :
LinearLayout(context, attrs, defStyleAttr, defStyleRes), DrawingView {
private val paint = Paint()
private val strokeWidth = 10f
private var cachedBitmap: Bitmap? = null
private val changeColorButton: Button
private val resetButton: Button
override fun getMotionEvents(): Observable<MotionEvent> = RxView.touches(this)
override fun getChangeColorClicks(): Observable<Any> = RxView.clicks(changeColorButton)
override fun getResetClicks(): Observable<Any> = RxView.clicks(resetButton)
fun render(drawingState: DrawingState) {
// Renders Our View Based On Drawing State
}
private fun generateDrawingPath(drawingPoints: List<DrawingPoint>): Path {
// Generates The Drawing Path, Called By Render
}
}
Maps To User
Interactions
Renders Our
ViewState
Helper Used To
Generate
Drawing Path
19. fun render(drawingState: DrawingState) {
if (!isAttachedToWindow) return
if (cachedBitmap == null || drawingState.redraw) {
cachedBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
}
paint.color = Color.parseColor(drawingState.drawingColor)
val drawingCanvas = Canvas(cachedBitmap)
drawingCanvas.drawPath(generateDrawingPath(drawingState.drawingPoints), paint)
background = BitmapDrawable(resources, cachedBitmap)
}
New Bitmap When
We Need To Redraw
Update The Color Of
Our Drawing Line
Draw Our Line Based On
generateDrawingPath
20. private fun generateDrawingPath(drawingPoints: List<DrawingPoint>): Path {
val drawingPath = Path()
var previousPoint: DrawingPoint? = null
for (currentPoint in drawingPoints) {
previousPoint?.let {
if (currentPoint.time - it.time < 25) {
drawingPath.quadTo(it.x, it.y, currentPoint.x, currentPoint.y)
} else {
drawingPath.moveTo(currentPoint.x, currentPoint.y)
}
} ?: drawingPath.moveTo(currentPoint.x, currentPoint.y)
previousPoint = currentPoint
}
return drawingPath
}
Iterates Over The
DrawingPoints To
Generate A Path