Slide 1

Slide 1 text

Flux with Kotlin Abema Dev Con 2016

Slide 2

Slide 2 text

About me @_a_akira AAkira a0akira Akira Aratani

Slide 3

Slide 3 text

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

Slide 4

Slide 4 text

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

Slide 5

Slide 5 text

Architecture

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

About Flux

Slide 8

Slide 8 text

About Flux Observer pattern

Slide 9

Slide 9 text

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

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

Architecture of FRESH

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

IUUQRJJUBDPNTBUPSVGVKJXBSBJUFNTDCGEBFDCBC

Slide 15

Slide 15 text

How to implement with kotlin

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

Archive List BSDIJWF BSDIJWF BSDIJWF BSDIJWF BSDIJWF

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

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) }) }) } }

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

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) } }

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

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>> }

Slide 28

Slide 28 text

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>> }

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

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() }) }) } }

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

Store 0CTFSWF42- 42-#SJUF class ArchiveStore(private val dispatcher: ArchiveDispatcher) { fun programs() = dispatcher.archiveDao.selectAll().subscribeOn(Schedulers.io()) }

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

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() }

Slide 39

Slide 39 text

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

Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

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

Slide 42

Slide 42 text

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

Slide 43

Slide 43 text

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

Slide 44

Slide 44 text

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() }

Slide 45

Slide 45 text

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

Slide 46

Slide 46 text

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) }

Slide 47

Slide 47 text

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

Slide 48

Slide 48 text

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

Slide 49

Slide 49 text

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() } } } } }

Slide 50

Slide 50 text

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

Slide 51

Slide 51 text

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 %*

Slide 52

Slide 52 text

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

Slide 53

Slide 53 text

Everything is a stream

Slide 54

Slide 54 text

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

Slide 55

Slide 55 text

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 }

Slide 56

Slide 56 text

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 }

Slide 57

Slide 57 text

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 }

Slide 58

Slide 58 text

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 }

Slide 59

Slide 59 text

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" } } }

Slide 60

Slide 60 text

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" } } }

Slide 61

Slide 61 text

List MJTU MJTU MJTU MJTU MJTU refresh

Slide 62

Slide 62 text

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

Slide 63

Slide 63 text

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

Slide 64

Slide 64 text

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

Slide 65

Slide 65 text

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

Slide 66

Slide 66 text

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

Slide 67

Slide 67 text

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

Slide 68

Slide 68 text

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

Slide 69

Slide 69 text

Flux with Kotlin @_a_akira