Slide 1

Slide 1 text

Compose everything Jaewe Heo · Riiid! · @importre Mar. 25, 2017 with Rx & Kotlin

Slide 2

Slide 2 text

Kotlin https://www.meetup.com/kotlinkr/

Slide 3

Slide 3 text

RxJava

Slide 4

Slide 4 text

Functional • Avoid intricate stateful programs, using clean input/output functions over observable streams. http://reactivex.io/

Slide 5

Slide 5 text

Functional • Avoid intricate stateful programs, using clean input/output functions over observable streams. http://reactivex.io/

Slide 6

Slide 6 text

Functional • Avoid intricate stateful programs, using clean input/output functions over observable streams. http://reactivex.io/

Slide 7

Slide 7 text

Functional • Avoid intricate stateful programs, using clean input/output functions over observable streams. http://reactivex.io/

Slide 8

Slide 8 text

진짜?!

Slide 9

Slide 9 text

Examples

Slide 10

Slide 10 text

Hello, World!

Slide 11

Slide 11 text

class HelloFragment : BaseFragment() { override val layoutId: Int = R.layout.fragment_hello override fun onViewCreated(view: View, savedInstanceState: Bundle?) { toolbar .navigationClicks() .subscribe { activity.onBackPressed() } inputName .textChanges() .subscribe { val hello = getString(R.string.hello_message) textHello.text = hello.format(it) } } }

Slide 12

Slide 12 text

class HelloFragment : BaseFragment() { override val layoutId: Int = R.layout.fragment_hello override fun onViewCreated(view: View, savedInstanceState: Bundle?) { toolbar .navigationClicks() .subscribe { activity.onBackPressed() } inputName .textChanges() .subscribe { val hello = getString(R.string.hello_message) textHello.text = hello.format(it) } } } Home As Up

Slide 13

Slide 13 text

class HelloFragment : BaseFragment() { override val layoutId: Int = R.layout.fragment_hello override fun onViewCreated(view: View, savedInstanceState: Bundle?) { toolbar .navigationClicks() .subscribe { activity.onBackPressed() } inputName .textChanges() .subscribe { val hello = getString(R.string.hello_message) textHello.text = hello.format(it) } } } EditText

Slide 14

Slide 14 text

class HelloFragment : BaseFragment() { override val layoutId: Int = R.layout.fragment_hello override fun onViewCreated(view: View, savedInstanceState: Bundle?) { toolbar .navigationClicks() .subscribe { activity.onBackPressed() } inputName .textChanges() .subscribe { val hello = getString(R.string.hello_message) textHello.text = hello.format(it) } } } Kotlin Android Extensions

Slide 15

Slide 15 text

class HelloFragment : BaseFragment() { override val layoutId: Int = R.layout.fragment_hello override fun onViewCreated(view: View, savedInstanceState: Bundle?) { toolbar .navigationClicks() .subscribe { activity.onBackPressed() } inputName .textChanges() .subscribe { val hello = getString(R.string.hello_message) textHello.text = hello.format(it) } } } RxBinding - Kotlin

Slide 16

Slide 16 text

class HelloFragment : BaseFragment() { override val layoutId: Int = R.layout.fragment_hello override fun onViewCreated(view: View, savedInstanceState: Bundle?) { toolbar .navigationClicks() .subscribe { activity.onBackPressed() } inputName .textChanges() .subscribe { val hello = getString(R.string.hello_message) textHello.text = hello.format(it) } } } Do Actions

Slide 17

Slide 17 text

Counter Increment / Decrement

Slide 18

Slide 18 text

class CounterFragment : BaseFragment() { override val layoutId: Int = R.layout.fragment_counter override fun onViewCreated(view: View, savedInstanceState: Bundle?) { val inc = buttonInc.clicks() .map { +1 } val dec = buttonDec.clicks() .map { -1 } Observable.merge(inc, dec) .scan(0) { acc: Int, value: Int -> acc + value } .subscribe { textValue.text = it.toString() } } }

Slide 19

Slide 19 text

class CounterFragment : BaseFragment() { override val layoutId: Int = R.layout.fragment_counter override fun onViewCreated(view: View, savedInstanceState: Bundle?) { val inc = buttonInc.clicks() .map { +1 } val dec = buttonDec.clicks() .map { -1 } Observable.merge(inc, dec) .scan(0) { acc: Int, value: Int -> acc + value } .subscribe { textValue.text = it.toString() } } } Plus / Minus

Slide 20

Slide 20 text

class CounterFragment : BaseFragment() { override val layoutId: Int = R.layout.fragment_counter override fun onViewCreated(view: View, savedInstanceState: Bundle?) { val inc = buttonInc.clicks() .map { +1 } val dec = buttonDec.clicks() .map { -1 } Observable.merge(inc, dec) .scan(0) { acc: Int, value: Int -> acc + value } .subscribe { textValue.text = it.toString() } } } Merge

Slide 21

Slide 21 text

No content

Slide 22

Slide 22 text

class CounterFragment : BaseFragment() { override val layoutId: Int = R.layout.fragment_counter override fun onViewCreated(view: View, savedInstanceState: Bundle?) { val inc = buttonInc.clicks() .map { +1 } val dec = buttonDec.clicks() .map { -1 } Observable.merge(inc, dec) .scan(0) { acc: Int, value: Int -> acc + value } .subscribe { textValue.text = it.toString() } } } Scan

Slide 23

Slide 23 text

No content

Slide 24

Slide 24 text

No content

Slide 25

Slide 25 text

class CounterFragment : BaseFragment() { override val layoutId: Int = R.layout.fragment_counter override fun onViewCreated(view: View, savedInstanceState: Bundle?) { val inc = buttonInc.clicks() .map { +1 } val dec = buttonDec.clicks() .map { -1 } Observable.merge(inc, dec) .scan(0) { acc: Int, value: Int -> acc + value } .subscribe { textValue.text = it.toString() } } } Do Actions

Slide 26

Slide 26 text

HTTP

Slide 27

Slide 27 text

override fun onViewCreated(view: View, savedInstanceState: Bundle?) { progress.hide() buttonUser.clicks() .doOnNext { progress.show() } .switchMap { api.getUsers() } .observeOn(AndroidSchedulers.mainThread()) .subscribe({ users -> val index = Random().nextInt(users.size) val user = users[index] textName.text = user.name textEmail.text = user.email progress.hide() }, { progress.hide() }) }

Slide 28

Slide 28 text

override fun onViewCreated(view: View, savedInstanceState: Bundle?) { progress.hide() buttonUser.clicks() .doOnNext { progress.show() } .switchMap { api.getUsers() } .observeOn(AndroidSchedulers.mainThread()) .subscribe({ users -> val index = Random().nextInt(users.size) val user = users[index] textName.text = user.name textEmail.text = user.email progress.hide() }, { progress.hide() }) } Kotlin Android Extensions / RxBinding

Slide 29

Slide 29 text

override fun onViewCreated(view: View, savedInstanceState: Bundle?) { progress.hide() buttonUser.clicks() .doOnNext { progress.show() } .switchMap { api.getUsers() } .observeOn(AndroidSchedulers.mainThread()) .subscribe({ users -> val index = Random().nextInt(users.size) val user = users[index] textName.text = user.name textEmail.text = user.email progress.hide() }, { progress.hide() }) } Show Progress

Slide 30

Slide 30 text

override fun onViewCreated(view: View, savedInstanceState: Bundle?) { progress.hide() buttonUser.clicks() .doOnNext { progress.show() } .switchMap { api.getUsers() } .observeOn(AndroidSchedulers.mainThread()) .subscribe({ users -> val index = Random().nextInt(users.size) val user = users[index] textName.text = user.name textEmail.text = user.email progress.hide() }, { progress.hide() }) } Retrofit / RxJava2 Adapter

Slide 31

Slide 31 text

override fun onViewCreated(view: View, savedInstanceState: Bundle?) { progress.hide() buttonUser.clicks() .doOnNext { progress.show() } .switchMap { api.getUsers() } .observeOn(AndroidSchedulers.mainThread()) .subscribe({ users -> val index = Random().nextInt(users.size) val user = users[index] textName.text = user.name textEmail.text = user.email progress.hide() }, { progress.hide() }) } Switch

Slide 32

Slide 32 text

No content

Slide 33

Slide 33 text

!!!!!!!!!… D/OkHttp: --> GET https://jsonplaceholder.typicode.com/users http/1.1 D/OkHttp: --> END GET D/OkHttp: <-- HTTP FAILED: java.io.IOException: Canceled D/OkHttp: --> GET https://jsonplaceholder.typicode.com/users http/1.1 D/OkHttp: --> END GET D/OkHttp: <-- HTTP FAILED: java.io.IOException: Canceled D/OkHttp: --> GET https://jsonplaceholder.typicode.com/users http/1.1 D/OkHttp: --> END GET D/OkHttp: <-- HTTP FAILED: java.io.IOException: Canceled D/OkHttp: --> GET https://jsonplaceholder.typicode.com/users http/1.1 D/OkHttp: --> END GET D/OkHttp: <-- HTTP FAILED: java.io.IOException: Canceled ...

Slide 34

Slide 34 text

!!!!!!!!!… D/OkHttp: --> GET https://jsonplaceholder.typicode.com/users http/1.1 D/OkHttp: --> END GET D/OkHttp: <-- HTTP FAILED: java.io.IOException: Canceled D/OkHttp: --> GET https://jsonplaceholder.typicode.com/users http/1.1 D/OkHttp: --> END GET D/OkHttp: <-- HTTP FAILED: java.io.IOException: Canceled D/OkHttp: --> GET https://jsonplaceholder.typicode.com/users http/1.1 D/OkHttp: --> END GET D/OkHttp: <-- HTTP FAILED: java.io.IOException: Canceled D/OkHttp: --> GET https://jsonplaceholder.typicode.com/users http/1.1 D/OkHttp: --> END GET D/OkHttp: <-- HTTP FAILED: java.io.IOException: Canceled ...

Slide 35

Slide 35 text

No content

Slide 36

Slide 36 text

No content

Slide 37

Slide 37 text

override fun onViewCreated(view: View, savedInstanceState: Bundle?) { progress.hide() buttonUser.clicks() .doOnNext { progress.show() } .switchMap { api.getUsers() } .observeOn(AndroidSchedulers.mainThread()) .subscribe({ users -> val index = Random().nextInt(users.size) val user = users[index] textName.text = user.name textEmail.text = user.email progress.hide() }, { progress.hide() }) } Change Scheduler

Slide 38

Slide 38 text

override fun onViewCreated(view: View, savedInstanceState: Bundle?) { progress.hide() buttonUser.clicks() .doOnNext { progress.show() } .switchMap { api.getUsers() } .observeOn(AndroidSchedulers.mainThread()) .subscribe({ users -> val index = Random().nextInt(users.size) val user = users[index] textName.text = user.name textEmail.text = user.email progress.hide() }, { progress.hide() }) } Do Actions

Slide 39

Slide 39 text

Pitfalls

Slide 40

Slide 40 text

Memory Leaks Pitfall #1

Slide 41

Slide 41 text

Example: Long-term jobs override fun onViewCreated(view: View, savedInstanceState: Bundle?) { // Printed for 1 hour if not killed Observable.interval(1, TimeUnit.SECONDS) .take(3600) .subscribe(::println) }

Slide 42

Slide 42 text

Example: RxBinding /** * ... * * Warning: The created observable keeps a strong * reference to view. Unsubscribe to free this reference. * * Warning: The created observable uses * View.setOnClickListener to observe clicks. * Only one observable can be used for a view at a time. */ public static Observable clicks(@NonNull View view) { // ... }

Slide 43

Slide 43 text

Simple solution: CompositeDisposable abstract class BaseFragment : Fragment() { protected val disposables by lazy { CompositeDisposable() } // ... override fun onDestroyView() { disposables.clear() super.onDestroyView() } }

Slide 44

Slide 44 text

CompositeDisposable abstract class BaseFragment : Fragment() { protected val disposables by lazy { CompositeDisposable() } // ... override fun onDestroyView() { disposables.clear() super.onDestroyView() } }

Slide 45

Slide 45 text

CompositeDisposable abstract class BaseFragment : Fragment() { protected val disposables by lazy { CompositeDisposable() } // ... override fun onDestroyView() { disposables.clear() super.onDestroyView() } }

Slide 46

Slide 46 text

Long-term jobs override fun onViewCreated(view: View, savedInstanceState: Bundle?) { Observable.interval(1, TimeUnit.SECONDS) .take(3600) .subscribe(::println) }

Slide 47

Slide 47 text

Add disposable to disposables using apply override fun onViewCreated(view: View, savedInstanceState: Bundle?) { Observable.interval(1, TimeUnit.SECONDS) .take(3600) .subscribe(::println) .apply { disposables.add(this) } } Function Literals with Receiver

Slide 48

Slide 48 text

Apply public inline fun T.apply(block: T.() -> Unit): T { block(); return this } Standard.kt

Slide 49

Slide 49 text

RxBinding class MainFragment : BaseFragment() { // ... override fun onViewCreated(view: View, savedInstanceState: Bundle?) { val hello = buttonHello.clicks() .map { HelloActivity::class } val counter = buttonCounter.clicks() .map { CounterActivity::class } Observable.mergeArray(hello, counter) .subscribe { start(it) } .apply { disposables.add(this) } } // ... }

Slide 50

Slide 50 text

Create streams class MainFragment : BaseFragment() { // ... override fun onViewCreated(view: View, savedInstanceState: Bundle?) { val hello = buttonHello.clicks() .map { HelloActivity::class } val counter = buttonCounter.clicks() .map { CounterActivity::class } Observable.mergeArray(hello, counter) .subscribe { start(it) } .apply { disposables.add(this) } } // ... }

Slide 51

Slide 51 text

Merge & Add to CompositeDisposable class MainFragment : BaseFragment() { // ... override fun onViewCreated(view: View, savedInstanceState: Bundle?) { val hello = buttonHello.clicks() .map { HelloActivity::class } val counter = buttonCounter.clicks() .map { CounterActivity::class } Observable.mergeArray(hello, counter) .subscribe { start(it) } .apply { disposables.add(this) } } // ... }

Slide 52

Slide 52 text

Merge class MainFragment : BaseFragment() { // ... override fun onViewCreated(view: View, savedInstanceState: Bundle?) { val hello = buttonHello.clicks() .map { HelloActivity::class } val counter = buttonCounter.clicks() .map { CounterActivity::class } Observable.mergeArray(hello, counter) .subscribe { start(it) } .apply { disposables.add(this) } } // ... }

Slide 53

Slide 53 text

startActivity() class MainFragment : BaseFragment() { // ... override fun onViewCreated(view: View, savedInstanceState: Bundle?) { val hello = buttonHello.clicks() .map { HelloActivity::class } val counter = buttonCounter.clicks() .map { CounterActivity::class } Observable.mergeArray(hello, counter) .subscribe { start(it) } .apply { disposables.add(this) } } // ... }

Slide 54

Slide 54 text

Add disposable to CompositeDisposable using apply class MainFragment : BaseFragment() { // ... override fun onViewCreated(view: View, savedInstanceState: Bundle?) { val hello = buttonHello.clicks() .map { HelloActivity::class } val counter = buttonCounter.clicks() .map { CounterActivity::class } Observable.mergeArray(hello, counter) .subscribe { start(it) } .apply { disposables.add(this) } } // ... }

Slide 55

Slide 55 text

Merge & Add it to CompositeDisposable class MainFragment : BaseFragment() { // ... override fun onViewCreated(view: View, savedInstanceState: Bundle?) { val hello = buttonHello.clicks() .map { HelloActivity::class } val counter = buttonCounter.clicks() .map { CounterActivity::class } Observable.mergeArray(hello, counter) .subscribe { start(it) } .apply { disposables.add(this) } } // ... } All streams will be completed

Slide 56

Slide 56 text

Whenis rotated ?! Pitfall #2 Configuration changes

Slide 57

Slide 57 text

Progress

Slide 58

Slide 58 text

Progress

Slide 59

Slide 59 text

Progress

Slide 60

Slide 60 text

Progress 0% ?!

Slide 61

Slide 61 text

Case #1 view ----+---------| -------> \ http ----1| switch ----------1---| ------->

Slide 62

Slide 62 text

Case #1 view ----+---------| -------> \ http ----1| switch ----------1---| -------> Click

Slide 63

Slide 63 text

Case #1 view ----+---------| -------> \ http ----1| switch ----------1---| -------> Loading

Slide 64

Slide 64 text

Case #1 view ----+---------| -------> \ http ----1| switch ----------1---| -------> Result

Slide 65

Slide 65 text

Case #1 view ----+---------| -------> \ http ----1| switch ----------1---| -------> Rotation

Slide 66

Slide 66 text

Case #1 view ----+---------| -------> \ http ----1| switch ----------1---| -------> Recreation

Slide 67

Slide 67 text

Case #1 view ----+---------| -------> \ http ----1| switch ----------1---| -------> ?!!

Slide 68

Slide 68 text

Case #2 view ----+---| -------> \ http --| switch --------| -------> Click

Slide 69

Slide 69 text

Case #2 view ----+---| -------> \ http --| switch --------| -------> Loading

Slide 70

Slide 70 text

Case #2 view ----+---| -------> \ http --| switch --------| -------> Rotation

Slide 71

Slide 71 text

Case #2 view ----+---| -------> \ http --| switch --------| -------> ?!!

Slide 72

Slide 72 text

Any solutions?

Slide 73

Slide 73 text

Rx ❤

Slide 74

Slide 74 text

Cache / Subject without LifeCycle

Slide 75

Slide 75 text

Make proxy User events (e.g. clicks, textChanges …) ——————————————————————————————— ——— View Model Stream, Proxy Event Stream ———> ——————————————————————————————— Long-term jobs (e.g. http, database, preferences …)

Slide 76

Slide 76 text

Area of Activity / Fragment User events (e.g. clicks, textChanges …) ——————————————————————————————— ——— View Model Stream, Proxy Event Stream ———> ——————————————————————————————— Long-term jobs (e.g. http, database, preferences …)

Slide 77

Slide 77 text

Outside of Activity/Fragment User events (e.g. clicks, textChanges …) ——————————————————————————————— ——— View Model Stream, Proxy Event Stream ———> ——————————————————————————————— Long-term jobs (e.g. http, database, preferences …)

Slide 78

Slide 78 text

Case #1: Subscribe after http is terminated view1 ----+---| \ http ------1 \---\------> view2 1-----> switch 1----->

Slide 79

Slide 79 text

Case #1: Subscribe after http is terminated view1 ----+---| \ http ------1 \---\------> view2 1-----> switch 1-----> Click

Slide 80

Slide 80 text

Case #1: Subscribe after http is terminated view1 ----+---| \ http ------1 \---\------> view2 1-----> switch 1-----> Subject

Slide 81

Slide 81 text

Case #1: Subscribe after http is terminated view1 ----+---| \ http ------1 \---\------> view2 1-----> switch 1-----> Send event to proxy using onNext

Slide 82

Slide 82 text

Case #1: Subscribe after http is terminated view1 ----+---| \ http ------1 \---\------> view2 1-----> switch 1-----> Rotation

Slide 83

Slide 83 text

Case #1: Subscribe after http is terminated view1 ----+---| \ http ------1 \---\------> view2 1-----> switch 1-----> Recreation, No Loading

Slide 84

Slide 84 text

Case #1: Subscribe after http is terminated view1 ----+---| \ http ------1 \---\------> view2 1-----> switch 1-----> Re-connect to previous stream

Slide 85

Slide 85 text

No content

Slide 86

Slide 86 text

Case #1: Subscribe after http is terminated view1 ----+---| \ http ------1 \---\------> view2 1-----> switch 1-----> Result

Slide 87

Slide 87 text

Case #2: Subscribe before http is terminated view1 ----+---| \ http ------1 \ view2 ----1--> switch ----1--> Click

Slide 88

Slide 88 text

Case #2: Subscribe before http is terminated view1 ----+---| \ http ------1 \ view2 ----1--> switch ----1--> Subject

Slide 89

Slide 89 text

Case #2: Subscribe before http is terminated view1 ----+---| \ http ------1 \ view2 ----1--> switch ----1--> Send event to proxy using onNext

Slide 90

Slide 90 text

Case #2: Subscribe before http is terminated view1 ----+---| \ http ------1 \ view2 ----1--> switch ----1--> Rotation

Slide 91

Slide 91 text

Case #2: Subscribe before http is terminated view1 ----+---| \ http ------1 \ view2 ----1--> switch ----1--> Recreation

Slide 92

Slide 92 text

Case #2: Subscribe before http is terminated view1 ----+---| \ http ------1 \ view2 ----1--> switch ----1--> Re-connect to previous stream

Slide 93

Slide 93 text

Case #2: Subscribe before http is terminated view1 ----+---| \ http ------1 \ view2 ----1--> switch ----1--> Loading

Slide 94

Slide 94 text

Case #2: Subscribe before http is terminated view1 ----+---| \ http ------1 \ view2 ----1--> switch ----1--> Result

Slide 95

Slide 95 text

No content

Slide 96

Slide 96 text

Compose everything

Slide 97

Slide 97 text

kotlin-maze https://github.com/importre/kotlin-maze

Slide 98

Slide 98 text

Maze • A simple way to implement applications using observable streams

Slide 99

Slide 99 text

HTTP with Maze

Slide 100

Slide 100 text

View Model #1

Slide 101

Slide 101 text

UsersModel - immutable data class UsersModel( val user: User = User(0, "", ""), val loading: Boolean = false, ) Data Class

Slide 102

Slide 102 text

Maze & Listener #2

Slide 103

Slide 103 text

class UsersFragment : BaseFragment(), MazeListener { override val layoutId: Int = R.layout.fragment_users private val maze by lazy { Maze(UsersModel()) } @Inject lateinit var api: Api override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) MazeApp.comp.inject(this) } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { maze.attach(this, arrayOf( toolbar.navigationClicks() .map { ClickEvent(R.id.homeAsUp) }, buttonUser.clicks() .map { ClickEvent(R.id.buttonUser) } )) } override fun onDestroyView() { maze.detach() super.onDestroyView() } override fun main(sources: Sources) = usersMain(sources, api) override fun render(prev: UsersModel, curr: UsersModel) { if (curr.loading) { progress.show() return } progress.hide() textName.text = curr.user.name textEmail.text = curr.user.email textName.setTextSize(TypedValue.COMPLEX_UNIT_SP, curr.nameSize) textEmail.setTextSize(TypedValue.COMPLEX_UNIT_SP, curr.nameSize) } override fun navigate(navigation: Navigation) { when (navigation) { is Back -> activity?.onBackPressed() } } override fun finish() = maze.finish() override fun error(t: Throwable) { t.printStackTrace() } }

Slide 104

Slide 104 text

Initialize maze with initial model class UsersFragment : BaseFragment(), MazeListener { override val layoutId: Int = R.layout.fragment_users private val maze by lazy { Maze(UsersModel()) } @Inject lateinit var api: Api override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) MazeApp.comp.inject(this) }

Slide 105

Slide 105 text

Initialize maze with initial model class UsersFragment : BaseFragment(), MazeListener { override val layoutId: Int = R.layout.fragment_users private val maze by lazy { Maze(UsersModel()) } @Inject lateinit var api: Api override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) MazeApp.comp.inject(this) }

Slide 106

Slide 106 text

Attach w/ user-event-streams, Detach override fun onViewCreated(view: View, savedInstanceState: Bundle?) { maze.attach(this, arrayOf( toolbar.navigationClicks() .map { ClickEvent(R.id.homeAsUp) }, buttonUser.clicks() .map { ClickEvent(R.id.buttonUser) } )) } override fun onDestroyView() { maze.detach() super.onDestroyView() }

Slide 107

Slide 107 text

Attach w/ user-event-streams, Detach override fun onViewCreated(view: View, savedInstanceState: Bundle?) { maze.attach(this, arrayOf( toolbar.navigationClicks() .map { ClickEvent(R.id.homeAsUp) }, buttonUser.clicks() .map { ClickEvent(R.id.buttonUser) } )) } override fun onDestroyView() { maze.detach() super.onDestroyView() }

Slide 108

Slide 108 text

Attach w/ user-event-streams, Detach override fun onViewCreated(view: View, savedInstanceState: Bundle?) { maze.attach(this, arrayOf( toolbar.navigationClicks() .map { ClickEvent(R.id.homeAsUp) }, buttonUser.clicks() .map { ClickEvent(R.id.buttonUser) } )) } override fun onDestroyView() { maze.detach() super.onDestroyView() }

Slide 109

Slide 109 text

Implement main function, Rendering view override fun main(sources: Sources) = usersMain(sources, api) override fun render(prev: UsersModel, curr: UsersModel) { if (curr.loading) { progress.show() return } progress.hide() textName.text = curr.user.name textEmail.text = curr.user.email }

Slide 110

Slide 110 text

Implement main function, Rendering view override fun main(sources: Sources) = usersMain(sources, api) override fun render(prev: UsersModel, curr: UsersModel) { if (curr.loading) { progress.show() return } progress.hide() textName.text = curr.user.name textEmail.text = curr.user.email }

Slide 111

Slide 111 text

Implement main function, Rendering view override fun main(sources: Sources) = usersMain(sources, api) override fun render(prev: UsersModel, curr: UsersModel) { if (curr.loading) { progress.show() return } progress.hide() textName.text = curr.user.name textEmail.text = curr.user.email }

Slide 112

Slide 112 text

Implement main function, Rendering view override fun main(sources: Sources) = usersMain(sources, api) override fun render(prev: UsersModel, curr: UsersModel) { if (curr.loading) { progress.show() return } progress.hide() textName.text = curr.user.name textEmail.text = curr.user.email }

Slide 113

Slide 113 text

Navigate something, Clear resources override fun navigate(navigation: Navigation) { when (navigation) { is Back -> activity?.onBackPressed() } } override fun finish() = maze.finish() override fun error(t: Throwable) { t.printStackTrace() } }

Slide 114

Slide 114 text

Main Function Data Flow

Slide 115

Slide 115 text

fun usersMain(sources: Sources, api: Api): Sinks { val click = sources.event .clicks(R.id.buttonUser) .shareReplay(1) val loading = click .withLatestFrom(sources.model, BiFunction { _: ClickEvent, model: UsersModel -> model.copy(loading = true) }) val users = click .switchMap { api.getUsers() } .withLatestFrom(sources.model, BiFunction { users: Users, model: UsersModel -> val index = Random().nextInt(users.size) model.copy(user = users[index], loading = false) }) val back = sources.event .clicks(R.id.homeAsUp) .map { Back() } val model = Observable .merge(loading, users) .cacheWithInitialCapacity(1) return Sinks(model, back) }

Slide 116

Slide 116 text

Main function: Input -> Output fun usersMain(sources: Sources, api: Api): Sinks { // data flow return Sinks(model, navigation) }

Slide 117

Slide 117 text

Main function: Sources -> Sinks fun usersMain(sources: Sources, api: Api): Sinks { // data flow return Sinks(model, navigation) }

Slide 118

Slide 118 text

Data Flow

Slide 119

Slide 119 text

Input val click = sources.event .clicks(R.id.buttonUser) .shareReplay(1) val loading = click .withLatestFrom(sources.model, BiFunction { _: ClickEvent, model: UsersModel -> model.copy(loading = true) }) val users = click .switchMap { api.getUsers() } .withLatestFrom(sources.model, BiFunction { users: Users, model: UsersModel -> val index = Random().nextInt(users.size) model.copy(user = users[index], loading = false) })

Slide 120

Slide 120 text

Input val click = sources.event .clicks(R.id.buttonUser) .shareReplay(1) val loading = click .withLatestFrom(sources.model, BiFunction { _: ClickEvent, model: UsersModel -> model.copy(loading = true) }) val users = click .switchMap { api.getUsers() } .withLatestFrom(sources.model, BiFunction { users: Users, model: UsersModel -> val index = Random().nextInt(users.size) model.copy(user = users[index], loading = false) })

Slide 121

Slide 121 text

Input val click = sources.event .clicks(R.id.buttonUser) .shareReplay(1) val loading = click .withLatestFrom(sources.model, BiFunction { _: ClickEvent, model: UsersModel -> model.copy(loading = true) }) val users = click .switchMap { api.getUsers() } .withLatestFrom(sources.model, BiFunction { users: Users, model: UsersModel -> val index = Random().nextInt(users.size) model.copy(user = users[index], loading = false) })

Slide 122

Slide 122 text

Input val click = sources.event .clicks(R.id.buttonUser) .shareReplay(1) val loading = click .withLatestFrom(sources.model, BiFunction { _: ClickEvent, model: UsersModel -> model.copy(loading = true) }) val users = click .switchMap { api.getUsers() } .withLatestFrom(sources.model, BiFunction { users: Users, model: UsersModel -> val index = Random().nextInt(users.size) model.copy(user = users[index], loading = false) })

Slide 123

Slide 123 text

No content

Slide 124

Slide 124 text

No content

Slide 125

Slide 125 text

Output val model = Observable .merge(loading, users) .cacheWithInitialCapacity(1) return Sinks(model, navigation)

Slide 126

Slide 126 text

Output val model = Observable .merge(loading, users) .cacheWithInitialCapacity(1) return Sinks(model, navigation)

Slide 127

Slide 127 text

UserModel: Input -> Output -> Input -> Output fun usersMain(sources: Sources, api: Api): Sinks { // data flow return Sinks(model, navigation) }

Slide 128

Slide 128 text

Data Flow: Initial data { "loading": false, "user": { "email": "", "id": 0, "name": "" } }

Slide 129

Slide 129 text

Data Flow: Click, Http { "loading": true, "user": { "email": "", "id": 0, "name": "" } }

Slide 130

Slide 130 text

Data Flow: Done { "loading": false, "user": { "email": "[email protected]", "id": 4, "name": "Patricia Lebsack" } }

Slide 131

Slide 131 text

More Examples • Counter • Progress • Http + + + • Animation • Previous, Current Model • Infinite Scroll • ... https://github.com/importre/kotlin-maze

Slide 132

Slide 132 text

No content

Slide 133

Slide 133 text

How to test

Slide 134

Slide 134 text

메인함수 š 순수함수

Slide 135

Slide 135 text

메인함수 ⊇ 순수함수

Slide 136

Slide 136 text

@Test fun testUiStream() { val user = User(0, "name", "[email protected]") val users = Observable.just(listOf(user)) given(api.getUsers()).willReturn(users) // 1. initialize maze streams val sinks = usersMain(sources, api) // 2. make test observer val testObserver = makeTestObserver(sources, sinks) // 3. click sources.event(ClickEvent(R.id.buttonUser)) // 4. tests testObserver.assertNoErrors() testObserver.assertValues( UsersModel(loading = true), UsersModel(user = user, loading = false) ) testObserver.onComplete() }

Slide 137

Slide 137 text

@Test fun testUiStream() { val user = User(0, "name", "[email protected]") val users = Observable.just(listOf(user)) given(api.getUsers()).willReturn(users) // 1. initialize maze streams val sinks = usersMain(sources, api) // 2. make test observer val testObserver = makeTestObserver(sources, sinks) // 3. click sources.event(ClickEvent(R.id.buttonUser)) // 4. tests testObserver.assertNoErrors() testObserver.assertValues( UsersModel(loading = true), UsersModel(user = user, loading = false) ) testObserver.onComplete() }

Slide 138

Slide 138 text

@Test fun testUiStream() { val user = User(0, "name", "[email protected]") val users = Observable.just(listOf(user)) given(api.getUsers()).willReturn(users) // 1. initialize maze streams val sinks = usersMain(sources, api) // 2. make test observer val testObserver = makeTestObserver(sources, sinks) // 3. click sources.event(ClickEvent(R.id.buttonUser)) // 4. tests testObserver.assertNoErrors() testObserver.assertValues( UsersModel(loading = true), UsersModel(user = user, loading = false) ) testObserver.onComplete() }

Slide 139

Slide 139 text

@Test fun testUiStream() { val user = User(0, "name", "[email protected]") val users = Observable.just(listOf(user)) given(api.getUsers()).willReturn(users) // 1. initialize maze streams val sinks = usersMain(sources, api) // 2. make test observer val testObserver = makeTestObserver(sources, sinks) // 3. click sources.event(ClickEvent(R.id.buttonUser)) // 4. tests testObserver.assertNoErrors() testObserver.assertValues( UsersModel(loading = true), UsersModel(user = user, loading = false) ) testObserver.onComplete() }

Slide 140

Slide 140 text

@Test fun testUiStream() { val user = User(0, "name", "[email protected]") val users = Observable.just(listOf(user)) given(api.getUsers()).willReturn(users) // 1. initialize maze streams val sinks = usersMain(sources, api) // 2. make test observer val testObserver = makeTestObserver(sources, sinks) // 3. click sources.event(ClickEvent(R.id.buttonUser)) // 4. tests testObserver.assertNoErrors() testObserver.assertValues( UsersModel(loading = true), UsersModel(user = user, loading = false) ) testObserver.onComplete() }

Slide 141

Slide 141 text

@Test fun testUiStream() { val user = User(0, "name", "[email protected]") val users = Observable.just(listOf(user)) given(api.getUsers()).willReturn(users) // 1. initialize maze streams val sinks = usersMain(sources, api) // 2. make test observer val testObserver = makeTestObserver(sources, sinks) // 3. click sources.event(ClickEvent(R.id.buttonUser)) // 4. tests testObserver.assertNoErrors() testObserver.assertValues( UsersModel(loading = true), UsersModel(user = user, loading = false) ) testObserver.onComplete() }

Slide 142

Slide 142 text

Conclusion

Slide 143

Slide 143 text

Conclusion

Slide 144

Slide 144 text

Conclusion • рױೣ • ݫੋ ೣࣻ • ചݶ Ӓܻӝ

Slide 145

Slide 145 text

Conclusion • рױೣ • ݫੋ ೣࣻ • ചݶ Ӓܻӝ • ੤ࢎਊ

Slide 146

Slide 146 text

Conclusion • рױೣ • ݫੋ ೣࣻ • ചݶ Ӓܻӝ • ੤ࢎਊ • ೟ण ࠺ਊ • ౱ ࢎ੉ૉ

Slide 147

Slide 147 text

Conclusion • рױೣ • ݫੋ ೣࣻ • ചݶ Ӓܻӝ • ੤ࢎਊ • ೟ण ࠺ਊ • ౱ ࢎ੉ૉ • ০਷ ߑೱ?!

Slide 148

Slide 148 text

감사합니다 &

Slide 149

Slide 149 text

References • http://reactivex.io/ • http://rxmarbles.com/ • https://github.com/importre/kotlin-maze • https://cycle.js.org/