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

Modularizing Legacy Apps

Modularizing Legacy Apps

Most of the existing Android applications are structured as a single module and this is proven to be a good solution for small projects and teams. Unfortunately, as a project grows, the developer team expands, and the complexity of working in parallel increases, building time raises, and productivity drops. Modularisation is the technique of separating the codebase into independent modules so to isolate specific functionalities, simplify parallel development, and support Dynamic Features. In this presentation I will explain how to modularise a legacy Android application by showing challenges and achievements experienced in the reformatting of Quandoo's application.

5cefbec62d834db21a3823a8e66d59d8?s=128

Marcello Galhardo
PRO

March 01, 2019
Tweet

Transcript

  1. Modularizing Legacy Apps A PRAGMATIC APPROACH Marcello Galhardo Slides at

    https://tinyurl.com/y9zcf4u9
  2. Marcello Galhardo Android Dev @ Quandoo Moderator @ Android Dev

    BR
  3. Why? • Build takes too long • Code ownership •

    Separation of concerns • Dynamic Features
  4. Strategy #1 Quality Directives

  5. • Code must be under continuous integration • Automate the

    build • Test coverage measure the code quality • Small Feature Branches and Pull Requests
  6. • Code must be under continuous integration • Automate the

    build • Test coverage measure the code quality • Small Feature Branches and Pull Requests
  7. Write code Push code to Git Run unit and ui

    tests Build generation Deploy on Store Distribute to QAs
  8. • Code must be under continuous integration • Automate the

    build • Test coverage measure the code quality • Small Feature Branches and Pull Requests
  9. [#ticket] New search page in separated module ? authored 2

    weeks ago Showing 272 changed files with 4582 additions and 365 deletions
  10. New Branch Your Branch Cherry-pick Cherry-pick PR 1 PR 2

    Develop Branch
  11. Strategy #2 Scout the Codebase

  12. • Keep things you need • Donate things someone else

    might need • Discard or recycle the rest
  13. fun View.isVisible() = this.visibility == View.VISIBLE @Deprecated( message = "Replaced

    by ViewKt.", level = DeprecationLevel.ERROR, replaceWith = ReplaceWith( expression = "ViewKt" ) ) object ViewUtils {…}
  14. Strategy #3 Keep It Simple, Stupid

  15. • Don’t create modules that you don’t need • Don’t

    plan too much ahead: Emergent Design • There are no final decisions • "Branch by Abstraction” when extracting modules
  16. Strategy #4 Don’t Talk to Strangers

  17. • Modules shouldn't reveal anything unnecessary to other modules •

    Modules don’t rely on other modules' implementations • Explicitly pass required data into your modules • Avoid similar modules: duplication is a symptom of structural problems
  18. Strategy #5 Don’t Repeat Yourself

  19. • Make it easier to share code • Centralise your

    resources • Share versions of dependencies using variables
  20. • Make it easier to share code • Centralise your

    resources • Share versions of dependencies using variables
  21. App

  22. Feature 2 Feature 1

  23. Feature 2 Feature 1 Common

  24. Feature 2 Feature 1 Common

  25. Build Output :feature2 :feature1 :base Use Use

  26. • Make it easier to share code • Centralise your

    resources • Share versions of dependencies using variables
  27. // ../scripts/gradle/base-build.gradle apply plugin: 'kotlin-android' apply plugin: 'kotlin-android-extensions' apply plugin:

    ‘kotlin-kapt' android { compileSdkVersion compile_sdk_version buildToolsVersion build_tools_version defaultConfig { minSdkVersion min_sdk_version targetSdkVersion target_sdk_version versionCode version_code versionName version_name } buildTypes {…} } dependencies { implementation “org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" }
  28. // Feature module apply plugin: 'com.android.feature' apply from: "../scripts/gradle/base-build.gradle" dependencies

    {…} // Library module apply plugin: 'com.android.library' apply from: “../scripts/gradle/base-build.gradle" dependencies {…}
  29. Strategy #6 Small Interactions

  30. :legacy :feature :facade Invokes new feature Access legacy functionalities by

    well-defined interface and dependency injection Implements interfaces on legacy code
  31. :legacy :feature2 :facade :feature1 :feature3 Extract features from :legacy

  32. :app :feature2 :base :feature1 :feature3 Move interfaces from facade and

    provide new Implementation to :base
  33. IntelliJ IDEA rocks. Luckily, Android Studio is powered by IntelliJ

    IDEA. https://www.jetbrains.com/help/idea/refactoring- source-code.html
  34. Strategy #7 Vertical Slice Architecture

  35. • Avoid “N-Tier Architectures” • Architectural boundaries don’t run between

    modules but rather through them
  36. • Avoid “N-Tier Architectures” • Architectural boundaries don’t run between

    modules but rather through them
  37. :app :domain :presentation :data All repositories exposed to the domain

    module All use cases exposed to the presentation module :app knows about all modules and up the DI framework
  38. Domain Presentation Data Feature

  39. • Avoid “N-Tier Architectures” • Architectural boundaries don’t run between

    modules but rather through them
  40. :app :feature2 :base :feature1 :component

  41. :module domain data ui model use case <interface>
 repository activity

    fragment view model store repository local remote
  42. Strategy #8 Dependency Injection

  43. :app App Component Discovery Component Base Component Use Depend Build

    :discovery Build :base Use Use Build Use
  44. @Singleton @Component(modules = [ BaseModule::class ]) interface BaseComponent { {…}

    fun router(): DiscoveryRouter companion object { // workaround val INSTANCE: BaseComponent = DaggerBaseComponent.create() } }
  45. @ActivityScope @Component( modules = [ DiscoveryModule::class ], dependencies = [

    BaseComponent::class ]) interface DiscoveryComponent : AndroidInjector<DiscoveryActivity> { @Component.Builder abstract class Builder : AndroidInjector.Builder<DiscoveryActivity>() { abstract fun plus(component: BaseComponent): Builder } }
  46. DaggerDiscoveryComponent.builder() .plus(BaseComponent.INSTANCE) .create(this) .inject(this)

  47. @Singleton @Component(modules = [ {…} BaseModule::class ]) interface AppComponent :

    AndroidInjector<ConsumerApp> { fun inject(target: Activity) {…} }
  48. Strategy #9 Only Talk to Your Friends

  49. :app :discovery :base :search :instantapp

  50. interface Router { companion object { const val BASE_URL =

    “https://example.com” } } interface DiscoveryRouter : Router { fun startAvailabilityFilterActivityForResult(activity: Activity, requestCode: Int, peopleCount: Int, date: LocalDate, time: LocalTime) object Params { const val PEOPLE_COUNT = “PEOPLE_COUNT" const val DATE = "DATE" const val TIME = "TIME" } }
  51. @Reusable class UrlDiscoveryRouter @Inject constructor() : DiscoveryRouter { override fun

    startAvailabilityFilterActivityForResult(activity: Activity, requestCode: Int, peopleCount: Int, date: LocalDate, time: LocalTime) { val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url)).apply { addCategory(Intent.CATEGORY_DEFAULT) addCategory(Intent.CATEGORY_BROWSABLE) setPackage(activity.packageName) putExtra(DiscoveryRouter.Params.PEOPLE_COUNT, peopleCount) putExtra(DiscoveryRouter.Params.DATE, date) putExtra(DiscoveryRouter.Params.TIME, time) } activity.startActivityForResult(fragment, intent, requestCode) } // companion object private const val URL = "${BASE_URL}/availability/filter" }
  52. <manifest> <application> <activity android:name=“DiscoveryActivity”> <intent-filter> <action android:name="android.intent.action.VIEW"/> <category android:name="android.intent.category.DEFAULT"/> <category

    android:name="android.intent.category.BROWSABLE"/> <data android:scheme="https" android:host=“example.com” android:pathPattern="/availability/filter"/> </intent-filter> </activity> </application> </manifest>
  53. class SimpleActivity : AppCompatActivity() { @Inject lateinit var discoveryRouter: DiscoveryRouter

    fun onButtonClicked(…) { discoveryRouter.startAvailabilityFilterActivityForResult( activity = this, requestCode = requestCode, peopleCount = peopleCount, date = date, time = time) } }
  54. Strategy #10 Delivery Small and Iterate

  55. Start State A State B State C Minor change Minor

    change Minor change Quickly identify issues End Fix issues Rollback as last option New interaction
  56. A Pragmatic Approach 1. Quality Directives 2. Scout the Codebase

    3. Keep it Simple, Stupid 4. Don’t Talk to Strangers 5. Don’t Repeat Yourself 6. Small Interactions 7. Vertical Slice Architecture 8. Dependency Injection 9. Only Talk to Your Friends 10.Delivery Small and Iterate
  57. Questions? We’re hiring! https://tinyurl.com/y4vfr9ud Marcello Galhardo Slides at https://tinyurl.com/y9zcf4u9