The document discusses using a unidirectional data flow architecture on Android applications. It describes combining elements of Flux and Redux, including actions, reducers, a store, and dispatcher. Views dispatch actions that are reduced by reducers to update the store's state. The store then notifies subscribers like the controller-view. The approach aims to have unidirectional data flows with immutable states and no external dependencies. An example app called KUnidirectional is presented to demonstrate this architecture in practice using Kotlin and Android.
18. Flux and Redux
Architectures based in unidirectional data flows.
We start in A and we finish in A.
Global app state. State has everything the App needs.
36. We use a mix of both
We have actions that describe what we want to do.
37. We use a mix of both
We have actions that describe what we want to do.
We have store.
38. We use a mix of both
We have actions that describe what we want to do.
We have store.
We have dispatcher that dispatch actions and new states.
Subscribers are subscribed to it.
39. We use a mix of both
We have actions that describe what we want to do.
We have store.
We have dispatcher that dispatch actions and new states.
Subscribers are subscribed to it.
We have reducers. Given an action return a new state.
40. We use a mix of both
We have actions that describe what we want to do.
We have store.
We have dispatcher that dispatch actions and new states.
Subscribers are subscribed to it.
We have reducers. Given an action return a new state.
We have middleware that extends our model (persistence, network, etc.)
41. We use a mix of both
We have actions that describe what we want to do.
We have store.
We have dispatcher that dispatch actions and new states.
Subscribers are subscribed to it.
We have reducers. Given an action return a new state.
We have middleware that extends our model (persistence, network, etc.)
We have ControllerView (we are in Android though :-) ).
61. KUnidirectional
OSS App created to show this architecture
on Android.
A simple item list.
Everything is unidirectional.
No external libraries, just Kotlin + Android.
64. View
Are Activities and Fragments.
It receives data from the ControllerView and uses it to update it self.
65. View
Are Activities and Fragments.
It receives data from the ControllerView and uses it to update it self.
It communicates with the ControllerView through an interface.
66. View
Are Activities and Fragments.
It receives data from the ControllerView and uses it to update it self.
We instantiate rest of the components here.
It communicates with the ControllerView through an interface.
70. ControllerView
“Man in the middle” between UI and everything else.
Powered presenter.
1 Activity/1Fragment -> 1 Controller-View.
71. ControllerView
“Man in the middle” between UI and everything else.
Powered presenter.
1 Activity/1Fragment -> 1 Controller-View.
Dispatches actions.
72. ControllerView
“Man in the middle” between UI and everything else.
Powered presenter.
1 Activity/1Fragment -> 1 Controller-View.
Dispatches actions.
Subscribes to state changes.
73. abstract class ControllerView(
protected val actionDispatcher: ActionDispatcher,
protected val stateDispatcher: StateDispatcher)
: Application.ActivityLifecycleCallbacks, Subscriber<State> {
74. protected fun <T : Action> dispatch(action: T) {
actionDispatcher.dispatch(action)
}
override fun onNext(data: State) {
if (isActivityRunning) handleState(state)
}
abstract fun handleState(state: State)
}
79. sealed class Action
sealed class UpdateAction : Action() {
data class ReorderItemsAction(val items: List<Item>) : UpdateAction()
data class UpdateItemAction(val localId: String,
val text: String?,
val color: Color) : UpdateAction()
data class UpdateFavoriteAction(
val localId: String, val favorite: Boolean) : UpdateAction()
data class UpdateColorAction(
val localId: String, val color: Color) : UpdateAction()
}
82. Dispatcher
Used to dispatch data. An action or a new state.
Subscribers subscribe to it, listening for a new action or state.
83. Dispatcher
Used to dispatch data. An action or a new state.
Subscribers subscribe to it, listening for a new action or state.
Subscribers can unsubscribe.
91. Store
Is subscribed to handle Actions.
It will use its Reducers to reduce an Action and dispatch a new State.
It has the State of the app.
92. State
enum class Navigation {
ITEMS_LIST,
EDIT_ITEM
}
data class ItemsListScreen(
val items: List<Item> = emptyList())
data class EditItemScreen(val currentItem: Item = Item())
data class State(
val itemsListScreen: ItemsListScreen = ItemsListScreen(),
val editItemScreen: EditItemScreen = EditItemScreen(),
val navigation: Navigation = Navigation.ITEMS_LIST)
State is immutable.
93. Store Action Subscriber
open class StoreActionSubscriber(actionDispatcher: ActionDispatcher,
val stateDispatcher: StateDispatcher)
: ActionSubscriber(actionDispatcher) {
override fun reduce(action: Action) {
val currentState = stateDispatcher.state
val newState = when (action) {
is CreationAction -> CreationReducer.reduce(action, currentState)
is UpdateAction -> UpdateReducer.reduce(action, currentState)
is ReadAction -> ReadReducer.reduce(action, currentState)
is DeleteAction -> DeleteReducer.reduce(action, currentState)
is NavigationAction -> NavigationReducer.reduce(action, currentState)
}
stateDispatcher.dispatch(newState)
}
}
101. abstract class Reducer<in T : Action> {
open fun reduce(action: T, currentState: State) =
with(currentState) {
currentState.copy(
itemsListScreen = reduceItemsListScreen(action, itemsListScreen),
editItemScreen = reduceEditItemScreen(action, editItemScreen),
navigation = reduceNavigation(action, navigation)
)
}
102. open fun reduceItemsListScreen(action: T, itemsListScreen: ItemsListScreen) =
itemsListScreen.copy(
items = reduceItemsCollection(action, itemsListScreen.items))
open fun reduceItemsCollection(action: T, currentItems: List<Item>) =
currentItems.findAndMap(
find = { shouldReduceItem(action, it) },
map = { changeItemFields(action, it) })
103. open fun reduceEditItemScreen(action: T, editItemScreen: EditItemScreen) =
editItemScreen.copy(
currentItem = reduceCurrentItem(
action, editItemScreen.currentItem))
open fun reduceCurrentItem(action: T, currentItem: Item) =
if (shouldReduceItem(action, currentItem))
changeItemFields(action, currentItem)
else currentItem
104. open fun shouldReduceItem(action: T, currentItem: Item) = false
open fun changeItemFields(action: T, currentItem: Item) = currentItem
open fun reduceNavigation(action: T, currentNavigation: Navigation) = currentNavigation
}
109. enum class Color {
RED, YELLOW, GREEN, BLUE, WHITE
}
data class Item(
val localId: String = generateLocalId(),
val text: String? = null,
val favorite: Boolean = false,
val color: Color = Color.WHITE,
val position: Long = object : PositionsFactory {}.newPosition())
Store Models
110. enum class Color {
RED, YELLOW, GREEN, BLUE, WHITE
}
@RealmClass
open class Item() : RealmModel {
constructor(localId: String, text: String?, favorite: Boolean = false,
colorEnum: Color = Color.WHITE, position: Long) : this() {
this.localId = localId
this.text = text
this.favorite = favorite
this.color = colorEnum.name
this.position = position
}
Persistence Models
111. @PrimaryKey open var localId: String = ""
open var text: String? = null
open var favorite: Boolean = false
@Ignore private var colorEnum: Color = Color.WHITE
open var color: String = colorEnum.name
open var position: Long = 0
fun getColorAsEnum(): Color = Color.valueOf(color)
fun setColorAsEnum(color: Color) {
this.color = color.name
}
//Equals and hashcode since Realm doesn’t allow data classes
}
Persistence Models (2)
112. import com.cesarvaliente.kunidirectional.persistence.Color as PersistenceColor
import com.cesarvaliente.kunidirectional.persistence.Item as PersistenceItem
import com.cesarvaliente.kunidirectional.store.Color as StoreColor
import com.cesarvaliente.kunidirectional.store.Item as StoreItem
Mapper
fun StoreItem.toPersistenceItem(): PersistenceItem =
with(this) {
PersistenceItem(localId, text, favorite, color.toPersistenceColor(), position)
}
fun StoreColor.toPersistenceColor(): PersistenceColor =
when (this) {
StoreColor.BLUE -> PersistenceColor.BLUE
StoreColor.GREEN -> PersistenceColor.GREEN
StoreColor.RED -> PersistenceColor.RED
StoreColor.WHITE -> PersistenceColor.WHITE
StoreColor.YELLOW -> PersistenceColor.YELLOW
}
113. Presentation models?
fun Color.toColorResource(): Int =
when (this) {
RED -> R.color.red
YELLOW -> R.color.yellow
GREEN -> R.color.green
BLUE -> R.color.blue
WHITE -> R.color.white
}
fun Item.getStableId(): Long = this.localId.hashCode().toLong()
116. Middleware
We can have as much middleware as we want.
Middleware is subscribed listening for Actions.
117. Middleware
We can have as much middleware as we want.
Middleware is subscribed listening for Actions.
Middleware can return a new Action after “apply” the previous one.
134. Testing?
UI tests on Presentation layer (View).
Unit tests + Integration tests on our ControllerViews.
135. Testing?
UI tests on Presentation layer (View).
Unit tests + Integration tests on our ControllerViews.
Acceptance tests in our Persistence layer (middleware).
136. Testing?
UI tests on Presentation layer (View).
Unit tests + Integration tests on our ControllerViews.
Acceptance tests in our Persistence layer (middleware).
Unit tests in our Store layer.
140. Demo time(1)!
OPEN EDIT ITEM NAVIGATION UPDATED
FETCH ITEM
DETAILS
EMPTY ITEM
FETCH LIST
ITEMS
LIST ITEMS
Action
State
141. Demo time(1)!
OPEN EDIT ITEM NAVIGATION UPDATED
FETCH ITEM
DETAILS
EMPTY ITEM
SAVE ITEM NEW ITEM
FETCH LIST
ITEMS
LIST ITEMS
Action
State
142. Demo time(1)!
OPEN EDIT ITEM NAVIGATION UPDATED
FETCH ITEM
DETAILS
EMPTY ITEM
SAVE ITEM NEW ITEM
TO NOTE LIST NAVIGATION UPDATED
FETCH LIST
ITEMS
LIST ITEMS
Action
State
143. Demo time(1)!
OPEN EDIT ITEM NAVIGATION UPDATED
FETCH ITEM
DETAILS
EMPTY ITEM
SAVE ITEM NEW ITEM
TO NOTE LIST NAVIGATION UPDATED
FETCH LIST
ITEMS
LIST ITEMS
FETCH LIST
ITEMS
LIST ITEMS
Action
State
146. Demo time(2)!
OPEN EDIT ITEM NAVIGATION UPDATED
FETCH ITEM
DETAILS
CURRENT ITEM
Action
State
147. Demo time(2)!
OPEN EDIT ITEM NAVIGATION UPDATED
FETCH ITEM
DETAILS
CURRENT ITEM
CHANGE COLOR CURRENT ITEM x 6
Action
State
148. Demo time(2)!
OPEN EDIT ITEM NAVIGATION UPDATED
FETCH ITEM
DETAILS
CURRENT ITEM
SAVE ITEM ITEM UPDATED
CHANGE COLOR CURRENT ITEM x 6
Action
State
149. Demo time(2)!
OPEN EDIT ITEM NAVIGATION UPDATED
FETCH ITEM
DETAILS
CURRENT ITEM
SAVE ITEM ITEM UPDATED
TO NOTE LIST NAVIGATION UPDATED
CHANGE COLOR CURRENT ITEM x 6
Action
State
150. Demo time(2)!
OPEN EDIT ITEM NAVIGATION UPDATED
FETCH ITEM
DETAILS
CURRENT ITEM
SAVE ITEM ITEM UPDATED
TO NOTE LIST NAVIGATION UPDATED
FETCH LIST
ITEMS
LIST ITEMS
CHANGE COLOR CURRENT ITEM x 6
Action
State
162. License
(cc) 2017 César Valiente.
Some rights reserved.
This document is distributed under the Creative
Commons Attribution-ShareAlike 3.0 license, available in
http://creativecommons.org/licenses/by-sa/3.0/
163. Image licenses
Flux and Redux images are property of Facebook.
Emojis by Emoji One (CC-BY): http://emojione.com/