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
PRO

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

    View Slide

  2. https://github.com/ubiratansoares/blockked
    Blockked
    A blockchain.info companion
    (and educational) app for Android

    View Slide

  3. DSL❤

    View Slide

  4. Extensions
    Functions
    Lambda
    Extensions
    Operators
    Overload
    Extension
    Properties
    Infix
    Notation
    Trailing
    Notation
    ETC
    Invoking
    Instances
    DSL
    Markers

    View Slide

  5. https://github.com/ubiratansoares/burster

    View Slide

  6. @Test fun `should handle error when caught from proper networking exception`() {
    using {
    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(incoming)
    assertHandling(execution, expected)
    }
    }
    }

    View Slide

  7. @Test fun `should handle error when caught from proper networking exception`() {
    using {
    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(incoming)
    assertHandling(execution, expected)
    }
    }
    }

    View Slide

  8. https://github.com/ubiratansoares/tite

    View Slide

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

    View Slide

  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"
    }
    }

    View Slide

  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"
    }
    }

    View Slide

  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
    }
    }
    }

    View Slide

  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
    }
    }
    }

    View Slide

  14. https://youtu.be/Qaqr3h8RUn8

    View Slide

  15. View Slide

  16. "Sandwich by
    Convention?”
    1

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  20. Repository Implementation ViewModel
    Activity
    Retrofit
    Observable
    Observable
    Observable
    Disposable

    View Slide

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

    View Slide

  22. Concrete Derivative
    (conforms to Domain Service)
    ViewModel
    Activity
    External World Bridge
    Observable
    Observable
    Observable
    Disposable
    D != P (preferred)
    D != V (eventually)

    View Slide

  23. Domain Service
    ViewModel
    Activity
    External World Bridge
    Observable
    Observable
    Observable
    Disposable
    Domain Service
    Observable
    External World Bridge
    Observable

    View Slide

  24. Domain Service
    ViewModel
    External World Bridge
    Observable
    Observable
    Observable
    Domain Service
    Observable
    External World Bridge
    Observable
    Use-case
    Observable
    Disposable Activity

    View Slide

  25. Domain Service
    ViewModel
    External World Bridge
    Observable
    Observable
    Observable
    Domain Service
    Observable
    External World Bridge
    Observable
    Use-case
    Observable
    Disposable Activity

    View Slide

  26. Data structures
    (“models")
    Services
    (indirections)
    Combinators
    (services + formatters + entities)
    Formatters
    Standalone Entities
    (Behaviours)
    ETC
    Application Domain

    View Slide

  27. "In the application domain,
    the programming language
    should be the ultimate
    level of abstraction”

    View Slide

  28. View Slide

  29. Idealism Idealism
    Language + frameworks

    View Slide

  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) }
    }

    View Slide

  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) }
    }

    View Slide

  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) }
    }

    View Slide

  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) }
    }

    View Slide

  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

    View Slide

  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, Listoverride fun apply(raw: Array) = raw.map { it as BitcoinStatistic }
    }
    }

    View Slide

  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, Listoverride fun apply(raw: Array) = raw.map { it as BitcoinStatistic }
    }
    }

    View Slide

  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, Listoverride fun apply(raw: Array) = raw.map { it as BitcoinStatistic }
    }
    }

    View Slide

  38. Rx Transformers : the
    abstraction everyone
    should know about
    2

    View Slide

  39. class SomeBehaviour : ObservableTransformer {
    override fun apply(upstream: Observable)=
    TODO()
    }

    View Slide

  40. class SomeBehaviour : ObservableTransformer {
    override fun apply(upstream: Observable)=
    TODO()
    }
    class SomePipeline {
    override fun someWork(): Observable {
    return someStream.compose(SomeBehaviour())
    }
    }

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  45. object ExecutionErrorHandler()
    : ObservableTransformer {
    override fun apply(upstream: Observable) =
    upstream
    .compose(HandleErrorByHttpStatus)
    .compose(HandleConnectivityIssue)
    .compose(HandleSerializationError)
    }

    View Slide

  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
    }

    View Slide

  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"
    )
    }
    }

    View Slide

  48. internal class BlockchainInfoInfrastructureTests {
    @get:Rule val rule = InfrastructureRule()
    lateinit var infrastructure: BrokerInfrastructure
    @Before fun `before each test`() {
    infrastructure = BrokerInfrastructure(
    service = rule.api,
    )
    }

    View Slide

  49. rule.defineScenario(
    status = 200,
    response = loadFile("200OK-market-price.json")
    )
    @Test fun `should retrieve Bitcoin market price with success`() {

    View Slide

  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`() {

    View Slide

  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

    View Slide

  52. The UI
    State Machine
    3

    View Slide

  53. ViewModel
    Observable Disposable
    AndroidSchedulers
    Activity
    class SomeViewModel {
    fun someExecution() : Observable {
    #$ %&'
    }
    }
    Lifecycle Control
    Threading Control

    View Slide

  54. View Slide

  55. Launched
    Result
    (remote)
    Done

    View Slide

  56. View Slide

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

    View Slide

  58. View Slide

  59. Launched
    Failed
    (remote)
    Done

    View Slide

  60. View Slide

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

    View Slide

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

    View Slide

  63. sealed class UIEvent
    object Launched : UIEvent()
    data class Failed(val reason: Throwable) : UIEvent()
    data class Result(val value: T) : UIEvent()
    object Done : UIEvent()

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  69. class DashboardViewModel(
    private val machine: StateMachineprivate val usecase: RetrieveStatistics) {
    fun retrieveDashboard() =
    usecase
    .execute()
    .map { BuildDashboardPresentation(it) }
    .compose(machine)
    }

    View Slide

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

    View Slide

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

    View Slide

  72. class DashboardViewModel(
    private val machine: StateMachineprivate 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)

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  76. class DashboardActivity : AppCompatActivity(), KodeinAware {
    private val disposer by instance()
    override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_dashboard)
    setupViews()
    disposer += loadDashboard()
    }
    #$ %&'
    }

    View Slide

  77. View Slide

  78. app module (plus variants)
    package package package
    package
    package package package
    package package
    package package
    package package package package package package package

    View Slide

  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

    View Slide

  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

    View Slide

  81. Life is harder
    than this …

    View Slide

  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?

    View Slide

  83. Learnings from
    the battlefield …

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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 (?)

    View Slide

  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 (?)

    View Slide

  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

    View Slide

  91. View Slide

  92. https://speakerdeck.com/ubiratansoares

    View Slide

  93. UBIRATAN
    SOARES
    Computer Scientist by ICMC/USP
    Software Engineer, curious guy
    Google Developer Expert for Android / Kotlin
    Teacher, speaker, etc, etc

    View Slide

  94. THANK YOU
    @ubiratanfsoares
    ubiratansoares.dev
    https://br.linkedin.com/in/ubiratanfsoares

    View Slide