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

Compose everything with rx & kotlin

Compose everything with rx & kotlin

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

A simple way to implement applications using observable streams

Jaewe Heo

March 20, 2017
Tweet

More Decks by Jaewe Heo

Other Decks in Programming

Transcript

  1. 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) } } }
  2. 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
  3. 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
  4. 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
  5. 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
  6. 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
  7. 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() } } }
  8. 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
  9. 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
  10. 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
  11. 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
  12. 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() }) }
  13. 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
  14. 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
  15. 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
  16. 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
  17. !!!!!!!!!… 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 ...
  18. !!!!!!!!!… 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 ...
  19. 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
  20. 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
  21. 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) }
  22. 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<Object> clicks(@NonNull View view) { // ... }
  23. Simple solution: CompositeDisposable abstract class BaseFragment : Fragment() { protected

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

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

    by lazy { CompositeDisposable() } // ... override fun onDestroyView() { disposables.clear() super.onDestroyView() } }
  26. 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
  27. 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) } } // ... }
  28. 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) } } // ... }
  29. 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) } } // ... }
  30. 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) } } // ... }
  31. 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) } } // ... }
  32. 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) } } // ... }
  33. 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
  34. Make proxy User events (e.g. clicks, textChanges …) ——————————————————————————————— ———

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

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

    ——— View Model Stream, Proxy Event Stream ———> ——————————————————————————————— Long-term jobs (e.g. http, database, preferences …)
  37. Case #1: Subscribe after http is terminated view1 ----+---| \

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    http ------1 \ view2 ----1--> switch ----1--> Result
  53. UsersModel - immutable data class UsersModel( val user: User =

    User(0, "", ""), val loading: Boolean = false, ) Data Class
  54. class UsersFragment : BaseFragment(), MazeListener<UsersModel> { 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<UsersModel>) = 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() } }
  55. Initialize maze with initial model class UsersFragment : BaseFragment(), MazeListener<UsersModel>

    { 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) }
  56. Initialize maze with initial model class UsersFragment : BaseFragment(), MazeListener<UsersModel>

    { 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) }
  57. 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() }
  58. 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() }
  59. 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() }
  60. Implement main function, Rendering view override fun main(sources: Sources<UsersModel>) =

    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 }
  61. Implement main function, Rendering view override fun main(sources: Sources<UsersModel>) =

    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 }
  62. Implement main function, Rendering view override fun main(sources: Sources<UsersModel>) =

    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 }
  63. Implement main function, Rendering view override fun main(sources: Sources<UsersModel>) =

    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 }
  64. 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() } }
  65. fun usersMain(sources: Sources<UsersModel>, api: Api): Sinks<UsersModel> { 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) }
  66. Main function: Input -> Output fun usersMain(sources: Sources<UsersModel>, api: Api):

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

    Sinks<UsersModel> { // data flow return Sinks(model, navigation) }
  68. 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) })
  69. 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) })
  70. 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) })
  71. 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) })
  72. UserModel: Input -> Output -> Input -> Output fun usersMain(sources:

    Sources<UsersModel>, api: Api): Sinks<UsersModel> { // data flow return Sinks(model, navigation) }
  73. More Examples • Counter • Progress • Http + +

    + • Animation • Previous, Current Model • Infinite Scroll • ... https://github.com/importre/kotlin-maze
  74. @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() }
  75. @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() }
  76. @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() }
  77. @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() }
  78. @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() }
  79. @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() }
  80. Conclusion • рױೣ • ݫੋ ೣࣻ • ചݶ Ӓܻӝ •

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

    ੤ࢎਊ • ೟ण ࠺ਊ • ౱ ࢎ੉ૉ • ০਷ ߑೱ?!