Blockked, Distilled

Blockked, Distilled

Reviewing opinionated architectural decisions applied to Android codebases and products. Presentation given at the following events

- Dextra Special Meetup (Campinas, April 2019)

D4b7a3e2ed10f86e0b52498713ba2601?s=128

Ubiratan Soares

April 08, 2019
Tweet

Transcript

  1. Ubiratan Soares April / 2019 Reviewing opinionated architectural decisions applied

    to Android codebases and products BLOCKKED, DISTILLED
  2. https://github.com/ubiratansoares/blockked Blockked A blockchain.info companion (and educational) app for Android

  3. DSL❤

  4. Extensions Functions Lambda Extensions Operators Overload Extension Properties Infix Notation

    Trailing Notation ETC Invoking Instances DSL Markers
  5. https://github.com/ubiratansoares/burster

  6. @Test fun `should handle error when caught from proper networking

    exception`() { using<Throwable, NetworkingIssue> { burst { values(UnknownHostException("No Internet"), HostUnreachable) values(ConnectException(), HostUnreachable) values(SocketTimeoutException(), OperationTimeout) values(NoRouteToHostException(), HostUnreachable) values(IOException("Canceled"), ConnectionSpike) } thenWith { incoming, expected !" val execution = Observable.error<Any>(incoming) assertHandling(execution, expected) } } }
  7. @Test fun `should handle error when caught from proper networking

    exception`() { using<Throwable, NetworkingIssue> { burst { values(UnknownHostException("No Internet"), HostUnreachable) values(ConnectException(), HostUnreachable) values(SocketTimeoutException(), OperationTimeout) values(NoRouteToHostException(), HostUnreachable) values(IOException("Canceled"), ConnectionSpike) } thenWith { incoming, expected !" val execution = Observable.error<Any>(incoming) assertHandling(execution, expected) } } }
  8. https://github.com/ubiratansoares/tite

  9. val words = Observable.just("Adenor", "Leonardo", "Bacchi") words.test() .assertComplete() .assertTerminated() .assertNoErrors()

    .assertValueSequence(listOf("Adenor", "Leonardo", "Bacchi")) #$ %&' more verifications
  10. val words = Observable.just("Adenor", "Leonardo", "Bacchi") words.test() .assertComplete() .assertTerminated() .assertNoErrors()

    .assertValueSequence(listOf("Adenor", "Leonardo", "Bacchi")) #$ %&' more verifications given(words) { assertThatSequence { should be completed should be terminated should notBe broken } verifyForEmissions { items match sequenceOf("Adenor", "Leonardo", "Bacchi") firstItem shouldBe "Adenor" never emmits "Parreira" } }
  11. val words = Observable.just("Adenor", "Leonardo", "Bacchi") words.test() .assertComplete() .assertTerminated() .assertNoErrors()

    .assertValueSequence(listOf("Adenor", "Leonardo", "Bacchi")) #$ %&' more verifications given(words) { assertThatSequence { should be completed should be terminated should notBe broken } verifyForEmissions { items match sequenceOf("Adenor", "Leonardo", "Bacchi") firstItem shouldBe "Adenor" never emmits "Parreira" } }
  12. @Test fun `should fetch from local cache, with cache hit`()

    { `cache has previous data`() val execution = fetcher.execute( SupportedStatistic.AverageMarketPrice, FetchStrategy.FromPrevious ) val mapped = BitcoinInfoMapper(PREVIOUSLY_CACHED) given(execution) { assertThatSequence { should be completed } verifyForEmissions { firstItem shouldBe mapped } } }
  13. @Test fun `should fetch from local cache, with cache hit`()

    { `cache has previous data`() val execution = fetcher.execute( SupportedStatistic.AverageMarketPrice, FetchStrategy.FromPrevious ) val mapped = BitcoinInfoMapper(PREVIOUSLY_CACHED) given(execution) { assertThatSequence { should be completed } verifyForEmissions { firstItem shouldBe mapped } } }
  14. https://youtu.be/Qaqr3h8RUn8

  15. None
  16. "Sandwich by Convention?” 1

  17. Domain Service(indirection) Concrete Derivative (low level detail) Use case ViewModel

    UI Delivery (Activity)
  18. Domain Service (indirection) Concrete Derivative (low level detail) Use case

    ViewModel UI Delivery (Activity)
  19. Domain Service (indirection) Concrete Derivative (low level detail) ViewModel UI

    Delivery (Activity)
  20. Repository Implementation ViewModel Activity Retrofit Observable<Data> Observable<Data> Observable<Data> Disposable

  21. "In the long term duplication is by far cheaper than

    the wrong abstraction”
  22. Concrete Derivative (conforms to Domain Service) ViewModel Activity External World

    Bridge Observable<D> Observable<V> Observable<P> Disposable D != P (preferred) D != V (eventually)
  23. Domain Service ViewModel Activity External World Bridge Observable<D1> Observable<V> Observable<P1>

    Disposable Domain Service Observable<D2> External World Bridge Observable<P2>
  24. Domain Service ViewModel External World Bridge Observable<D1> Observable<V> Observable<P1> Domain

    Service Observable<D2> External World Bridge Observable<P2> Use-case Observable<D1+D2> Disposable Activity
  25. Domain Service ViewModel External World Bridge Observable<D1> Observable<V> Observable<P1> Domain

    Service Observable<D2> External World Bridge Observable<P2> Use-case Observable<D1+D2> Disposable Activity
  26. Data structures (“models") Services (indirections) Combinators (services + formatters +

    entities) Formatters Standalone Entities (Behaviours) ETC Application Domain
  27. "In the application domain, the programming language should be the

    ultimate level of abstraction”
  28. None
  29. Idealism Idealism Language + frameworks

  30. class FetcherStrategist( private val remote: BlockchainInfoService, private val local: CacheService

    ) : FetchBitcoinStatistic { override fun execute(target: SupportedStatistic, strategy: FetchStrategy) = when (strategy) { is ForceUpdate !" remoteThenCache(target) is FromPrevious !" fromCache(target) } private fun fromCache(target: SupportedStatistic) = local.retrieveOrNull(target) ()let { Observable.just(BitcoinInfoMapper(it)) } *+ Observable.empty() private fun remoteThenCache(statistic: SupportedStatistic) = remote .fetchStatistics(statistic) .doOnNext { local.save(statistic, it) } .map { BitcoinInfoMapper(it) } }
  31. class FetcherStrategist( private val remote: BlockchainInfoService, private val local: CacheService

    ) : FetchBitcoinStatistic { override fun execute(target: SupportedStatistic, strategy: FetchStrategy) = when (strategy) { is ForceUpdate !" remoteThenCache(target) is FromPrevious !" fromCache(target) } private fun fromCache(target: SupportedStatistic) = local.retrieveOrNull(target) ()let { Observable.just(BitcoinInfoMapper(it)) } *+ Observable.empty() private fun remoteThenCache(statistic: SupportedStatistic) = remote .fetchStatistics(statistic) .doOnNext { local.save(statistic, it) } .map { BitcoinInfoMapper(it) } }
  32. class FetcherStrategist( private val remote: BlockchainInfoService, private val local: CacheService

    ) : FetchBitcoinStatistic { override fun execute(target: SupportedStatistic, strategy: FetchStrategy) = when (strategy) { is ForceUpdate !" remoteThenCache(target) is FromPrevious !" fromCache(target) } private fun fromCache(target: SupportedStatistic) = local.retrieveOrNull(target) ()let { Observable.just(BitcoinInfoMapper(it)) } *+ Observable.empty() private fun remoteThenCache(statistic: SupportedStatistic) = remote .fetchStatistics(statistic) .doOnNext { local.save(statistic, it) } .map { BitcoinInfoMapper(it) } }
  33. class FetcherStrategist( private val remote: BlockchainInfoService, private val local: CacheService

    ) : FetchBitcoinStatistic { override fun execute(target: SupportedStatistic, strategy: FetchStrategy) = when (strategy) { is ForceUpdate !" remoteThenCache(target) is FromPrevious !" fromCache(target) } private fun fromCache(target: SupportedStatistic) = local.retrieveOrNull(target) ()let { Observable.just(BitcoinInfoMapper(it)) } *+ Observable.empty() private fun remoteThenCache(statistic: SupportedStatistic) = remote .fetchStatistics(statistic) .doOnNext { local.save(statistic, it) } .map { BitcoinInfoMapper(it) } }
  34. A Senior Engineer knows that composition should be preferred over

    inheritance Therefore, you should EVER avoid inherit from a given use-case base class
  35. class RetrieveStatistics(private val fetcher: FetchBitcoinStatistic) { private val cached by

    lazy { retrieveAll(strategy = FetchStrategy.FromPrevious) } private val updated by lazy { retrieveAll(strategy = FetchStrategy.ForceUpdate) } fun execute() = cached.concatWith(updated) private fun retrieveAll(strategy: FetchStrategy) = Observable .fromIterable(SupportedStatistic.ALL) .flatMap { Observable.just(fetcher.execute(it, strategy)) } .let { Observable.zip(it, Zipper) } private companion object Zipper : Function<Array<Any>, List<BitcoinStatistic,- { override fun apply(raw: Array<Any>) = raw.map { it as BitcoinStatistic } } }
  36. class RetrieveStatistics(private val fetcher: FetchBitcoinStatistic) { private val cached by

    lazy { retrieveAll(strategy = FetchStrategy.FromPrevious) } private val updated by lazy { retrieveAll(strategy = FetchStrategy.ForceUpdate) } fun execute() = cached.concatWith(updated) private fun retrieveAll(strategy: FetchStrategy) = Observable .fromIterable(SupportedStatistic.ALL) .flatMap { Observable.just(fetcher.execute(it, strategy)) } .let { Observable.zip(it, Zipper) } private companion object Zipper : Function<Array<Any>, List<BitcoinStatistic,- { override fun apply(raw: Array<Any>) = raw.map { it as BitcoinStatistic } } }
  37. class RetrieveStatistics(private val fetcher: FetchBitcoinStatistic) { private val cached by

    lazy { retrieveAll(strategy = FetchStrategy.FromPrevious) } private val updated by lazy { retrieveAll(strategy = FetchStrategy.ForceUpdate) } fun execute() = cached.concatWith(updated) private fun retrieveAll(strategy: FetchStrategy) = Observable .fromIterable(SupportedStatistic.ALL) .flatMap { Observable.just(fetcher.execute(it, strategy)) } .let { Observable.zip(it, Zipper) } private companion object Zipper : Function<Array<Any>, List<BitcoinStatistic,- { override fun apply(raw: Array<Any>) = raw.map { it as BitcoinStatistic } } }
  38. Rx Transformers : the abstraction everyone should know about 2

  39. class SomeBehaviour<T> : ObservableTransformer<T, T> { override fun apply(upstream: Observable<T>)=

    TODO() }
  40. class SomeBehaviour<T> : ObservableTransformer<T, T> { override fun apply(upstream: Observable<T>)=

    TODO() } class SomePipeline<T> { override fun someWork(): Observable<T> { return someStream.compose(SomeBehaviour()) } }
  41. object HandleErrorByHttpStatus<T> : ObservableTransformer<T, T> { override fun apply(upstream: Observable<T>):

    ObservableSource<T> { return upstream.onErrorResumeNext(this./handleIfRestError) } private fun handleIfRestError(incoming: Throwable): Observable<T> = if (incoming is HttpException) toInfrastructureError(incoming) else Observable.error(incoming) private fun toInfrastructureError(restError: HttpException): Observable<T> { val infraError = mapErrorWith(restError.code()) return Observable.error(infraError) } private fun mapErrorWith(code: Int) = when (code) { in 40001499 !" RemoteIntegrationIssue.ClientOrigin else !" RemoteIntegrationIssue.RemoteSystem } }
  42. object HandleErrorByHttpStatus<T> : ObservableTransformer<T, T> { override fun apply(upstream: Observable<T>):

    ObservableSource<T> { return upstream.onErrorResumeNext(this::handleIfRestError) } private fun handleIfRestError(incoming: Throwable): Observable<T> = if (incoming is HttpException) toInfrastructureError(incoming) else Observable.error(incoming) private fun toInfrastructureError(restError: HttpException): Observable<T> { val infraError = mapErrorWith(restError.code()) return Observable.error(infraError) } private fun mapErrorWith(code: Int) = when (code) { in 400..499 !" RemoteIntegrationIssue.ClientOrigin else !" RemoteIntegrationIssue.RemoteSystem } }
  43. object HandleConnectivityIssue<T> : ObservableTransformer<T, T> { override fun apply(upstream: Observable<T>):

    ObservableSource<T> = upstream.onErrorResumeNext(this./handleIfNetworkingError) #$ Implementation } object HandleSerializationError<T> : ObservableTransformer<T, T> { override fun apply(upstream: Observable<T>): Observable<T> { return upstream.onErrorResumeNext(this./handleSerializationError) } #$ Implementation }
  44. object HandleConnectivityIssue<T> : ObservableTransformer<T, T> { override fun apply(upstream: Observable<T>):

    ObservableSource<T> = upstream.onErrorResumeNext(this::handleIfNetworkingError) #$ Implementation } object HandleSerializationError<T> : ObservableTransformer<T, T> { override fun apply(upstream: Observable<T>): Observable<T> { return upstream.onErrorResumeNext(this::handleSerializationError) } #$ Implementation }
  45. object ExecutionErrorHandler<T>() : ObservableTransformer<T, T> { override fun apply(upstream: Observable<T>)

    = upstream .compose(HandleErrorByHttpStatus) .compose(HandleConnectivityIssue) .compose(HandleSerializationError) }
  46. internal class BrokerInfrastructure( private val service: BlockchainInfo, private val targetScheduler:

    Scheduler = Schedulers.trampoline() ) : BlockchainInfoService { override fun fetchStatistics(statistic: SupportedStatistic)= service .fetchWith(statistic.toString(), ARGS) .subscribeOn(targetScheduler) .compose(ExecutionErrorHandler) private companion object { val ARGS = mapOf( "timespan" to "4weeks", "format" to "json" ) } } interface BlockchainInfoService { fun fetchStatistics(statistic : SupportedStatistic) : Observable<BitcoinStatsResponse> }
  47. internal class BrokerInfrastructure( private val service: BlockchainInfo, private val targetScheduler:

    Scheduler = Schedulers.trampoline() ) : BlockchainInfoService { override fun fetchStatistics(statistic: SupportedStatistic)= service .fetchWith(statistic.toString(), ARGS) .subscribeOn(targetScheduler) .compose(ExecutionErrorHandler) private companion object { val ARGS = mapOf( "timespan" to "4weeks", "format" to "json" ) } }
  48. internal class BlockchainInfoInfrastructureTests { @get:Rule val rule = InfrastructureRule() lateinit

    var infrastructure: BrokerInfrastructure @Before fun `before each test`() { infrastructure = BrokerInfrastructure( service = rule.api, ) }
  49. rule.defineScenario( status = 200, response = loadFile("200OK-market-price.json") ) @Test fun

    `should retrieve Bitcoin market price with success`() {
  50. val expected = BitcoinStatsResponse( name = "Market Price (USD)", description

    = "Average USD market value across exchanges.", unit = "USD", values = listOf( StatisticPoint( timestamp = 1540166400, value = 6498f ), StatisticPoint( timestamp = 1540252800, value = 6481f ) ) ) @Test fun `should retrieve Bitcoin market price with success`() {
  51. val execution = infrastructure.fetchStatistics(AverageMarketPrice) given(execution) { assertThatSequence { should be

    completed should emit something } verifyForEmissions { firstItem shouldBe expected } } @Test fun `should retrieve Bitcoin market price with success`() { Tested in a integrated fashion
  52. The UI State Machine 3

  53. ViewModel Observable<Execution> Disposable AndroidSchedulers Activity class SomeViewModel { fun someExecution()

    : Observable<Execution> { #$ %&' } } Lifecycle Control Threading Control
  54. None
  55. Launched Result (remote) Done

  56. None
  57. Launched Result (remote) Done Result (local)

  58. None
  59. Launched Failed (remote) Done

  60. None
  61. Launched Failed (remote) Done Result (local)

  62. Launched Failed (reason) Done Result (value)

  63. sealed class UIEvent<out T> object Launched : UIEvent<Nothing>() data class

    Failed(val reason: Throwable) : UIEvent<Nothing>() data class Result<out T>(val value: T) : UIEvent<T>() object Done : UIEvent<Nothing>()
  64. class StateMachine<T>( private val uiScheduler: Scheduler = Schedulers.trampoline() ) :

    ObservableTransformer<T, UIEvent<T,- { override fun apply(upstream: Observable<T>): Observable<UIEvent<T,- { val beginning = Launched val end = Observable.just(Done) return upstream .map { value: T !" Result(value) as UIEvent<T> } .onErrorReturn { error: Throwable !" Failed(error) } .startWith(beginning) .concatWith(end) .observeOn(uiScheduler) } }
  65. class StateMachine<T>( private val uiScheduler: Scheduler = Schedulers.trampoline() ) :

    ObservableTransformer<T, UIEvent<T,- { override fun apply(upstream: Observable<T>): Observable<UIEvent<T,- { val beginning = Launched val end = Observable.just(Done) return upstream .map { value: T !" Result(value) as UIEvent<T> } .onErrorReturn { error: Throwable !" Failed(error) } .startWith(beginning) .concatWith(end) .observeOn(uiScheduler) } }
  66. class StateMachine<T>( private val uiScheduler: Scheduler = Schedulers.trampoline() ) :

    ObservableTransformer<T, UIEvent<T,- { override fun apply(upstream: Observable<T>): Observable<UIEvent<T,- { val beginning = Launched val end = Observable.just(Done) return upstream .map { value: T !" Result(value) as UIEvent<T> } .onErrorReturn { error: Throwable !" Failed(error) } .startWith(beginning) .concatWith(end) .observeOn(uiScheduler) } }
  67. class StateMachine<T>( private val uiScheduler: Scheduler = Schedulers.trampoline() ) :

    ObservableTransformer<T, UIEvent<T,- { override fun apply(upstream: Observable<T>): Observable<UIEvent<T>> { val beginning = Launched val end = Observable.just(Done) return upstream .map { value: T !" Result(value) as UIEvent<T> } .onErrorReturn { error: Throwable !" Failed(error) } .startWith(beginning) .concatWith(end) .observeOn(uiScheduler) } }
  68. class StateMachine<T>( private val uiScheduler: Scheduler = Schedulers.trampoline() ) :

    ObservableTransformer<T, UIEvent<T,- { override fun apply(upstream: Observable<T>): Observable<UIEvent<T>> { val beginning = Launched val end = Observable.just(Done) return upstream .map { value: T !" Result(value) as UIEvent<T> } .onErrorReturn { error: Throwable !" Failed(error) } .startWith(beginning) .concatWith(end) .observeOn(uiScheduler) } }
  69. class DashboardViewModel( private val machine: StateMachine<List<DashboardPresentation,-, private val usecase: RetrieveStatistics)

    { fun retrieveDashboard() = usecase .execute() .map { BuildDashboardPresentation(it) } .compose(machine) }
  70. class DashboardViewModel( private val machine: StateMachine<List<DashboardPresentation>>, private val usecase: RetrieveStatistics)

    { fun retrieveDashboard() = usecase .execute() .map { BuildDashboardPresentation(it) } .compose(machine) }
  71. class DashboardViewModel( private val machine: StateMachine<List<DashboardPresentation>>, private val usecase: RetrieveStatistics)

    { fun retrieveDashboard() = usecase .execute() .map { BuildDashboardPresentation(it) } .compose(machine) }
  72. class DashboardViewModel( private val machine: StateMachine<List<DashboardPresentation,-, private val usecase: RetrieveStatistics)

    { fun retrieveDashboard() = usecase .execute() .map { BuildDashboardPresentation(it) } .compose(machine) } Mapping function runs in the Scheduler defined by the upstream (IO or Computation)
  73. private fun loadDashboard() = viewModel .retrieveDashboard() .subscribeBy( onNext = {

    changeState(it) }, onError = { logger.e("Error !" $it") } ) private fun changeState(event: UIEvent<List<DashboardPresentation,-) { when (event) { is Launched !" startExecution() is Result !" presentDashboard(event.value) is Failed !" reportError(event.reason) is Done !" finishExecution() } }
  74. private fun loadDashboard() = viewModel .retrieveDashboard() .subscribeBy( onNext = {

    changeState(it) }, onError = { logger.e("Error !" $it") } ) private fun changeState(event: UIEvent<List<DashboardPresentation,-) { when (event) { is Launched !" startExecution() is Result !" presentDashboard(event.value) is Failed !" reportError(event.reason) is Done !" finishExecution() } } Threading already handled
  75. private fun loadDashboard() = viewModel .retrieveDashboard() .subscribeBy( onNext = {

    changeState(it) }, onError = { logger.e("Error !" $it") } ) private fun changeState(event: UIEvent<List<DashboardPresentation,-) { when (event) { is Launched -> startExecution() is Result -> presentDashboard(event.value) is Failed -> reportError(event.reason) is Done -> finishExecution() } }
  76. class DashboardActivity : AppCompatActivity(), KodeinAware { private val disposer by

    instance<AutoDisposer>() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_dashboard) setupViews() disposer += loadDashboard() } #$ %&' }
  77. None
  78. app module (plus variants) package package package package package package

    package package package package package package package package package package package package
  79. Why multi-module builds ? Better files-per-feature co-location Better isolation of

    legacy code Better visibility for internal-libs candidates Faster builds with parallel tasks execution Faster builds with more cacheable tasks Dynamic features ready !!! ETC
  80. app module (plus variants) Library Library Library Library Library Library

    Library Library Library Library Feature Feature Feature Feature Feature Feature Feature Feature Feature Feature Feature
  81. Life is harder than this …

  82. Multi-module design "Should this new module be Kotlin-only or Android-library?”

    Can we maximize the number of Kotlin-only modules? How do you handle circular dependencies? How do you share assets between feature modules? How do you handle DI between modules? How do you share configuration logic between Gradle files? Does this design scale?
  83. Learnings from the battlefield …

  84. app module (plus variants) Feature Flow Screen Behavior Feature Domain

    Models Domain Services Combinators Analytics Navigator Feature Flow Infrastructure Infrastructure Infrastructure Networking Database Dependency Injection Logger Formatters Infrastructure Infra ETC Ktx extensions (aka new utils 2.0) Circular dependency hack Shared mess between related features
  85. Eventually, let the composition of your modules to be “inspired”

    by the design your architectural layers specially when extracting modules from an existing codebase
  86. app module (plus variants) Feature Flow Screen Dynamic Feature Domain

    Models Domain Services Combinators Analytics Navigator Feature Flow Infrastructure Infrastructure Infrastructure Networking Database Dependency Injection Logger Formatters Infrastructure Infra ETC Shared extensions Circular dependency hack Shared utilities between Related Features
  87. Instant App Feature Flow Screen Behaviour Feature Domain Models Domain

    Services Combinators Analytics Navigator Feature Flow Infrastructure Infrastructure Infrastructure Networking Database Dependency Injection Logger Formatters Infrastructure Infra ETC Shared extensions Circular dependency hack Shared utilities between Related Features
  88. Going annotation-processors free No to AA libs for each micro-issue

    No to compiler-generated Moshi adapters No to Dagger2 No to kapt No to Room (?)
  89. Going annotation-processors free No to AA libs for each micro-issue

    No to compiler-generated Moshi adapters No to Dagger2 No to kapt No to Room (?)
  90. Build speed + multi-module projects Kotlinx.Serialization for JSON D8 for

    AAC Lifecycle or Java8 desugaring Isolate modules that require kapt Minimize the # of modules relying on kapt If doomed by Dagger2: consider dagger-reflect https://github.com/JakeWharton/dagger-reflect
  91. None
  92. https://speakerdeck.com/ubiratansoares

  93. UBIRATAN SOARES Computer Scientist by ICMC/USP Software Engineer, curious guy

    Google Developer Expert for Android / Kotlin Teacher, speaker, etc, etc
  94. THANK YOU @ubiratanfsoares ubiratansoares.dev https://br.linkedin.com/in/ubiratanfsoares