Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Solving all problems with reactive streams

pakoito
April 20, 2018

Solving all problems with reactive streams

This session takes a deep dive into several real-world rewrites of large features in a popular app using exclusively reactive streams, from the view all the way to the network!

Fully Reactive Apps: https://speakerdeck.com/pakoito/fully-reactive-apps

pakoito

April 20, 2018
Tweet

More Decks by pakoito

Other Decks in Technology

Transcript

  1. @pacoworks
    Solving all problems with reactive
    streams
    Paco Estevez - 2018
    1

    View Slide

  2. @pacoworks
    Key Takeaways
    Take business requirements and convert them into a
    reactive domain model
    Create your own solutions to general problems
    Apply those solution to specific use cases
    Write code for maintainability
    2

    View Slide

  3. @pacoworks
    3
    Use case: Song Picker

    View Slide

  4. 4

    View Slide

  5. 5

    View Slide

  6. @pacoworks
    Requirements
    Select any number of songs
    Play previews inline
    Filters by genre
    Do not display songs on soundtrack
    6

    View Slide

  7. @pacoworks
    7
    Modeling the data

    View Slide

  8. @pacoworks
    Making data simple
    Everything is plain data
    Data values aggregate and change over time
    Provide sensible default values
    8

    View Slide

  9. @pacoworks
    Information breakdown
    Select any
    number of
    songs
    Play previews
    inline
    Filters by
    genre
    Do not display
    songs already
    on soundtrack
    9
    List of songs
    Set of selected songs
    Play information: total and currently
    played
    List of filters
    Currently selected filter
    List of soundtrack songs

    View Slide

  10. @pacoworks
    Model
    10
    List of songs
    List of selected
    songs
    Play information:
    total and
    currently played
    List of filters
    Currently
    selected filter
    List of
    soundtrack songs
    typealias Genre = String
    data class Song(val genre: Genre, val url: String, ...)
    data class Beat(val current: Int, val total: Int)
    data class SoundtrackElement(val song: Song, ...)

    View Slide

  11. @pacoworks
    State
    11
    Select any
    number of
    songs
    Play previews
    inline
    Filters by
    genre
    Do not display
    songs already
    on soundtrack
    val songList: BehaviorSubject> =
    BehaviorSubject.create(emptyList())
    val songSelected: BehaviorSubject> =
    BehaviorSubject.create(emptySet())
    val filterList: BehaviorSubject> =
    BehaviorSubject.create(emptyList())
    val filterSelected: BehaviorSubject> =
    BehaviorSubject.create(emptySet())
    val soundtrack: BehaviorSubject> =
    BehaviorSubject.create(emptyList())
    Say bye bye to songs on soundtrack for
    now
    Notice there is no Beat state. Beats are transient data.

    View Slide

  12. @pacoworks
    12
    Displaying the information
    to the user

    View Slide

  13. @pacoworks
    Dumb passive views
    Receive plain data from the state
    Keep dumb copies of the state for internal use
    13

    View Slide

  14. @pacoworks
    View display
    Select
    any
    number of
    songs
    Play
    previews
    inline
    Filters
    by genre
    14
    interface MusicViewDisplay {
    fun showSongs(songs: List)
    fun showSelected(selected: Set)
    fun showBeat(beat: Tuple2)
    fun clearBeat()
    fun showGenres(genres: List)
    fun showVisible(visible: Set)
    }

    View Slide

  15. @pacoworks
    Binding State to UI
    fun bind(lifecycle: Observable,
    mainThreadScheduler: Scheduler,
    state: Observable,
    viewAction: (T) -> Unit) =
    lifecycle
    .filter(is(CREATE))
    .switchMap(state)
    .observeOn(mainThreadScheduler)
    .takeUntil(
    lifecycle
    .filter(is(DESTROY)))
    .subscribe(viewAction)
    15
    No leaky UI!
    Bind with
    lifecycle
    assurance
    Add main thread
    too
    Observable
    subscribed with
    (T) -> Unit

    View Slide

  16. @pacoworks
    View display
    Select
    any
    number of
    songs
    Play
    previews
    inline
    Filters
    by genre
    16
    val bindSimple =
    ::bind.apply(
    lifecycle, AndroidSchedulers.mainThread())
    bindSimple(songList, ::showSongs)
    bindSimple(songSelected, ::showSelected)
    bindSimple(filterList, ::showGenres)
    bindSimple(filterSelected, ::showVisible)
    Beats have no permanent state -> no
    binding!

    View Slide

  17. @pacoworks
    View adapter
    Select
    any
    number of
    songs
    Play
    previews
    inline
    Filters
    by genre
    17
    class MusicAdapter: RecyclerView.Adapter {
    val songs: List
    val selected: Set
    val visible: Set
    val beat: AtomicReference>
    /*...*/
    }
    class MusicFilterSpinner: BaseAdapter {
    val genres: List
    /*...*/
    }

    View Slide

  18. @pacoworks
    View adapter
    Select
    any
    number of
    songs
    Play
    previews
    inline
    Filters
    by genre
    18
    class MusicAdapter: RecyclerView.Adapter {
    val selected: Set
    val visible: Set
    val beat: AtomicReference>
    fun getItemViewType(pos: Int, element: Song): Int =
    if visible.contains(element) SHOW else HIDDEN
    fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VH =
    if (viewType == HIDDEN) HiddenSongViewHolder(parent.context)
    else SongViewHolder(/*...*/)
    fun onBindViewHolder(holder: VH, pos: Int) {
    val position = songs.get(pos);
    if (selected.contains(position)) { /*...*/ }
    val beatC = beat.get()
    if (beatC != null && beatC.b == position) { /*...*/ }
    }
    }

    View Slide

  19. @pacoworks
    19
    Interacting with services

    View Slide

  20. @pacoworks
    Providing information
    Identify all information sources
    Different model from the data layer
    Write abstractions that can be reusable
    20

    View Slide

  21. @pacoworks
    Information sources
    Select
    any
    number of
    songs
    Play
    previews
    inline
    Filters
    by genre
    21
    Song Provider: network
    Media Player: Android Framework
    Filters: derived from list of songs

    View Slide

  22. @pacoworks
    Song Provider
    Select any
    number of
    songs
    Play previews
    inline
    Filters by
    genre
    Do not
    display songs
    already on
    soundtrack
    22
    interface SongService {
    fun getListOfSongs(): Observable>
    }
    interface SongAPI {
    fun getSongs(): Observable>
    }
    object SongProvider: SongService {
    override fun getListOfSongs(): Observable> =
    /*...*/
    }

    View Slide

  23. @pacoworks
    Song Provider
    Select any
    number of
    songs
    Play previews
    inline
    Filters by
    genre
    Do not
    display songs
    already on
    soundtrack
    23
    object SongProvider: SongService {
    override fun getListOfSongs(): Observable> =
    getAPI().getSongs()
    // Failure policy
    .retry(3)
    .flatMap { songPOJOs -> toSongList(songPOJOs) }
    .flatMap { songs ->
    getSoundtrack().flatMap { soundtrack ->
    Observable.from(songs).filter { song ->
    !soundtrack.contains(song)
    }
    }
    }
    .toList()
    // Error handling with a good default
    .onErrorReturn(emptyList())
    }

    View Slide

  24. @pacoworks
    Media Player
    Select
    any
    number of
    songs
    Play
    previews
    inline
    Filters
    by genre
    24
    data class AndroidMusicPlayer(url: String) {
    fun play(): Observable = /* ... */
    fun observeBeats(): Observable = /* ... */
    fun stop(): Observable = /* ... */
    }
    interface SongPlayer {
    fun playSong(url: String): Observable
    }
    data class Player(val lfcy: Observable): SongPlayer {
    override fun playSong(url: String): Observable =
    AndroidMusicPlayer(url).let { player ->
    player.play()
    .flatMap { player.observeBeats() }
    .takeUntil(lfcy.filter { it is PAUSE })
    .concatWith(player.stop())
    }
    }

    View Slide

  25. @pacoworks
    Media Player
    Select
    any
    number of
    songs
    Play
    previews
    inline
    Filters
    by genre
    25
    data class AndroidMusicPlayer private constructor (url: String) {
    fun play(): Observable = /* ... */
    fun observeBeats(): Observable = /* ... */
    fun stop(): Observable = /* ... */
    companion object {
    operator fun invoke(url: String) =
    Observable.create { /* url validation code */ }
    }
    }
    data class Player(val lfcy: Observable): SongPlayer {
    override fun playSong(url: String): Observable =
    AndroidMusicPlayer(url).flatMap { player ->
    player.play()
    .flatMap { player.observeBeats() }
    .takeUntil(lfcy.filter { it is PAUSE })
    .concatWith(player.stop())
    }
    }

    View Slide

  26. @pacoworks
    User Interaction
    Select
    any
    number of
    songs
    Play
    previews
    inline
    Filters
    by genre
    26
    interface UserInteractionProvider {

    fun clickSong(): Observable>

    fun clickPlay(): Observable>

    fun clickStop(): Observable>
    fun clickFilter(): Observable
    fun clickSave(): Observable
    }
    Two separate buttons for play/stop

    View Slide

  27. @pacoworks
    Writing our business logic
    27

    View Slide

  28. @pacoworks
    Breaking down use cases
    Match data sources with resulting state
    Work on small, self-contained ideas
    Use cases talk between them by modifying state
    State gets applied to UI automatically
    28

    View Slide

  29. @pacoworks
    Loading songs & filters
    29
    // Service
    fun getListOfSongs()
    : Observable>
    // State
    val songList
    : BehaviorSubject>
    val filterList
    : BehaviorSubject>
    songList.switchMap { currentSongs ->
    if (currentSongs.isEmpty()) {
    getListOfSongs()
    } else {
    Observable.empty()
    }
    }.doOnNext(songList)
    .flatMap { songs ->
    Observable.from(songs)
    .map { song -> song.genre }
    .distinct()
    .toList()
    }.subscribe(filterList)
    Beware the default value!

    View Slide

  30. @pacoworks
    Selecting songs
    30
    // Service
    fun clickSong()
    : Observable>

    // State
    val songSelected
    : BehaviorSubject>
    songSelected.switchMap { selectedSongs ->
    clickSong().switchMap { songClicked ->
    if (selectedSongs.contains(songClicked)){
    selectedSongs
    .run { remove(songClicked) }
    } else {
    selectedSongs
    .run { add(songClicked) }
    }
    }
    }.subscribe(songSelected)
    Add and remove return a new List

    View Slide

  31. @pacoworks
    Selecting filters
    31
    // Service
    fun clickFilter()
    : Observable
    // State
    val songList
    : BehaviorSubject>
    val filterSelected
    : BehaviorSubject>
    filterSelected.switchMap { _ ->
    clickFilter().switchMap { genre ->
    songList.first().map { songs ->
    if (genre.isNotEmpty()) {
    songs.filter { song ->
    song.genre == genre
    }
    } else {
    songs
    }
    }
    }
    }.subscribe(filterSelected)
    Hot into cold observable
    Beware the default value!

    View Slide

  32. @pacoworks
    Playing songs
    32
    // Services
    fun clickPlay()
    : Observable>

    fun clickStop()
    : Observable>
    fun playSong(url: String)
    : Observable
    // Display
    fun showBeat(
    beat:Tuple2)
    fun clearBeat()
    clickPlay().switchMap { songPosition ->
    playSong(songPosition.element.url)
    .map { beat ->
    beat to songPosition.element
    }
    .takeUntil(
    clickStop().filter { stopPosition ->
    songPosition == stopPosition
    })
    .doOnUnsubscribe(view::clearBeat)
    }.subscribe(view::showBeat)
    No state, values are transient!
    Lifecycle already embedded in
    playSong!

    View Slide

  33. @pacoworks
    Saving to soundtrack
    33
    // Service
    fun clickSave()
    : Observable
    // State
    val songSelected
    : BehaviorSubject>
    val soundtrack
    : BehaviorSubject>
    songSelected.switchMap { selectedSongs ->
    clickSave().switchMap { _ ->
    soundtrack.map { soundtrackSongs ->
    soundtrackSongs + selectedSongs.toList()
    }
    }
    }.subscribe(soundtrack)

    View Slide

  34. @pacoworks
    Improving
    maintainability
    34

    View Slide

  35. @pacoworks
    Saving to soundtrack
    - with syntactic sugar
    35
    // Service
    fun clickSave()
    : Observable
    // State
    val songSelected
    : BehaviorSubject>
    val soundtrack
    : BehaviorSubject>
    ObservableKW.monadErrorSwitch().binding {
    val selectedSongs = songSelected.bind().toList()
    clickSave().bind()
    val currentSoundtrack = soundtrack.bind()
    currentSoundtrack + selectedSongs
    }.fix().subscribe(soundtrack)

    View Slide

  36. @pacoworks
    Saving to soundtrack
    - with other frameworks
    36
    // Service
    fun clickSave()
    : ???
    // State
    val songSelected
    : ???>
    val soundtrack
    : ???>
    IO.monadError().binding {
    /*...*/
    }.fix().unsafeRunAsync { soundtrack.set(it) }
    PromiseK.monadError().binding {
    /*...*/
    }.fix().success { soundtrack.set(it) }
    FlowableK.monadErrorSwitch().binding {
    /*...*/
    }.fix().subscribe(soundtrack)

    View Slide

  37. @pacoworks
    Saving to soundtrack
    - framework agnostic
    37
    fun MonadError.saveToSoundtrack(
    songSelected : Kind>,
    soundtrack : Kind>,
    clickSave: () -> Kind) =
    binding {
    val selectedSongs = songSelected.bind().toList()
    clickSave().bind()
    val currentSoundtrack = soundtrack.bind()
    currentSoundtrack + selectedSongs
    }
    // Service
    fun clickSave()
    : Kind
    // State
    val songSelected
    : Kind>
    val soundtrack
    : Kind>

    View Slide

  38. @pacoworks
    Saving to soundtrack
    - framework agnostic
    38
    ObservableK.monadErrorSwitch().saveToSoundtrack(/*…*/)
    .fix().subscribe(soundtrack)
    IO.monadError().saveToSoundtrack(/*…*/)
    .fix().unsafeRunAsync { soundtrack.set(it) }
    PromiseK.monadError().saveToSoundtrack(/*…*/)
    .fix().success { soundtrack.set(it) }
    FlowableK.monadErrorSwitch().saveToSoundtrack(/*…*/)
    .fix().subscribe(soundtrack)
    // Service
    fun clickSave()
    : Kind
    // State
    val songSelected
    : Kind>
    val soundtrack
    : Kind>
    // Business logic
    fun MonadError.saveToSoudtrack(
    songSelected : Kind>,
    soundtrack : Kind>,
    clickSave: () -> Kind)

    View Slide

  39. @pacoworks
    Arrow:
    http://arrow-kt.io/
    Fully Reactive Apps:
    http://www.pacoworks.com/2016/11/02/fully-
    reactive-apps-at-droidcon-uk-2016-2/
    Domain modeling with sealed classes:
    http://www.pacoworks.com/2016/10/03/new-talk-
    a-domain-driven-approach-to-kotlins-new-types-
    at-mobilization-2016/
    DI with typeclasses:
    https://www.pacoworks.com/2018/02/25/simple-
    dependency-injection-in-kotlin-part-1/
    Sample project:
    https://github.com/pakoito/
    FunctionalAndroidReference
    Copyright slides 4 & 5 - CC BY 4.0 Carmen
    Martín Herrero
    pacoworks.com
    @pacoworks
    github.com/pakoito
    Slides: http://tinyurl.com/RxUseCaseAsos18
    39

    View Slide