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.

Marcello Galhardo

March 01, 2019
Tweet

More Decks by Marcello Galhardo

Other Decks in Programming

Transcript

  1. Why? • Build takes too long • Code ownership •

    Separation of concerns • Dynamic Features
  2. • Code must be under continuous integration • Automate the

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

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

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

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

    weeks ago Showing 272 changed files with 4582 additions and 365 deletions
  7. • Keep things you need • Donate things someone else

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

    by ViewKt.", level = DeprecationLevel.ERROR, replaceWith = ReplaceWith( expression = "ViewKt" ) ) object ViewUtils {…}
  9. • 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
  10. • 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
  11. • Make it easier to share code • Centralise your

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

    resources • Share versions of dependencies using variables
  13. App

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

    resources • Share versions of dependencies using variables
  15. // ../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" }
  16. // 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 {…}
  17. :legacy :feature :facade Invokes new feature Access legacy functionalities by

    well-defined interface and dependency injection Implements interfaces on legacy code
  18. IntelliJ IDEA rocks. Luckily, Android Studio is powered by IntelliJ

    IDEA. https://www.jetbrains.com/help/idea/refactoring- source-code.html
  19. :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
  20. :module domain data ui model use case <interface>
 repository activity

    fragment view model store repository local remote
  21. @Singleton @Component(modules = [ BaseModule::class ]) interface BaseComponent { {…}

    fun router(): DiscoveryRouter companion object { // workaround val INSTANCE: BaseComponent = DaggerBaseComponent.create() } }
  22. @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 } }
  23. @Singleton @Component(modules = [ {…} BaseModule::class ]) interface AppComponent :

    AndroidInjector<ConsumerApp> { fun inject(target: Activity) {…} }
  24. 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" } }
  25. @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" }
  26. <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>
  27. class SimpleActivity : AppCompatActivity() { @Inject lateinit var discoveryRouter: DiscoveryRouter

    fun onButtonClicked(…) { discoveryRouter.startAvailabilityFilterActivityForResult( activity = this, requestCode = requestCode, peopleCount = peopleCount, date = date, time = time) } }
  28. 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
  29. 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