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

Compose everything with rx & kotlin

Avatar for Jaewe Heo Jaewe Heo
March 20, 2017

Compose everything with rx & kotlin

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

A simple way to implement applications using observable streams

Avatar for Jaewe Heo

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 • рױೣ • ݫੋ ೣࣻ • ചݶ Ӓܻӝ •

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