$30 off During Our Annual Pro Sale. View Details »

Unidirectional Data Flow on Android

Unidirectional Data Flow on Android

The introduction of Android Architecture Components made things are easier for the less experienced developers. However, In order to build robust, testable and scalable applications we need more. That's where the Unidirectional Data Flow architecture comes in, borrowed from Flex and Redux on the web.

The approach is based on the idea of an immutable state that represents the state of our app. All the components are decoupled from each other, and they work together taking advantage of Kotlin's language features. We solve the asynchrony problem using Coroutines, specifically Channels and Actors.

Do you want to know more? This talk will guide you through our implementation of this paradigm, with clear steps and a detailed explanation that will help you understand its value and spark your curiosity!

David González

April 05, 2019
Tweet

More Decks by David González

Other Decks in Programming

Transcript

  1. Unidirectional Data Flow on Android
    David González
    @dggonzalez
    [email protected]
    Droidcon Italy 2019

    View Slide

  2. Credit when credit is due…
    Fernando Cejas Cesar Valiente Fabio Collini

    View Slide

  3. View ViewModel
    Store
    Reducer
    Action
    State
    send
    interpret
    dispatch
    notify

    View Slide

  4. View ViewModel
    Action

    View Slide

  5. sealed class OrdersViewAction {
    object NavigateToChatScreen : OrdersViewAction()
    data class SelectAddress(val orderId: String) : OrdersViewAction()
    data class CancelOrder(val orderId: String) : OrdersViewAction()
    data class TrackShipment(val url: String) : OrdersViewAction()
    }

    View Slide

  6. open class MainViewModel : ViewModel() {
    val store: Store = Store()
    fun observe(owner: LifecycleOwner, stateObserver: (ViewState) -> Unit) =
    store.stateLiveData.observe(owner, Observer {
    observer(it!!)
    })
    fun interpret(action: ViewAction)
    override fun onCleared() {
    store.cancel()
    }
    }

    View Slide

  7. open class MainViewModel : ViewModel() {
    val store: Store = Store()
    fun observe(owner: LifecycleOwner,stateObserver: (ViewState) -> Unit) =
    store.stateLiveData.observe(owner, Observer {
    observer(it!!)
    })
    fun interpret(action: ViewAction)
    override fun onCleared() {
    store.cancel()
    }
    }

    View Slide

  8. open class MainViewModel : ViewModel() {
    val store: Store = Store()
    fun observe(owner: LifecycleOwner, stateObserver: (ViewState) -> Unit) =
    store.stateLiveData.observe(owner, Observer {
    observer(it!!)
    })
    fun interpret(action: ViewAction)
    override fun onCleared() {
    store.cancel()
    }
    }

    View Slide

  9. val store: Store = Store()
    fun observe(owner: LifecycleOwner, stateObserver: (ViewState) -> Unit) =
    store.stateLiveData.observe(owner, Observer {
    observer(it!!)
    })
    fun interpret(action: ViewAction)
    override fun onCleared() {
    store.cancel()
    }
    }

    View Slide

  10. store.stateLiveData.observe(owner, Observer {
    observer(it!!)
    })
    fun interpret(action: ViewAction)
    override fun onCleared() {
    store.cancel()
    }
    }

    View Slide

  11. ViewModel
    Store
    Reducer
    Action
    State

    View Slide

  12. class Store(initialState: ViewState = ViewState.Idle){
    val stateLiveData = MutableLiveData()
    .apply {
    value = initialState
    }
    @MainThread
    fun dispatchState(state: ViewState) {
    stateLiveData.value = state
    }
    }

    View Slide

  13. class Store(initialState: ViewState = ViewState.Idle){
    val stateLiveData = MutableLiveData()
    .apply {
    value = initialState
    }
    @MainThread
    fun dispatchState(state: ViewState) {
    stateLiveData.value = state
    }
    }

    View Slide

  14. class Store(initialState: ViewState = ViewState.Idle){
    val stateLiveData = MutableLiveData()
    .apply {
    value = initialState
    }
    @MainThread
    fun dispatchState(state: ViewState) {
    stateLiveData.value = state
    }
    }

    View Slide

  15. class Store(initialState: ViewState = ViewState.Idle) : CoroutineScope {
    private val job = Job()
    fun cancel() = job.cancel()
    override val coroutineContext: CoroutineContext = job + Dispatchers.IO
    private fun state() = stateLiveData.value!!
    fun dispatchState(f: suspend (ViewState) -> State) {
    launch {
    val action = f(state())
    withContext(Dispatchers.Main) {
    dispatchState(action(state()))
    }

    View Slide

  16. class Store(initialState: ViewState = ViewState.Idle) : CoroutineScope {
    private val job = Job()
    fun cancel() = job.cancel()
    override val coroutineContext: CoroutineContext = job + Dispatchers.IO
    private fun state() = stateLiveData.value!!
    fun dispatchState(f: suspend (ViewState) -> State) {
    launch {
    val action = f(state())
    withContext(Dispatchers.Main) {
    dispatchState(action(state()))
    }

    View Slide

  17. class Store(initialState: ViewState = ViewState.Idle) : CoroutineScope {
    private val job = Job()
    fun cancel() = job.cancel()
    override val coroutineContext: CoroutineContext = job + Dispatchers.IO
    private fun state() = stateLiveData.value!!
    fun dispatchState(f: suspend (ViewState) -> State) {
    launch {
    val action = f(state())
    withContext(Dispatchers.Main) {
    dispatchState(action(state()))
    }

    View Slide

  18. class Store(initialState: ViewState = ViewState.Idle) : CoroutineScope {
    private val job = Job()
    fun cancel() = job.cancel()
    override val coroutineContext: CoroutineContext = job + Dispatchers.IO
    private fun state() = stateLiveData.value!!
    fun dispatchState(f: suspend (ViewState) -> State) {
    launch {
    val reducer = f(state())
    withContext(Dispatchers.Main) {
    dispatchState(reducer(state()))
    }
    }

    View Slide

  19. private val job = Job()
    fun cancel() = job.cancel()
    override val coroutineContext: CoroutineContext = job + Dispatchers.IO
    private fun state() = stateLiveData.value!!
    fun dispatchState(f: suspend (ViewState) -> State) {
    launch {
    val reducer = f(state())
    withContext(Dispatchers.Main) {
    dispatchState(reducer(state()))
    }
    }
    }
    }

    View Slide

  20. fun cancel() = job.cancel()
    override val coroutineContext: CoroutineContext = job + Dispatchers.IO
    private fun state() = stateLiveData.value!!
    fun dispatchState(f: suspend (ViewState) -> State) {
    launch {
    val reducer = f(state())
    withContext(Dispatchers.Main) {
    dispatchState(reducer(state()))
    }
    }
    }
    }

    View Slide

  21. private fun state() = stateLiveData.value!!
    fun dispatchState(f: suspend (ViewState) -> State) {
    launch {
    val reducer = f(state())
    withContext(Dispatchers.Main) {
    dispatchState(reducer(state()))
    }
    }
    }
    }

    View Slide

  22. private fun state() = stateLiveData.value!!
    fun dispatchState(f: suspend (ViewState) -> State) {
    launch {
    val reducer = f(state())
    withContext(Dispatchers.Main) {
    dispatchState(reducer(state()))
    }
    }
    }
    }

    View Slide

  23. class PitchReducer
    @Inject constructor(val testimonialsRepo: TestimonialsRepository,
    val pricingRepo: PricingRepository){
    operator fun invoke() = loadPitch()
    private fun loadPitch(): State {
    val testimonials = testimonialsRepo.all()
    val pricing = pricingRepo.current()
    return State {
    Pitch(testimonials, pricing)
    }
    }
    }

    View Slide

  24. class PitchReducer
    @Inject constructor(val testimonialsRepo: TestimonialsRepository,
    val pricingRepo: PricingRepository){
    operator fun invoke() = loadPitch()
    private fun loadPitch(): State {
    val testimonials = testimonialsRepo.all()
    val pricing = pricingRepo.current()
    return State {
    Pitch(testimonials, pricing)
    }
    }
    }

    View Slide

  25. class PitchReducer
    @Inject constructor(val testimonialsRepo: TestimonialsRepository,
    val pricingRepo: PricingRepository){
    operator fun invoke() = loadPitch()
    private fun loadPitch(): State {
    val testimonials = testimonialsRepo.all()
    val pricing = pricingRepo.current()
    return State {
    Pitch(testimonials, pricing)
    }
    }
    }

    View Slide

  26. class PitchReducer
    @Inject constructor(val testimonialsRepo: TestimonialsRepository,
    val pricingRepo: PricingRepository){
    operator fun invoke() = loadPitch()
    private fun loadPitch(): State {
    val testimonials = testimonialsRepo.all()
    val pricing = pricingRepo.current()
    return State {
    Pitch(testimonials, pricing)
    }
    }
    }

    View Slide

  27. private fun initViewModel() {
    viewModel=ViewModelProviders.of(this,vmf)[…]
    viewModel.observe(this, ::renderViewState)
    }
    override fun renderViewState(viewState: ViewState) {
    when (viewState) {
    is Pitch -> renderPitch(viewState)
    }
    }
    PitchActivity

    View Slide

  28. class PitchViewModel
    @Inject constructor(private val reducer: PitchReducer): MainViewModel(){
    fun interpret(action: ViewAction) {
    return when (action) {
    is LoadPitch -> store.dispatchState(reducer())
    else -> throw IncompatibleActionException()
    }
    }
    PitchViewModel

    View Slide

  29. class PitchViewModel
    @Inject constructor(private val reducer: PitchReducer): MainViewModel(){
    fun interpret(action: ViewAction) {
    return when (action) {
    is LoadPitch -> store.dispatchState(reducer())
    else -> throw IncompatibleActionException()
    }
    }
    PitchViewModel

    View Slide

  30. class PitchViewModel
    @Inject constructor(private val reducer: PitchReducer): MainViewModel(){
    fun interpret(action: ViewAction) {
    return when (action) {
    is LoadPitch -> store.dispatchState(reducer())
    else -> throw IncompatibleActionException()
    }
    }
    PitchViewModel

    View Slide

  31. class PitchViewModel
    @Inject constructor(private val reducer: PitchReducer): MainViewModel(){
    fun interpret(action: ViewAction) {
    return when (action) {
    is LoadPitch -> store.dispatchState(reducer())
    else -> throw IncompatibleActionException()
    }
    }
    PitchViewModel

    View Slide

  32. class PitchViewModel
    @Inject constructor(private val reducer: PitchReducer): MainViewModel(){
    fun interpret(action: ViewAction) {
    return when (action) {
    is LoadPitch -> store.dispatchState(reducer())
    else -> throw IncompatibleActionException()
    }
    }
    PitchViewModel

    View Slide

  33. Problems going forward
    How do you deal with a more complicated ViewState?
    How do you deal with an error?
    How do you deal with multiple errors that must be displayed in screen?

    View Slide

  34. data class
    Address(val order: Order = Order.empty(),
    val wrongCity: Boolean,
    val wrongCountry: Boolean,
    val wrongAddress: Boolean,
    val isLoading: Boolean,
    val isCompleted: Boolean,
    val exception: Exception) : ViewState()
    }
    ViewState

    View Slide

  35. private fun render(viewState: Address) {
    if (viewState.isCompleted){
    finish()
    else {
    progress.isVisible = viewState.isLoading
    viewState.exception?.let{
    renderException(it)
    }
    cityError.isVisible = viewState.wrongCity
    countryError.isVisible = viewState.wrongCountry
    addressError.isVisible = viewState.wrongAddress
    renderOrder(order)
    }
    }
    Activity

    View Slide

  36. private fun render(viewState: Address) {
    if (viewState.isCompleted){
    renderCompleted()
    else {
    progress.isVisible = viewState.isLoading
    viewState.exception?.let{
    renderException(it)
    }
    cityError.isVisible = viewState.wrongCity
    countryError.isVisible = viewState.wrongCountry
    addressError.isVisible = viewState.wrongAddress
    renderOrder(order)
    }
    }
    Activity

    View Slide

  37. private fun render(viewState: Address) {
    if (viewState.isCompleted){
    renderCompleted()
    else {
    progress.isVisible = viewState.isLoading
    viewState.exception?.let{
    renderException(it)
    }
    cityError.isVisible = viewState.wrongCity
    countryError.isVisible = viewState.wrongCountry
    addressError.isVisible = viewState.wrongAddress
    renderOrder(order)
    }
    }

    View Slide

  38. private fun render(viewState: Address) {
    if (viewState.isCompleted){
    renderCompleted()
    else {
    progress.isVisible = viewState.isLoading
    viewState.exception?.let{
    renderException(it)
    }
    cityError.isVisible = viewState.wrongCity
    countryError.isVisible = viewState.wrongCountry
    addressError.isVisible = viewState.wrongAddress
    renderOrder(order)
    }
    }

    View Slide

  39. renderCompleted()
    else {
    progress.isVisible = viewState.isLoading
    viewState.exception?.let{
    renderException(it)
    }
    cityError.isVisible = viewState.wrongCity
    countryError.isVisible = viewState.wrongCountry
    addressError.isVisible = viewState.wrongAddress
    renderOrder(order)
    }
    }

    View Slide

  40. progress.isVisible = viewState.isLoading
    viewState.exception?.let{
    renderException(it)
    }
    cityError.isVisible = viewState.wrongCity
    countryError.isVisible = viewState.wrongCountry
    addressError.isVisible = viewState.wrongAddress
    renderOrder(order)
    }
    }

    View Slide

  41. sealed class ChangeAddressViewAction {
    object TalkToUs : ChangeAddressViewAction()
    data class LoadOrder(val orderId: String): ChangeAddressViewAction()
    data class ChangeAddress(
    val orderId: String,
    val shippingAddress: ShippingAddress): ChangeAddressViewAction()
    }
    Action

    View Slide

  42. fun interpret(action: ChangeAddressViewAction) {
    when (action) {
    is LoadOrder -> loadOrder(action.orderId)
    is ChangeAddress -> changeAddress(action)
    is TalkToUs -> navigator.toChat()
    }
    }
    private fun changeAddress(orderId: String, address: ShippingAddress) {
    store.dispatchState { State { Loading } }
    store.dispatchState (changeOrderAddress(orderId, shippingAddress))
    }
    ViewModel

    View Slide

  43. when (action) {
    is LoadOrder -> loadOrder(action.orderId)
    is ChangeAddress -> changeAddress(action)
    is TalkToUs -> navigator.toChat()
    }
    }
    private fun changeAddress(orderId: String, address: ShippingAddress) {
    store.dispatchState { State { Loading } }
    store.dispatchState { changeOrderAddress(orderId, shippingAddress)}
    }

    View Slide

  44. Moar problems…
    Navigation Event vs View State
    ViewModel is calling store twice, once per state

    View Slide

  45. sealed class Either {
    // Represents the left side of Either class
    // which by convention is a "Failure"
    data class Left(val a: L) : Either()
    //Represents the right side of Either class
    //which by convention is a "Success"
    data class Right(val b: R) : Either()
    }
    https://arrow-kt.io/docs/arrow/core/either/

    View Slide

  46. sealed class Failure {
    // App wide failures
    data class ServerError(val error: String) : Failure()
    object NetworkConnection : Failure()
    data class GenericError(val exception: Exception) : Failure()
    // Extend this class for feature specific failures
    abstract class FeatureFailure : Failure()
    }

    View Slide

  47. sealed class Success {
    // Extend this classes for feature specific success
    abstract class ViewState : Success()
    abstract class ViewEvent : Success()
    // App wide success view states
    object Idle : ViewState()
    object Loading : ViewState()
    }

    View Slide

  48. class Store(initialState: ViewState) : CoroutineScope {
    val stateLiveData = MutableLiveData>()
    .apply { value = initialState }
    fun dispatchState(f: suspend (Either)
    -> State>) {
    launch {
    val reducer = f(state())
    withContext(Dispatchers.Main) {
    dispatchState(reducer(state()))
    }

    View Slide

  49. class Store(initialState: ViewState) : CoroutineScope {
    val stateLiveData = MutableLiveData>()
    .apply { value = initialState }
    fun dispatchState(f: suspend (Either)
    -> State>) {
    launch {
    val reducer = f(state())
    withContext(Dispatchers.Main) {
    dispatchState(reducer(state()))
    }

    View Slide

  50. class Store(initialState: ViewState) : CoroutineScope {
    val stateLiveData = MutableLiveData>()
    .apply { value = initialState }
    fun dispatchState(f: suspend (Either)
    -> State>) {
    launch {
    val reducer = f(state())
    withContext(Dispatchers.Main) {
    dispatchState(reducer(state()))
    }

    View Slide

  51. val stateLiveData = MutableLiveData>()
    .apply { value = initialState }
    fun dispatchState(f: suspend (Either)
    -> State>) {
    launch {
    val reducer = f(state())
    withContext(Dispatchers.Main) {
    dispatchState(reducer(state()))
    }

    View Slide

  52. open class MainViewModel : ViewModel() {
    private val successLiveData = LiveData()
    private val failureLiveData = LiveData()
    fun observe(owner: LifecycleOwner,
    successObserver: (Success) -> Unit,
    failureObserver: (Failure) -> Unit) {
    successLiveData.observe(owner,
    Observer { successObserver(it!!) })
    failureLiveData.observe(owner,
    Observer { failureObserver(it) })
    store.observeState(owner) {
    when (it) {

    View Slide

  53. open class MainViewModel : ViewModel() {
    private val successLiveData = LiveData()
    private val failureLiveData = LiveData()
    fun observe(owner: LifecycleOwner,
    successObserver: (Success) -> Unit,
    failureObserver: (Failure) -> Unit) {
    successLiveData.observe(owner,
    Observer { successObserver(it!!) })
    failureLiveData.observe(owner,
    Observer { failureObserver(it) })
    store.observeState(owner) {
    when (it) {

    View Slide

  54. open class MainViewModel : ViewModel() {
    private val successLiveData = LiveData()
    private val failureLiveData = LiveData()
    fun observe(owner: LifecycleOwner,
    successObserver: (Success) -> Unit,
    failureObserver: (Failure) -> Unit) {
    successLiveData.observe(owner,
    Observer { successObserver(it!!) })
    failureLiveData.observe(owner,
    Observer { failureObserver(it) })
    store.observeState(owner) {
    when (it) {
    is Either.Left -> failureLiveData.value = it.a
    is Either.Right -> successLiveData.value = it.b

    View Slide

  55. private val failureLiveData = LiveData()
    fun observe(owner: LifecycleOwner,
    successObserver: (Success) -> Unit,
    failureObserver: (Failure) -> Unit) {
    successLiveData.observe(owner,
    Observer { successObserver(it!!) })
    failureLiveData.observe(owner,
    Observer { failureObserver(it) })
    store.observeState(owner) {
    when (it) {
    is Either.Left -> failureLiveData.value = it.a
    is Either.Right -> successLiveData.value = it.b
    }
    }

    View Slide

  56. failureObserver: (Failure) -> Unit) {
    successLiveData.observe(owner,
    Observer { successObserver(it!!) })
    failureLiveData.observe(owner,
    Observer { failureObserver(it) })
    store.observeState(owner) {
    when (it) {
    is Either.Left -> failureLiveData.value = it.a
    is Either.Right -> successLiveData.value = it.b
    }
    }

    View Slide

  57. data class
    Address(val order: Order = Order.empty(),
    val wrongCity: Boolean,
    val wrongCountry: Boolean,
    val wrongAddress: Boolean,
    val isLoading: Boolean,
    val isCompleted: Boolean,
    val exception: Exception) : ViewState()
    }

    View Slide

  58. data class
    Address(val order: Order = Order.empty()) : Success.ViewState()
    object TalkToUs : Success.ViewEvent()
    object AddressChanged : Success.ViewEvent()
    data class InvalidAddress(val wrongCity: Boolean,
    val wrongCountry: Boolean,
    val wrongAddress: Boolean) : FeatureFailure()

    View Slide

  59. viewModel = ViewModelProviders.of(this,
    viewModelFactory)[MyViewModel::class.java]
    viewModel.observe(this, ::renderSuccess, ::renderFailure)
    protected fun renderSuccess(success: Success) {
    when (success) {
    is Success.ViewEvent -> reactToEvent(success)
    is Success.ViewState -> renderViewState(success)
    }
    }
    override fun renderViewState(viewState: Success.ViewState) {
    when (viewState) {
    Activity

    View Slide

  60. viewModel = ViewModelProviders.of(this,
    viewModelFactory)[MyViewModel::class.java]
    viewModel.observe(this, ::renderSuccess, ::renderFailure)
    protected fun renderSuccess(success: Success) {
    when (success) {
    is Success.ViewEvent -> reactToEvent(success)
    is Success.ViewState -> renderViewState(success)
    }
    }
    override fun renderViewState(viewState: Success.ViewState) {
    when (viewState) {
    is Orders -> renderOrders(viewState)
    }
    }

    View Slide

  61. when (success) {
    is Success.ViewEvent -> reactToEvent(success)
    is Success.ViewState -> renderViewState(success)
    }
    }
    override fun renderViewState(viewState: Success.ViewState) {
    when (viewState) {
    is Orders -> renderOrders(viewState)
    }
    }
    override fun reactToEvent(event: Success.ViewEvent) {
    when (event) {
    is Success.TalkToUs -> navigator.toSupportChat()
    is SelectAddress -> navigator.toChangeAddress(this, event.orderId)
    is OrderCancelled -> displaySnackbar(event.failureMessage)
    is CancelOrder -> navigator.toCancelOrder(this, event.orderId)

    View Slide

  62. override fun renderViewState(viewState: Success.ViewState) {
    when (viewState) {
    is Orders -> renderOrders(viewState)
    }
    }
    override fun reactToEvent(event: Success.ViewEvent) {
    when (event) {
    is Success.TalkToUs -> navigator.toSupportChat()
    is SelectAddress -> navigator.toChangeAddress(this, event.orderId)
    is OrderCancelled -> displaySnackbar(event.failureMessage)
    is CancelOrder -> navigator.toCancelOrder(this, event.orderId)
    is TrackShipment -> navigator.toTrackingShipment(event.url)
    }
    }
    override fun renderFailure(failure: Failure) {
    ordersList.gone()

    View Slide

  63. is Success.TalkToUs -> navigator.toSupportChat()
    is SelectAddress -> navigator.toChangeAddress(this, event.orderId)
    is OrderCancelled -> displaySnackbar(event.failureMessage)
    is CancelOrder -> navigator.toCancelOrder(this, event.orderId)
    is TrackShipment -> navigator.toTrackingShipment(event.url)
    }
    }
    override fun renderFailure(failure: Failure) {
    ordersList.gone()
    loading.gone()
    empty.show()
    }

    View Slide

  64. The final step…

    View Slide

  65. View Slide

  66. public interface SendChannel {
    public suspend fun send(element: E)
    public fun offer(element: E)
    public fun close(cause: Throwable? = null): Boolean
    }
    public interface ReceiveChannel {
    public suspend fun receive(): E
    public fun close(cause: Throwable? = null): Boolean
    }
    Deferred vs Channel

    View Slide

  67. fun main() = runBlocking {
    val channel = Channel()
    launch {
    for (i in 1..5) {
    channel.send(i)
    }
    launch {
    for (value in channel) {
    println("received: $value")
    }
    //console output
    received: 1
    received: 2
    received: 3
    received: 4
    received: 5
    Channel

    View Slide

  68. fun main() = runBlocking {
    val channel = Channel()
    launch {
    for (i in 1..5) {
    channel.send(i)
    }
    launch {
    for (value in channel) {
    println("received: $value")
    }
    //console output
    received: 1
    received: 2
    received: 3
    received: 4
    received: 5
    Channel

    View Slide

  69. fun main() = runBlocking {
    val channel = Channel()
    launch {
    for (i in 1..5) {
    channel.send(i)
    }
    launch {
    for (value in channel) {
    println("received: $value")
    }
    //console output
    received: 1
    received: 2
    received: 3
    received: 4
    received: 5
    Channel

    View Slide

  70. fun main() = runBlocking {
    val channel = Channel()
    launch {
    for (i in 1..5) {
    channel.send(i)
    }
    launch {
    for (value in channel) {
    println("received: $value")
    }
    //console output
    received: 1
    received: 2
    received: 3
    received: 4
    received: 5
    Channel

    View Slide


  71. public fun CoroutineScope.produce(
    context: CoroutineContext = EmptyCoroutineContext,
    capacity: Int = 0,
    block: suspend ProducerScope.() -> Unit
    ): ReceiveChannel
    Producer

    View Slide

  72. val publisher = produce(capacity = 2){
    for (i in 1..5){
    send(i)
    }
    launch {
    publisher.consumeEach {
    println("received: $it”)
    }
    }
    //console output
    received: 1
    received: 2
    received: 3
    received: 4
    received: 5
    Producer

    View Slide

  73. val publisher = produce(capacity = 2){
    for (i in 1..5){
    send(i)
    }
    launch {
    publisher.consumeEach {
    println("received: $it”)
    }
    }
    //console output
    received: 1
    received: 2
    received: 3
    received: 4
    received: 5
    Producer

    View Slide

  74. val publisher = produce(capacity = 2){
    for (i in 1..5){
    send(i)
    }
    launch {
    publisher.consumeEach {
    println("received: $it”)
    }
    }
    //console output
    received: 1
    received: 2
    received: 3
    received: 4
    received: 5
    Producer

    View Slide

  75. val publisher = produce(capacity = 2){
    for (i in 1..5){
    send(i)
    }
    launch {
    publisher.consumeEach {
    println("received: $it”)
    }
    }
    //console output
    received: 1
    received: 2
    received: 3
    received: 4
    received: 5
    Producer

    View Slide

  76. open class Reducer {
    suspend fun ProducerScope>.send(f: T.() -> T)
    = send(State(f))
    fun produceStates(f: suspend ProducerScope>.() -> Unit)
    : ReceiveChannel> =
    GlobalScope.produce(block = f)
    }

    View Slide

  77. open class Reducer {
    suspend fun ProducerScope>.send(f: T.() -> T)
    = send(State(f))
    fun produceStates(f: suspend ProducerScope>.() -> Unit)
    : ReceiveChannel> =
    GlobalScope.produce(block = f)
    }

    View Slide

  78. fun dispatchStates(channel: ReceiveChannel>>) {
    launch {
    channel.consumeEach { reducer ->
    withContext(Dispatchers.Main) {
    dispatchState(reducer(state()))
    }
    }
    }
    }
    fun loadOrders() {
    store.dispatchStates(getOrders())
    }
    Store

    View Slide

  79. fun dispatchStates(channel: ReceiveChannel>>) {
    launch {
    channel.consumeEach { reducer ->
    withContext(Dispatchers.Main) {
    dispatchState(reducer(state()))
    }
    }
    }
    }
    fun loadOrders() {
    store.dispatchStates(getOrders())
    }
    Store

    View Slide

  80. launch {
    channel.consumeEach { action ->
    withContext(Dispatchers.Main) {
    dispatchState(action(state()))
    }
    }
    }
    }
    fun loadOrders() {
    store.dispatchStates(getOrders())
    }

    View Slide

  81. class GetOrders
    @Inject constructor(private val repository: OrdersRepository)
    : Reducer() {
    operator fun invoke() = getOrders()
    private fun getOrders()
    : ReceiveChannel>> = produceActions
    {
    try {
    send { Either.Right(Success.Loading) }
    val orders = repository.allOrders()
    send { Either.Right(Orders(orders)) }
    } catch (e: Exception) {
    GetOrders

    View Slide

  82. @Inject constructor(private val repository: OrdersRepository)
    : Reducer() {
    operator fun invoke() = getOrders()
    private fun getOrders()
    : ReceiveChannel>> = produceStates
    {
    try {
    send { Either.Right(Success.Loading) }
    val orders = repository.allOrders()
    send { Either.Right(Orders(orders)) }
    } catch (e: Exception) {
    send { Either.Left(OrdersFailure(e)) }
    }
    }

    View Slide

  83. private fun getOrders()
    : ReceiveChannel>> = produceStates
    {
    try {
    send { Either.Right(Success.Loading) }
    val orders = repository.allOrders()
    send { Either.Right(Orders(orders)) }
    } catch (e: Exception) {
    send { Either.Left(OrdersFailure(e)) }
    }
    }

    View Slide

  84. {
    try {
    send { Either.Right(Success.Loading) }
    val orders = repository.allOrders()
    send { Either.Right(Orders(orders)) }
    } catch (e: Exception) {
    send { Either.Left(OrdersFailure(e)) }
    }
    }

    View Slide

  85. Testing
    UI tests on Activity / Fragments

    Unit + Integration tests on ViewModels
    Unit + instrumentation tests on Reducers

    View Slide

  86. suspend inline fun ReceiveChannel>.states(initialState: T): List
    {
    return fold(emptyList()) { states, action ->
    states + action(states.lastOrNull() ?: initialState)
    }
    }
    ReducerTest

    View Slide

  87. @Test
    fun `changes state on successful loading`() {
    val expectedState = Orders(listOf(ORDER_1, ORDER_2))
    coEvery { ordersRepository.all() } returns listOf(ORDER_1, ORDER_2)
    val states = runBlocking {
    useCase.invoke().states(Either.Right(Success.Idle))
    }
    assert(states).hasSize(2)
    assert(states[0]).isEqualTo(Either.Right(Success.Loading))
    assert(states[1]).isEqualTo(Either.Right(expectedState))
    }
    ReducerTest

    View Slide

  88. @Test
    fun `changes state on error`() {
    val exception = IOException()
    coEvery { ordersRepository.allOrders() } throws exception
    val states = runBlocking {
    useCase.invoke().states(Either.Right(Success.Idle))
    }
    assert(states).hasSize(2)
    assert(states[0]).isEqualTo(Either.Right(Success.Loading))
    assert(states[1]).isEqualTo(Either.Left(OrdersFailure(exception)))
    }
    ReducerTest

    View Slide

  89. What’s next?
    Explore Data Binding - Watch Jose’s talk
    This API will become obsolete in future updates with
    introduction of lazy asynchronous streams
    (https://github.com/Kotlin/kotlinx.coroutines/issues/254).

    View Slide

  90. View Slide

  91. Thank you!
    David González
    @dggonzalez
    [email protected]

    View Slide

  92. app/build.gradle
    buildscript {
    repositories {
    jcenter()
    }
    dependencies {
    classpath 'com.android.tools.build:gradle:1.5.0'
    classpath 'com.getkeepsafe.dexcount:dexcount-gradle-plugin:0.2.1'
    }
    }
    ../build.gradle
    dependencies {
    compile 'com.android.support:herbs-v13:23.1.1'
    compile 'com.android.support:meat:23.1.1'
    compile ‘com.android.support:cookware-v7:23.1.1'
    compile ‘com.google.android.gms:play-services:8.4.0'
    }

    View Slide

  93. CookingMethod cookingMethod = new CookingMethod.Builder()
    .withFlavour(Flavours.MediumRare)
    .withSide(Sides.Fries)
    .fromButcher(Butchers.GingerPig)
    .build();
    private Steak from(CookingMethod cookingMethod){
    Steak steak = Steak.from(cookingMethod.getButcher());
    steak.addOil(new Oil());
    steak.addSalt(new Salt());
    steak.addPepper(new Pepper());
    return steak;
    }
    Steak.java

    View Slide