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

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)

Ubiratan Soares

April 08, 2019
Tweet

More Decks by Ubiratan Soares

Other Decks in Programming

Transcript

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

    to Android codebases and products BLOCKKED, DISTILLED
  2. @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) } } }
  3. @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) } } }
  4. val words = Observable.just("Adenor", "Leonardo", "Bacchi") words.test() .assertComplete() .assertTerminated() .assertNoErrors()

    .assertValueSequence(listOf("Adenor", "Leonardo", "Bacchi")) #$ %&' more verifications
  5. 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" } }
  6. 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" } }
  7. @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 } } }
  8. @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 } } }
  9. Concrete Derivative (conforms to Domain Service) ViewModel Activity External World

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

    Disposable Domain Service Observable<D2> External World Bridge Observable<P2>
  11. 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
  12. 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
  13. Data structures (“models") Services (indirections) Combinators (services + formatters +

    entities) Formatters Standalone Entities (Behaviours) ETC Application Domain
  14. 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) } }
  15. 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) } }
  16. 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) } }
  17. 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) } }
  18. A Senior Engineer knows that composition should be preferred over

    inheritance Therefore, you should EVER avoid inherit from a given use-case base class
  19. 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 } } }
  20. 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 } } }
  21. 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 } } }
  22. 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()) } }
  23. 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 } }
  24. 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 } }
  25. 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 }
  26. 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 }
  27. object ExecutionErrorHandler<T>() : ObservableTransformer<T, T> { override fun apply(upstream: Observable<T>)

    = upstream .compose(HandleErrorByHttpStatus) .compose(HandleConnectivityIssue) .compose(HandleSerializationError) }
  28. 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> }
  29. 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" ) } }
  30. internal class BlockchainInfoInfrastructureTests { @get:Rule val rule = InfrastructureRule() lateinit

    var infrastructure: BrokerInfrastructure @Before fun `before each test`() { infrastructure = BrokerInfrastructure( service = rule.api, ) }
  31. 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`() {
  32. 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
  33. ViewModel Observable<Execution> Disposable AndroidSchedulers Activity class SomeViewModel { fun someExecution()

    : Observable<Execution> { #$ %&' } } Lifecycle Control Threading Control
  34. 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>()
  35. 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) } }
  36. 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) } }
  37. 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) } }
  38. 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) } }
  39. 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) } }
  40. class DashboardViewModel( private val machine: StateMachine<List<DashboardPresentation,-, private val usecase: RetrieveStatistics)

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

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

    { fun retrieveDashboard() = usecase .execute() .map { BuildDashboardPresentation(it) } .compose(machine) }
  43. 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)
  44. 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() } }
  45. 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
  46. 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() } }
  47. 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() } #$ %&' }
  48. app module (plus variants) package package package package package package

    package package package package package package package package package package package package
  49. 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
  50. 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
  51. 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?
  52. 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
  53. Eventually, let the composition of your modules to be “inspired”

    by the design your architectural layers specially when extracting modules from an existing codebase
  54. 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
  55. 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
  56. 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 (?)
  57. 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 (?)
  58. 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
  59. UBIRATAN SOARES Computer Scientist by ICMC/USP Software Engineer, curious guy

    Google Developer Expert for Android / Kotlin Teacher, speaker, etc, etc