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

What's In Store: KotlinConf19 Mike Nakhimovich ...

Sponsored · Your Podcast. Everywhere. Effortlessly. Share. Educate. Inspire. Entertain. You do you. We'll handle the rest.

What's In Store: KotlinConf19 Mike Nakhimovich Yigit Boyar

Avatar for Mike Nakhimovich

Mike Nakhimovich

December 06, 2019
Tweet

More Decks by Mike Nakhimovich

Other Decks in Programming

Transcript

  1. We stand on shoulders of giant (libraries) • Fetch Data

    - Retrofit, Okhttp, Apollo, Firebase
  2. We stand on shoulders of giant (libraries) • Fetch Data

    - Retrofit, Okhttp, Apollo, Firebase • Cache - Room, SQLDelight, Okio
  3. We stand on shoulders of giant (libraries) • Fetch Data

    - Retrofit, Okhttp, Apollo, Firebase • Cache - Room, SQLDelight, Okio • Transmitting - Rxjava, LiveData, Flow
  4. https://developer.android.com/jetpack/docs/guide Fragment/ Activity ViewModel LiveData Repository Local Source Remote Data

    Source Jetpack Jetpack Jetpack Square Figure it out yourself lolz https://developer.android.com/jetpack/docs/guide
  5. The Repository Pattern Repositories are classes or components that encapsulate

    the logic required to access data sources. https://docs.microsoft.com/en-us/dotnet/architecture/microservices/microservice-ddd-cqrs-patterns/infrastructure-persistence-layer-design
  6. What does repository do ? • Centralize logic to manage

    data • Remove data retrieval burden from UI code
  7. What does repository do ? • Centralize logic to manage

    data • Remove data retrieval burden from UI code • Enable Data Driven UI’s
  8. Sharing of responses was important Fragment1 ConfigApi Fragment2 Fragment3 FragmentN

    get() get() get() get() get() get() get() get() Why NY Times made Store
  9. Sharing of responses was important Fragment1 ConfigApi Fragment2 Fragment3 FragmentN

    Users Wallet get() get() get() get() Why NY Times made Store
  10. RxJava was new & we wanted abstractions switchIfEmpty concat first

    Maybe Single Observable cache onErrorResumeNext empty map filter doOnSuccess error doAfterTerminate PublishSubject hide startWith repeatWhen Why NY Times released Store
  11. Store gave consistent apis for all requests Get = I

    want data don’t care source Fresh = I want new data
  12. Fast forward 3 years Biggest issue: observable sources public interface

    Store<T, V> { Single<T> get(V key); Single<T> fetch(V key); // returns an Observable that emits “fresh" any response // from the store that hit the fetcher Observable<T> stream(); }
  13. public interface Store<T, V> { Single<T> get(V key); Single<T> fetch(V

    key); // returns an Observable that emits “fresh" any response // from the store that hit the fetcher Observable<T> stream(); } Fast forward 3 years Biggest issue: observable sources
  14. Rxjava - warns you when you might leak 
 //

    Flowable.java @CheckReturnValue public final Disposable subscribe(Consumer<? super T> onNext) { } Structured Concurrency
  15. Rxjava - warns you when you might leak 
 //

    Flowable.java @CheckReturnValue public final Disposable subscribe(Consumer<? super T> onNext) { } Structured Concurrency
  16. Coroutines - forces you to not leak 
 public fun

    CoroutineScope.launch(...) Structured Concurrency
  17. Coroutines - forces you to not leak 
 public fun

    CoroutineScope.launch(...) lifecycleScope.launch { Structured Concurrency
  18. Coroutines - forces you to not leak 
 public fun

    CoroutineScope.launch(...) lifecycleScope.launch { // scoped to the lifecycle } Structured Concurrency
  19. Building a Store StoreBuilder .fromNonFlow(api::listFiles) .persister( reader = dao::read, writer

    = dao::write) suspend fun write(path: String, files: List<File>)
  20. Simple case: I want data & updates val store =

    StoreBuilder .from(api::listFiles)
  21. Simple case: I want data & updates val store =

    StoreBuilder .from(api::listFiles) .withPersister(
  22. Simple case: I want data & updates val store =

    StoreBuilder .from(api::listFiles) .withPersister( reader = dao::load,
  23. Simple case: I want data & updates val store =

    StoreBuilder .from(api::listFiles) .withPersister( reader = dao::load, writer = dao::insert
  24. Simple case: I want data & updates val store =

    StoreBuilder .from(api::listFiles) .withPersister( reader = dao::load, writer = dao::insert ).build()
  25. val store = StoreBuilder .from(api::listFiles) .withPersister( reader = dao::load, writer

    = dao::insert ).build() Simple case: I want data & updates
  26. val store = StoreBuilder .from(api::listFiles) .withPersister( reader = dao::load, writer

    = dao::insert ).build() store.get( Simple case: I want data & updates
  27. val store = StoreBuilder .from(api::listFiles) .withPersister( reader = dao::load, writer

    = dao::insert ).build() store.get( path, Simple case: I want data & updates
  28. val store = StoreBuilder .from(api::listFiles) .withPersister( reader = dao::load, writer

    = dao::insert ).build() store.get( path, refresh = false Simple case: I want data & updates
  29. val store = StoreBuilder .from(api::listFiles) .withPersister( reader = dao::load, writer

    = dao::insert ).build() store.get( path, refresh = false ).collect { Simple case: I want data & updates
  30. val store = StoreBuilder .from(api::listFiles) .withPersister( reader = dao::load, writer

    = dao::insert ).build() store.get( path, refresh = false ).collect { updateUI(it) Simple case: I want data & updates
  31. val store = StoreBuilder .from(api::listFiles) .withPersister( reader = dao::load, writer

    = dao::insert ).build() store.get( path, refresh = false ).collect { updateUI(it) } Simple case: I want data & updates
  32. Memory Disk Network Store store.get( path, refresh = false ).collect

    { updateUI(it) } 1 1 StoreRequest: Path, Refresh
  33. Memory Disk Network Store store.get( path, refresh = false ).collect

    { updateUI(it) } 1 StoreRequest: Path, Refresh Flow
  34. Memory Disk Network Store store.get( path, refresh = false ).collect

    { updateUI(it) } 1 1 StoreRequest: Path, Refresh Flow 1
  35. Memory Disk Network Store store.get( path, refresh = false ).collect

    { updateUI(it) } 1 StoreRequest: Path, Refresh Flow 1
  36. Memory Disk Network Store store.get( path, refresh = true ).collect

    { updateUI(it) } 1 1 1 StoreRequest: Path, Refresh
  37. Memory Disk Network Store Flow store.get( path, refresh = true

    ).collect { updateUI(it) } 1 1 1 StoreRequest: Path, Refresh
  38. Memory Disk Network Store Flow store.get( path, refresh = true

    ).collect { updateUI(it) } 1 1 StoreRequest: Path, Refresh 1
  39. Memory Disk Network Store Flow store.get( path, refresh = true

    ).collect { updateUI(it) } 1 1 StoreRequest: Path, Refresh 1
  40. Memory Disk Network Store Flow store.get( path, refresh = true

    ).collect { updateUI(it) } 1 1 StoreRequest: Path, Refresh 1 LOADING
  41. Memory Disk Network Store Flow store.get( path, refresh = true

    ).collect { updateUI(it) } 1 1 StoreRequest: Path, Refresh 1
  42. Memory Source Of Truth Network Store store.get( path, refresh =

    false ).collect { updateUI(it) } 1 StoreRequest: Path, Cached
  43. Memory Source Of Truth Network Store Flow store.get( path, refresh

    = false ).collect { updateUI(it) } 1 StoreRequest: Path, Cached
  44. Memory Source Of Truth Network Store Flow store.get( path, refresh

    = false ).collect { updateUI(it) } 1 1 StoreRequest: Path, Cached 1
  45. Memory Source Of Truth Network Store Flow store.get( path, refresh

    = false ).collect { updateUI(it) } 1 StoreRequest: Path, Cached 1
  46. Memory Source Of Truth Network Store Flow store.get( path, refresh

    = false ).collect { updateUI(it) } 1 StoreRequest: Path, Cached 2
  47. Memory Source Of Truth Network Store Flow store.get( path, refresh

    = false ).collect { updateUI(it) } 1 StoreRequest: Path, Cached 2 2
  48. List Files Screen fetch files Add Folder Screen Screen add

    new folder List Updated Files Screen fetch files
  49. List Files Screen fetch files Add Folder Screen Screen add

    new folder List Updated Files Screen fetch files Local Source of Truth
  50. List Files Screen fetch files Add Folder Screen Screen add

    new folder List Updated Files Screen fetch files Local Source of Truth Is this Store ?
  51. Should Store be The Source of Truth? VS Local Storage

    Store put(Key, Value) load(Key): Value? Local Storage Store put(Key, Value) load(Key): Flow<Value?>
  52. Should Store be The Source of Truth? VS Local Storage

    Store put(Key, Value) load(Key): Value? Local Storage Store put(Key, Value) load(Key): Flow<Value?>
  53. Should Store be The Source of Truth? • PRO •

    Easy for Store to track invalidations
  54. Should Store be The Source of Truth? • PRO •

    Easy for Store to track invalidations • CON
  55. Should Store be The Source of Truth? • PRO •

    Easy for Store to track invalidations • CON • Hard to migrate
  56. Should Store be The Source of Truth? • PRO •

    Easy for Store to track invalidations • CON • Hard to migrate • Developer needs to “report back” to Store
  57. We stand on shoulders of giant (libraries) • Both Room

    and SQLDelight supports observable streams!
  58. Supporting source of truth • Fits architecture recommendation -> Easy

    migrations • Allows out of band updates • Simplifies user code -> No tracking, no reporting back
  59. Supporting source of truth • Fits architecture recommendation -> Easy

    migrations • Allows out of band updates • Simplifies user code -> No tracking, no reporting back • Enables partial migration
  60. Source of truth gotchas sealed class StoreResponse<T> { abstract val

    origin: ResponseOrigin data class Loading<T>(...) : StoreResponse<T>() data class Data<T>(...) : StoreResponse<T>() data class Error<T>(...) : StoreResponse<T>() }
  61. Source of truth gotchas sealed class StoreResponse<T> { abstract val

    origin: ResponseOrigin data class Loading<T>(...) : StoreResponse<T>() data class Data<T>(...) : StoreResponse<T>() data class Error<T>(...) : StoreResponse<T>() }
  62. Source of truth gotchas sealed class StoreResponse<T> { abstract val

    origin: ResponseOrigin data class Loading<T>(...) : StoreResponse<T>() data class Data<T>(...) : StoreResponse<T>() data class Error<T>(...) : StoreResponse<T>() } cache | persister | fetcher
  63. Memory Source Of Truth Network Store StoreRequest: Path, Cached store.get(

    path, refresh = true ).collect { updateUI(it) }
  64. Memory Source Of Truth Network Store StoreRequest: Path, Cached store.get(

    path, refresh = true ).collect { updateUI(it) } Flow
  65. Memory Source Of Truth Network Store StoreRequest: Path, Cached StoreRequest:

    Path, Cached store.get( path, refresh = true ).collect { updateUI(it) } Flow Flow
  66. Memory Source Of Truth Network Store StoreRequest: Path, Cached StoreRequest:

    Path, Cached StoreRequest: Path, Cached store.get( path, refresh = true ).collect { updateUI(it) } Flow Flow Flow
  67. Memory Source Of Truth Network Store StoreRequest: Path, Cached StoreRequest:

    Path, Cached StoreRequest: Path, Cached store.get( path, refresh = true ).collect { updateUI(it) } Flow Flow Flow LOADING
  68. Memory Source Of Truth Network Store StoreRequest: Path, Cached StoreRequest:

    Path, Cached StoreRequest: Path, Cached store.get( path, refresh = true ).collect { updateUI(it) } Flow Flow Flow LOADING
  69. Memory Source Of Truth Network Store StoreRequest: Path, Cached StoreRequest:

    Path, Cached StoreRequest: Path, Cached store.get( path, refresh = true ).collect { updateUI(it) } Flow Flow Flow LOADING LOADING LOADING
  70. Memory Source Of Truth Network Store StoreRequest: Path, Cached StoreRequest:

    Path, Cached StoreRequest: Path, Cached store.get( path, refresh = true ).collect { updateUI(it) } Flow Flow Flow
  71. Memory Source Of Truth Network Store StoreRequest: Path, Cached StoreRequest:

    Path, Cached StoreRequest: Path, Cached store.get( path, refresh = true ).collect { updateUI(it) } Flow Flow Flow
  72. Memory Source Of Truth Network Store StoreRequest: Path, Cached StoreRequest:

    Path, Cached StoreRequest: Path, Cached store.get( path, refresh = true ).collect { updateUI(it) } Flow Flow Flow 1
  73. Memory Source Of Truth Network Store StoreRequest: Path, Cached StoreRequest:

    Path, Cached StoreRequest: Path, Cached store.get( path, refresh = true ).collect { updateUI(it) } Flow Flow Flow 1 1
  74. Memory Source Of Truth Network Store StoreRequest: Path, Cached StoreRequest:

    Path, Cached StoreRequest: Path, Cached store.get( path, refresh = true ).collect { updateUI(it) } Flow Flow Flow 1 1 1
  75. Memory Source Of Truth Network Store StoreRequest: Path, Cached StoreRequest:

    Path, Cached StoreRequest: Path, Cached store.get( path, refresh = true ).collect { updateUI(it) } Flow Flow Flow 1 1 1 1 1
  76. Memory Source Of Truth Network Store StoreRequest: Path, Cached StoreRequest:

    Path, Cached StoreRequest: Path, Cached store.get( path, refresh = true ).collect { updateUI(it) } Flow Flow Flow 1 1
  77. Context Preservation val flow:Flow<Value> = …. val shared = flow.share()

    val collector1 = shared.onEach { ... }.launchIn(IO) val collector2 = shared.onEach { ... }.launchIn(COMPUTATION)
  78. Context Preservation val flow:Flow<Value> = …. val shared = flow.share()

    val collector1 = shared.onEach { ... }.launchIn(IO) val collector2 = shared.onEach { ... }.launchIn(COMPUTATION)
  79. Context Preservation val flow:Flow<Value> = …. val shared = flow.share()

    val collector1 = shared.onEach { ... }.launchIn(IO) val collector2 = shared.onEach { ... }.launchIn(COMPUTATION)
  80. Context Preservation val flow:Flow<Value> = …. val shared = flow.share()

    val collector1 = shared.onEach { ... }.launchIn(IO) val collector2 = shared.onEach { ... }.launchIn(COMPUTATION) WHICH SCOPE DO WE EMIT IN?
  81. Context Preservation val flow:Flow<Value> = …. val shared = flow.share(

    ) val collector1 = shared.onEach { ... }.launchIn(IO) val collector2 = shared.onEach { ... }.launchIn(COMPUTATION)
  82. Context Preservation val flow:Flow<Value> = …. val shared = flow.share(

    ) val collector1 = shared.onEach { ... }.launchIn(IO) val collector2 = shared.onEach { ... }.launchIn(COMPUTATION) upstreamScope
  83. Store Multicast Producer ChannelManager Multicaster Entry point Core of the

    implementation - An Actor Upstream wrapper to work with ChannelManager
  84. Store Multicast ChannelManager Producer Downstream Flow 1 addChannel Create producer

    Downstream Flow 2 addChannel Receive 1 if buffering is enabled
  85. Store Multicast ChannelManager Downstream Flow 2 Downstream Flow 1 2

    ack 2 Downstream Flow 3 addChannel cancel cancel
  86. Store Multicast ChannelManager Downstream Flow 2 Downstream Flow 1 2

    ack 2 Downstream Flow 3 addChannel cancel cancel didn’t receive any value
  87. Store Multicast ChannelManager Downstream Flow 3 addChannel Downstream Flow 2

    Downstream Flow 1 2 ack 2 cancel cancel didn’t receive any value
  88. TestCoroutineScope • Scope for testing coroutines • Very good at

    catching unnecessary work • kotlinx-coroutines-test
  89. TestCoroutineScope • Scope for testing coroutines • Very good at

    catching unnecessary work • Easy to produce control parallel work order • kotlinx-coroutines-test
  90. Catching work leaks val flow = channelFlow { myScope.launch {

    send(1) delay(1000) send(2) }.join() }
  91. private val testScope = TestCoroutineScope() @Test fun test() = testScope.runBlockingTest

    { assertThat( flow.take(2).toList() ).contains(1, 2) } Catching work leaks
  92. private val testScope = TestCoroutineScope() @Test fun test() = testScope.runBlockingTest

    { assertThat( flow.take(2).toList() ).contains(1, 2) } Catching work leaks
  93. private val testScope = TestCoroutineScope() @Test fun test() = testScope.runBlockingTest

    { assertThat( flow.take(2).toList() ).contains(1, 2) } java.lang.IllegalStateException: This job has not completed yet Catching work leaks
  94. val flow = channelFlow { myScope.launch { send(1) delay(1000) send(2)

    }.join() } Will only cancel here Catching work leaks
  95. private val testScope = TestCoroutineScope() @Test fun test() = testScope.runBlockingTest

    { assertThat( flow.take(2).toList() ).contains(1, 2) } Catching work leaks
  96. private val testScope = TestCoroutineScope() @Test fun test() = testScope.runBlockingTest

    { assertThat( flow.take(2).toList() ).contains(1, 2) } Catching work leaks
  97. private val testScope = TestCoroutineScope() @Test fun test() = testScope.runBlockingTest

    { assertThat( flow.take(2).toList() ).contains(1, 2) } ✅ 100 ms Catching work leaks
  98. private val testScope = TestCoroutineScope() @Test fun test() = testScope.runBlockingTest

    { assertThat( flow.take(2).toList() ).contains(1, 2) } ✅ 100 ms Delays are fake! :) Catching work leaks
  99. val flowA = flowOf(1, 2, 3) val flowB = flowOf(7,

    8, 9) Predictable parallel work
  100. val flowA = flowOf(1, 2, 3) val flowB = flowOf(7,

    8, 9) val subject = TestSubject(flowA.merge(flowB)) Predictable parallel work
  101. val flowA = flowOf(1, 2, 3) val flowB = flowOf(7,

    8, 9) val subject = TestSubject(flowA.merge(flowB)) Predictable parallel work 1 2 3 7 9 8
  102. Predictable parallel work val flowA = flowOf(1, 2, 3).onEach {

    delay(1) } val flowB = flowOf(7, 8, 9) val subject = TestSubject(flowA.merge(flowB))
  103. Predictable parallel work val flowA = flowOf(1, 2, 3).onEach {

    delay(1) } val flowB = flowOf(7, 8, 9) val subject = TestSubject(flowA.merge(flowB)) 7 9 8
  104. Predictable parallel work val flowA = flowOf(1, 2, 3).onEach {

    delay(1) } val flowB = flowOf(7, 8, 9) val subject = TestSubject(flowA.merge(flowB)) 1 2 3 7 9 8
  105. Predictable parallel work val flowA = flowOf(1, 2, 3).onEach {

    delay(2) } val flowB = flowOf(7, 8, 9).onEach { delay(5) } val subject = TestSubject(flowA.merge(flowB))
  106. Predictable parallel work val flowA = flowOf(1, 2, 3).onEach {

    delay(2) } val flowB = flowOf(7, 8, 9).onEach { delay(5) } val subject = TestSubject(flowA.merge(flowB)) 1 2
  107. Predictable parallel work val flowA = flowOf(1, 2, 3).onEach {

    delay(2) } val flowB = flowOf(7, 8, 9).onEach { delay(5) } val subject = TestSubject(flowA.merge(flowB)) 1 2 7
  108. Predictable parallel work val flowA = flowOf(1, 2, 3).onEach {

    delay(2) } val flowB = flowOf(7, 8, 9).onEach { delay(5) } val subject = TestSubject(flowA.merge(flowB)) 1 2 3 7 9 8