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

flux with kotlin (Abema Dev Con 2016)

AAkira
October 15, 2016

flux with kotlin (Abema Dev Con 2016)

Presented at Abema developer conference 2016
Introduced flux architecture with Kotlin and RxJava.

conference page : http://developer.abema.io/
video in Japanese : https://abemafresh.tv/tech-conference/47693

AAkira

October 15, 2016
Tweet

More Decks by AAkira

Other Decks in Technology

Transcript

  1. Flux with Kotlin
    Abema Dev Con 2016

    View Slide

  2. About me
    @_a_akira
    AAkira
    a0akira
    Akira Aratani

    View Slide

  3. About FRESH!
    • 生放送配信プラットフォーム
    • FRESH! ≒ AbemaTV

    View Slide

  4. Released map
    M1 2012-04-12
    M11 2015-03-19
    M14 2015-10-01
    1.0-beta4 2015-12-22
    M13 2015-09-16
    1.0 2016-02-16
    1.0-RC 2016-02-04
    2016-01-21 Release
    2015-04 開発開始
    kotlin
    FRESH

    View Slide

  5. Architecture

    View Slide

  6. https://facebook.github.io/flux/docs/overview.html
    Data in a Flux application flows in a single direction
    About Flux

    View Slide

  7. About Flux

    View Slide

  8. About Flux
    Observer pattern

    View Slide

  9. https://speakerdeck.com/ogaclejapan/flux-de-relax

    View Slide

  10. Architecture of FRESH
    IUUQTHJUIVCDPNBOESPJE"OESPJE$MFBO"SDIJUFDUVSF
    $MFBO"SDIJUFDUVSF

    View Slide

  11. Architecture of FRESH

    View Slide

  12. View
    Action
    Dispatcher
    Store
    DB WEB
    view(flux)
    data
    Repository

    View Slide

  13. View
    Action
    Dispatcher
    Store
    Repository
    DB WEB
    view(flux)
    data
    OKHttp(Retrofit)
    SQLBrite
    Observable
    Subject
    Observable Observable

    View Slide

  14. IUUQRJJUBDPNTBUPSVGVKJXBSBJUFNTDCGEBFDCBC

    View Slide

  15. How to implement with kotlin

    View Slide

  16. Using libraries
    • Dagger2
    • RxJava(RxKotlin)
    • RxAndroid
    • RxBinding
    • SQLBrite
    • RxPreferences
    • RxLifecycle
    => DI
    => Reactive Extensions
    => RxJava bindings for Android
    => binding APIs for Android's UI widgets
    => introduces reactive stream to SQLite
    => introduces reactive stream to Preferences
    => handles Android LifeCycles

    View Slide

  17. Archive List
    BSDIJWF
    BSDIJWF
    BSDIJWF
    BSDIJWF
    BSDIJWF

    View Slide

  18. View Dispatcher
    Store
    DB WEB
    view(flux)
    data
    Repository
    Action

    View Slide

  19. Action
    class ArchiveAction(private val dispatcher: ArchiveDispatcher, private val
    programRepository: ProgramRepository) {
    fun refresh() {
    programRepository.getProgramsArchive(count = 20, offset = 0)
    .subscribe({
    dispatcher.archiveDao.replaceAll(it)
    }, {
    dispatcher.onError(this, it, recoverAction = { refresh(screenId) })
    })
    }
    }

    View Slide

  20. Action
    class ArchiveAction(private val dispatcher: ArchiveDispatcher, private val
    programRepository: ProgramRepository) {
    fun refresh() {
    programRepository.getProgramsArchive(count = 20, offset = 0)
    .subscribe({
    dispatcher.archiveDao.replaceAll(it)
    }, {
    dispatcher.onError(this, it, recoverAction = { refresh(screenId) })
    })
    }
    }
    *OKFDU%JTQBUDIFSBOE3FQPTJUPSZ

    View Slide

  21. Action
    class ArchiveAction(private val dispatcher: ArchiveDispatcher, private val
    programRepository: ProgramRepository) {
    fun refresh() {
    programRepository.getProgramsArchive(count = 20, offset = 0)
    .subscribe({
    dispatcher.archiveDao.replaceAll(it)
    }, {
    dispatcher.onError(this, it, recoverAction = { refresh(screenId) })
    })
    }
    }
    $BMM3FQPTJUPSZ

    View Slide

  22. View
    Action
    Dispatcher
    Store
    DB WEB
    view(flux)
    data
    Repository

    View Slide

  23. Repository
    class ProgramRepository(private val programService: ProgramService) {
    fun getProgramsArchive(count: Int? = null, offset: Int? = null) =
    programService.getPrograms(count = count, offset = offset,
    status = "archive", orderBy = "endAt")
    .subscribeOn(Schedulers.io())
    .map { it.data.nullSafeMap(::toProgram) }
    }

    View Slide

  24. Repository
    class ProgramRepository(private val programService: ProgramService) {
    fun getProgramsArchive(count: Int? = null, offset: Int? = null) =
    programService.getPrograms(count = count, offset = offset,
    status = "archive", orderBy = "endAt")
    .subscribeOn(Schedulers.io())
    .map { it.data.nullSafeMap(::toProgram) }
    }
    *OKFDU4FSWJDF

    View Slide

  25. Repository
    class ProgramRepository(private val programService: ProgramService) {
    fun getProgramsArchive(count: Int? = null, offset: Int? = null) =
    programService.getPrograms(count = count, offset = offset,
    status = "archive", orderBy = "endAt")
    .subscribeOn(Schedulers.io())
    .map { it.data.nullSafeMap(::toProgram) }
    }
    $BMM4FSWJDF

    View Slide

  26. View
    Action
    Dispatcher
    Store
    view(flux)
    data
    Repository
    DB WEB

    View Slide

  27. WEB (Service)
    interface ProgramService {
    @GET("/programs")
    fun getPrograms(@Query("channelId") channelId: String? = null,
    @Query("count") count: Int?,
    @Query("orderBy") orderBy: String? = null,
    @Query("status") status: String? = null,
    ……,
    ……,
    ): Observable>>
    }

    View Slide

  28. WEB (Service)
    3FUVSOPCTFSWBCMF 3FUSPpU 0,)UUQ

    interface ProgramService {
    @GET("/programs")
    fun getPrograms(@Query("channelId") channelId: String? = null,
    @Query("count") count: Int?,
    @Query("orderBy") orderBy: String? = null,
    @Query("status") status: String? = null,
    ……,
    ……,
    ): Observable>>
    }

    View Slide

  29. View
    Action
    Store
    DB WEB
    view(flux)
    data
    Repository
    Dispatcher

    View Slide

  30. Dispatcher
    class ArchiveDispatcher(provider: DispatcherProvider) {
    val archiveDao = provider.archiveDao()
    }

    View Slide

  31. Action / Dispatcher
    class ArchiveAction(private val dispatcher: ArchiveDispatcher, private val
    programRepository: ProgramRepository) {
    fun refresh() {
    programRepository.getProgramsArchive(count = 20, offset = 0)
    .subscribe({
    dispatcher.archiveDao.replaceAll(it)
    }, {
    dispatcher.onError(this, it, recoverAction = { refresh() })
    })
    }
    }

    View Slide

  32. Action / Dispatcher
    class ArchiveAction(private val dispatcher: ArchiveDispatcher, private val
    programRepository: ProgramRepository) {
    fun refresh() {
    programRepository.getProgramsArchive(count = 20, offset = 0)
    .subscribe({
    dispatcher.archiveDao.replaceAll(it)
    }, {
    dispatcher.onError(this, it, recoverAction = { refresh() })
    })
    }
    }
    6QEBUFEBUBCBTF

    View Slide

  33. View
    Action
    Dispatcher
    DB WEB
    view(flux)
    data
    Repository
    Store

    View Slide

  34. Store
    class ArchiveStore(private val dispatcher: ArchiveDispatcher) {
    fun programs() =
    dispatcher.archiveDao.selectAll().subscribeOn(Schedulers.io())
    }

    View Slide

  35. Store
    *OKFDU%JTQBUDIFS
    class ArchiveStore(private val dispatcher: ArchiveDispatcher) {
    fun programs() =
    dispatcher.archiveDao.selectAll().subscribeOn(Schedulers.io())
    }

    View Slide

  36. Store
    0CTFSWF42- 42-#SJUF

    class ArchiveStore(private val dispatcher: ArchiveDispatcher) {
    fun programs() =
    dispatcher.archiveDao.selectAll().subscribeOn(Schedulers.io())
    }

    View Slide

  37. Action
    Dispatcher
    Store
    DB WEB
    view(flux)
    data
    Repository
    View

    View Slide

  38. View
    class ArchiveFragment {
    @Inject lateinit var archiveAction: ArchiveAction
    @Inject lateinit var archiveStore: ArchiveStore
    override fun onViewCreated(view: View?, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)
    archiveStore.programs()
    .observeOn(AndroidSchedulers.mainThread())
    .bindToLifecycle(this)
    .subscribe {
    adapter.clear()
    it.forEach { … } // add to adapter
    adapter.notifyDataSetChanged()
    }
    savedInstanceState ?: archiveAction.refresh()
    }

    View Slide

  39. View
    class ArchiveFragment {
    @Inject lateinit var archiveAction: ArchiveAction
    @Inject lateinit var archiveStore: ArchiveStore
    override fun onViewCreated(view: View?, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)
    archiveStore.programs()
    .observeOn(AndroidSchedulers.mainThread())
    .bindToLifecycle(this)
    .subscribe {
    adapter.clear()
    it.forEach { … } // add to adapter
    adapter.notifyDataSetChanged()
    }
    savedInstanceState ?: archiveAction.refresh()
    }
    *OKFDU"DUJPOBOE4UPSF

    View Slide

  40. View
    class ArchiveFragment {
    @Inject lateinit var archiveAction: ArchiveAction
    @Inject lateinit var archiveStore: ArchiveStore
    override fun onViewCreated(view: View?, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)
    archiveStore.programs()
    .observeOn(AndroidSchedulers.mainThread())
    .bindToLifecycle(this)
    .subscribe {
    adapter.clear()
    it.forEach { … } // add to adapter
    adapter.notifyDataSetChanged()
    }
    savedInstanceState ?: archiveAction.refresh()
    }
    $BMMSFGSFTIBDUJPO

    View Slide

  41. View
    class ArchiveFragment {
    @Inject lateinit var archiveAction: ArchiveAction
    @Inject lateinit var archiveStore: ArchiveStore
    override fun onViewCreated(view: View?, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)
    archiveStore.programs()
    .observeOn(AndroidSchedulers.mainThread())
    .bindToLifecycle(this)
    .subscribe {
    adapter.clear()
    it.forEach { … } // add to adapter
    adapter.notifyDataSetChanged()
    }
    savedInstanceState ?: archiveAction.refresh()
    }
    0CTFSWFTUPSF

    View Slide

  42. View
    class ArchiveFragment {
    @Inject lateinit var archiveAction: ArchiveAction
    @Inject lateinit var archiveStore: ArchiveStore
    override fun onViewCreated(view: View?, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)
    archiveStore.programs()
    .observeOn(AndroidSchedulers.mainThread())
    .bindToLifecycle(this)
    .subscribe {
    adapter.clear()
    it.forEach { … } // add to adapter
    adapter.notifyDataSetChanged()
    }
    savedInstanceState ?: archiveAction.refresh()
    }
    3Y"OESPJE

    View Slide

  43. View
    class ArchiveFragment {
    @Inject lateinit var archiveAction: ArchiveAction
    @Inject lateinit var archiveStore: ArchiveStore
    override fun onViewCreated(view: View?, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)
    archiveStore.programs()
    .observeOn(AndroidSchedulers.mainThread())
    .bindToLifecycle(this)
    .subscribe {
    adapter.clear()
    it.forEach { … } // add to adapter
    adapter.notifyDataSetChanged()
    }
    savedInstanceState ?: archiveAction.refresh()
    }
    3Y-JGFDZDMF

    View Slide

  44. View
    class ArchiveFragment {
    @Inject lateinit var archiveAction: ArchiveAction
    @Inject lateinit var archiveStore: ArchiveStore
    override fun onViewCreated(view: View?, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)
    archiveStore.programs()
    .observeOn(AndroidSchedulers.mainThread())
    .bindToLifecycle(this)
    .subscribe {
    adapter.clear()
    it.forEach { … } // add to adapter
    adapter.notifyDataSetChanged()
    }
    savedInstanceState ?: archiveAction.refresh()
    }

    View Slide

  45. View
    class ArchiveFragment {
    @Inject lateinit var archiveAction: ArchiveAction
    @Inject lateinit var archiveStore: ArchiveStore
    override fun onViewCreated(view: View?, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)
    archiveStore.programs()
    .observeOn(AndroidSchedulers.mainThread())
    .bindToLifecycle(this)
    .subscribe {
    adapter.clear()
    it.forEach { … } // add to adapter
    adapter.notifyDataSetChanged()
    }
    savedInstanceState ?: archiveAction.refresh()
    }
    Why don’t you implement onError

    View Slide

  46. Action / Dispatcher
    class ArchiveAction(private val dispatcher: ArchiveDispatcher, private val
    programRepository: ProgramRepository) {
    fun refresh() {
    programRepository.getProgramsArchive(count = 20, offset = 0)
    .subscribe({
    dispatcher.archiveDao.replaceAll(it)
    }, {
    dispatcher.onError(this, it, recoverAction = { refresh() })
    })
    }
    }
    4FOEFSSPS
    open class CommonDispatcher(val provider: DispatcherProvider) {
    fun errors() = provider.errorDispatcher.errorObservable
    fun onError(action: Any, cause: Throwable, extras: Any? = null,
    recoverAction: (() -> Unit)? = null) =
    provider.errorDispatcher.onError(action, cause, extras, recoverAction)
    }

    View Slide

  47. Action / Dispatcher
    class ArchiveAction(private val dispatcher: ArchiveDispatcher, private val
    programRepository: ProgramRepository) {
    fun refresh() {
    programRepository.getProgramsArchive(count = 20, offset = 0)
    .subscribe({
    dispatcher.archiveDao.replaceAll(it)
    }, {
    dispatcher.onError(this, it, recoverAction = { refresh() })
    })
    }
    }
    open class CommonDispatcher(val provider: DispatcherProvider) {
    fun errors() = provider.errorDispatcher.errorObservable
    fun onError(action: Any, cause: Throwable, extras: Any? = null,
    recoverAction: (() -> Unit)? = null) =
    provider.errorDispatcher.onError(action, cause, extras, recoverAction)
    }
    $PNNPO%JTQBUDIFS

    View Slide

  48. Action / Dispatcher
    class ArchiveAction(private val dispatcher: ArchiveDispatcher, private val
    programRepository: ProgramRepository) {
    fun refresh() {
    programRepository.getProgramsArchive(count = 20, offset = 0)
    .subscribe({
    dispatcher.archiveDao.replaceAll(it)
    }, {
    dispatcher.onError(this, it, recoverAction = { refresh() })
    })
    }
    }
    open class CommonDispatcher(val provider: DispatcherProvider) {
    fun errors() = provider.errorDispatcher.errorObservable
    fun onError(action: Any, cause: Throwable, extras: Any? = null,
    recoverAction: (() -> Unit)? = null) =
    provider.errorDispatcher.onError(action, cause, extras, recoverAction)
    }
    *OMJOF'VODUJPOTCZ,PUMJO

    View Slide

  49. View
    class ArchiveFragment {
    @Inject lateinit var archiveStore: ArchiveStore
    override fun onResume() {
    super.onResume()
    onAirStore.errors()
    .filter { it.action is ArchiveAction }
    .observeOn(AndroidSchedulers.mainThread())
    .bindToLifecycle(this)
    .subscribe {
    if (it.kind == ErrorEvent.Kind.NETWORK) {
    showSnackBar(){ it.recoverAction?.invoke() }
    }
    }
    }
    }

    View Slide

  50. View
    class ArchiveFragment {
    @Inject lateinit var archiveStore: ArchiveStore
    override fun onResume() {
    super.onResume()
    onAirStore.errors()
    .filter { it.action is ArchiveAction }
    .observeOn(AndroidSchedulers.mainThread())
    .bindToLifecycle(this)
    .subscribe {
    if (it.kind == ErrorEvent.Kind.NETWORK) {
    showSnackBar(){ it.recoverAction?.invoke() }
    }
    }
    }
    }
    4VCTDSJCFJOPO3FTVNF

    View Slide

  51. View
    class ArchiveFragment {
    @Inject lateinit var archiveStore: ArchiveStore
    override fun onResume() {
    super.onResume()
    onAirStore.errors()
    .filter { it.action is ArchiveAction }
    .observeOn(AndroidSchedulers.mainThread())
    .bindToLifecycle(this)
    .subscribe {
    if (it.kind == ErrorEvent.Kind.NETWORK) {
    showSnackBar(){ it.recoverAction?.invoke() }
    }
    }
    }
    }
    'JMUFSPOMZBSDIJWFBDUJPO %*

    View Slide

  52. View
    class ArchiveFragment {
    @Inject lateinit var archiveStore: ArchiveStore
    override fun onResume() {
    super.onResume()
    onAirStore.errors()
    .filter { it.action is ArchiveAction }
    .observeOn(AndroidSchedulers.mainThread())
    .bindToLifecycle(this)
    .subscribe {
    if (it.kind == ErrorEvent.Kind.NETWORK) {
    showSnackBar(){ it.recoverAction?.invoke() }
    }
    }
    }
    }
    3FUSZSFDPWFS"DUJPOJGVTFSTDMJDLBTOBDLCBS

    View Slide

  53. Everything is a stream

    View Slide

  54. DB WEB
    view(flux)
    data
    Repository
    Action
    Dispatcher
    Store
    View

    View Slide

  55. Like event bus
    class ArchiveDispatcher(provider: DispatcherProvider) {
    val statusObservable =
    SerializedSubject(BehaviorSubject.create())
    }
    class ArchiveAction(private val dispatcher: ArchiveDispatcher) {
    fun updateStatus(status: Status) {
    dispatcher.statusObservable.onNext(status)
    }
    }
    class ArchiveStore(private val dispatcher: ArchiveDispatcher) {
    fun status() = dispatcher.statusObservable
    }

    View Slide

  56. Like event bus
    class ArchiveDispatcher(provider: DispatcherProvider) {
    val statusObservable =
    SerializedSubject(BehaviorSubject.create())
    }
    class ArchiveAction(private val dispatcher: ArchiveDispatcher) {
    fun updateStatus(status: Status) {
    dispatcher.statusObservable.onNext(status)
    }
    }
    class ArchiveStore(private val dispatcher: ArchiveDispatcher) {
    fun status() = dispatcher.statusObservable
    }

    View Slide

  57. Like event bus
    class ArchiveDispatcher(provider: DispatcherProvider) {
    val statusObservable =
    SerializedSubject(BehaviorSubject.create())
    }
    class ArchiveAction(private val dispatcher: ArchiveDispatcher) {
    fun updateStatus(status: Status) {
    dispatcher.statusObservable.onNext(status)
    }
    }
    class ArchiveStore(private val dispatcher: ArchiveDispatcher) {
    fun status() = dispatcher.statusObservable
    }

    View Slide

  58. Like event bus
    class ArchiveDispatcher(provider: DispatcherProvider) {
    val statusObservable =
    SerializedSubject(BehaviorSubject.create())
    }
    class ArchiveAction(private val dispatcher: ArchiveDispatcher) {
    fun updateStatus(status: Status) {
    dispatcher.statusObservable.onNext(status)
    }
    }
    class ArchiveStore(private val dispatcher: ArchiveDispatcher) {
    fun status() = dispatcher.statusObservable
    }

    View Slide

  59. Like event bus
    class ArchiveFragment {
    @Inject lateinit var archiveStore: ArchiveStore
    override fun onViewCreated(view: View?, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)
    archiveStore.status()
    .observeOn(AndroidSchedulers.mainThread())
    .bindToLifecycle(this)
    .subscribe {
    when(it) {
    HOGE -> textView.text = "hoge"
    FOO -> textView.text = "foo"
    BAR -> textView.text = "bar"
    }
    }
    }

    View Slide

  60. Like event bus
    class ArchiveFragment {
    @Inject lateinit var archiveStore: ArchiveStore
    override fun onViewCreated(view: View?, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)
    archiveStore.status()
    .observeOn(AndroidSchedulers.mainThread())
    .bindToLifecycle(this)
    .subscribe {
    when(it) {
    HOGE -> textView.text = "hoge"
    FOO -> textView.text = "foo"
    BAR -> textView.text = "bar"
    }
    }
    }

    View Slide

  61. List
    MJTU
    MJTU
    MJTU
    MJTU
    MJTU
    refresh

    View Slide

  62. Connect observables
    refreshButton.clicks()
    .mergeWith(recyclerView.pull())
    .withLatestFrom(archiveStore.programs())
    { void, programs -> programs }
    .observeOn(AndroidSchedulers.mainThread())
    .bindToLifecycle(this)
    .subscribe {
    // add to adapter
    }

    View Slide

  63. Connect observables
    3Y#JOEJOH
    refreshButton.clicks()
    .mergeWith(recyclerView.pull())
    .withLatestFrom(archiveStore.programs())
    { void, programs -> programs }
    .observeOn(AndroidSchedulers.mainThread())
    .bindToLifecycle(this)
    .subscribe {
    // add to adapter
    }

    View Slide

  64. Connect observables
    .FSHFDVTUPNPCTFSWBCMF
    refreshButton.clicks()
    .mergeWith(recyclerView.pull())
    .withLatestFrom(archiveStore.programs())
    { void, programs -> programs }
    .observeOn(AndroidSchedulers.mainThread())
    .bindToLifecycle(this)
    .subscribe {
    // add to adapter
    }

    View Slide

  65. Connect observables
    $POOFDUJUFNTGSPNTUPSF
    refreshButton.clicks()
    .mergeWith(recyclerView.pull())
    .withLatestFrom(archiveStore.programs())
    { void, programs -> programs }
    .observeOn(AndroidSchedulers.mainThread())
    .bindToLifecycle(this)
    .subscribe {
    // add to adapter
    }

    View Slide

  66. Connect observables
    "EEUPJUFNTJOTVCTDSJCF
    refreshButton.clicks()
    .mergeWith(recyclerView.pull())
    .withLatestFrom(archiveStore.programs())
    { void, programs -> programs }
    .observeOn(AndroidSchedulers.mainThread())
    .bindToLifecycle(this)
    .subscribe {
    // add to adapter
    }

    View Slide

  67. • combineLatest, mergeWith, zipWith
    • map, switchMap, concatMap
    • scan, reduce
    • interval
    • filter, skip, distinctUntilChanged
    • etc…
    Other operators used in FRESH

    View Slide

  68. Conclusion
    • Clarity ;-)
    • Beautiful :-)
    • Comfortable :-D

    View Slide

  69. Flux with Kotlin
    @_a_akira

    View Slide