Why we failed at modularizing our app

8123b9ca408d9b35d0cf955feb32cfb8?s=47 Marcos
July 03, 2019

Why we failed at modularizing our app

Modularization is the new trend, almost everybody in the Android ecosystem is refactoring their apps to use a modularized approach. We at Sky are no different, we had a big monolithic codebase supporting 4 apps in different countries that we started modularizing in September 2017. But we failed, big time.

This talk is an honest retrospective of everything that went wrong, the bad decisions that we made, the approach we initially took and how we, against all odds, eventually started re-building a maintainable, sustainable and extensible modularized codebase. In this talk you will learn from our mistakes and struggles, like defining what is a module and its responsibilities, how to integrate Dagger in a multi-module environment, set some rules and best practices and much more but, more importantly, you will learn what not to do when modularizing your codebase.

8123b9ca408d9b35d0cf955feb32cfb8?s=128

Marcos

July 03, 2019
Tweet

Transcript

  1. Why we failed at modularizing “our app” An honest retrospective

    Droidcon Berlin 19
  2. Marcos Holgado @Orbycius

  3. “Common sense is not that common” @Orbycius Voltaire, Dictionnaire Philosophique

    (1764)
  4. @Orbycius

  5. @Orbycius

  6. @Orbycius

  7. @Orbycius

  8. @Orbycius

  9. @Orbycius

  10. @Orbycius

  11. None
  12. How does it work? @Orbycius

  13. productFlavors { uk { ... apply from: 'uk.gradle' } international

    { ... apply from: 'international.gradle' } germany { ... apply from: 'germany.gradle' } italia { ... apply from: 'italy.gradle' } } @Orbycius
  14. productFlavors { uk { ... apply from: 'uk.gradle' } international

    { ... apply from: 'international.gradle' } germany { ... apply from: 'germany.gradle' } italia { ... apply from: 'italy.gradle' } } @Orbycius App
  15. productFlavors { uk { ... apply from: 'uk.gradle' } international

    { ... apply from: 'international.gradle' } germany { ... apply from: 'germany.gradle' } italia { ... apply from: 'italy.gradle' } } @Orbycius App src
  16. productFlavors { uk { ... apply from: 'uk.gradle' } international

    { ... apply from: 'international.gradle' } germany { ... apply from: 'germany.gradle' } italia { ... apply from: 'italy.gradle' } } @Orbycius App src main
  17. productFlavors { uk { ... apply from: 'uk.gradle' } international

    { ... apply from: 'international.gradle' } germany { ... apply from: 'germany.gradle' } italia { ... apply from: 'italy.gradle' } } @Orbycius App src main germany uk italia international
  18. @Orbycius App src main germany uk italia international

  19. @Orbycius src main uk germany

  20. @Orbycius src main uk germany Feature 1

  21. @Orbycius src main uk germany Feature 1

  22. @Orbycius src main uk germany Feature 1 Feature 2

  23. @Orbycius src main uk germany Feature 1.1 Feature 2

  24. @Orbycius src main uk germany Feature 1.1 Feature 2

  25. @Orbycius src main uk germany Feature 1.1 Feature 2

  26. @Orbycius Author: Marcos Holgado <…@sky.uk> Date: Bug fix for tab

    colours... again Tue Oct 17 09:36:44 2017 +0100
  27. @Orbycius Author: Marcos Holgado <…@sky.uk> Date: Bug fix for tab

    colours... again Author: Marcos Holgado <…@sky.uk> Date: Fix tab colours again :( Tue Oct 17 09:36:44 2017 +0100 Mon Oct 23 16:10:08 2017 +0100
  28. @Orbycius Author: Marcos Holgado <…@sky.uk> Date: Bug fix for tab

    colours... again Author: Marcos Holgado <…@sky.uk> Date: Fix tab colours again :( Tue Oct 17 09:36:44 2017 +0100 Mon Oct 23 16:10:08 2017 +0100
  29. Long build times Change one break many Painful to work

    with It doesn’t scale … @Orbycius
  30. Convincing people is hard @Orbycius

  31. Convincing people out of your team is hard @Orbycius

  32. “The Vision” @Orbycius

  33. Showcase App @Orbycius

  34. Feature 1 @Orbycius Feature 2 Feature 3 Feature 4 Feature

    5 Feature 6 Feature 7 Feature 8
  35. Feature 1 @Orbycius Feature 2 Feature 3 Feature 4 Feature

    5 Feature 6 Feature 7 Feature 8
  36. Feature 1 @Orbycius Feature 2 Feature 3 Feature 4 Feature

    5 Feature 6 Feature 7 Feature 8
  37. Feature 1 @Orbycius Feature 2 Feature 3 Feature 4 Feature

    5 Feature 6 Feature 7 Feature 8
  38. Feature 1 @Orbycius Feature 2 Feature 3 Feature 4 Feature

    5 Feature 6 Feature 7 Feature 8
  39. Feature 1 @Orbycius Feature 2 Feature 3 Feature 4 Feature

    5 Feature 6 Feature 7 Feature 8
  40. What is a module? @Orbycius

  41. @Orbycius Feature modules Core

  42. @Orbycius Feature modules Core

  43. @Orbycius

  44. @Orbycius Inputs

  45. @Orbycius Inputs Outputs

  46. @Orbycius Inputs Outputs listeners()

  47. Inputs Outputs listeners() App @Orbycius

  48. Inputs Outputs listeners() App @Orbycius implements

  49. What is a feature? @Orbycius

  50. Inputs Outputs App @Orbycius Article List

  51. Inputs Outputs ArticleListActivity App @Orbycius Article List

  52. Inputs Outputs ArticleListActivity App @Orbycius Article List ArticleWebViewActivity?

  53. Inputs Outputs App @Orbycius Article List onArticleClick() Article Reader Inputs

    Outputs
  54. Inputs Outputs App @Orbycius implements Article List onArticleClick() Article Reader

    Inputs Outputs
  55. Inputs Outputs App @Orbycius implements Article List onArticleClick() Article Reader

    Inputs Outputs
  56. The first module @Orbycius

  57. App @Orbycius

  58. App @Orbycius Streaming

  59. App @Orbycius Streaming

  60. App @Orbycius Streaming Live TV

  61. App @Orbycius Streaming Live TV

  62. App @Orbycius Streaming Live TV

  63. Extremely complicated feature @Orbycius

  64. Extremely complicated feature Hard deadline @Orbycius

  65. Extremely complicated feature Hard deadline Mainly done by one person

    @Orbycius
  66. Extremely complicated feature Hard deadline Mainly done by one person

    Very bad 1st example @Orbycius
  67. Start with the easiest @Orbycius

  68. Splash Screen @Orbycius

  69. Start with the most valuable @Orbycius

  70. Integrate ASAP @Orbycius

  71. @Orbycius

  72. @Orbycius

  73. Don’t compromise your modules @Orbycius

  74. Dependency Injection @Orbycius

  75. App @Orbycius Streaming Live TV Streaming player

  76. App @Orbycius Streaming Live TV Core Streaming player

  77. App @Orbycius Streaming Live TV Core Streaming player

  78. App @Orbycius Streaming Live TV Core Streaming player

  79. App @Orbycius Streaming Live TV Core Streaming player User

  80. App @Orbycius Streaming Live TV Core Streaming player User User

  81. App @Orbycius Streaming Live TV Core Streaming player User User

  82. App @Orbycius Streaming Live TV Core Streaming player User User

  83. Dagger to the rescue @Orbycius

  84. Dagger to the rescue @Orbycius ?

  85. Dagger vs Koin @Orbycius

  86. If you don’t want to understand it, don’t use it

    @Orbycius
  87. SODD @Orbycius

  88. Stack Overflow Driven Development @Orbycius

  89. @Orbycius @Provides public ForegroundManager provideForegroundManager(Context context) { useForegroundManager = new

    ForegroundManager((Application) context); if (!foregroundManager.compareAndSet(null, useForegroundManager)) { useForegroundManager = foregroundManager.get(); } } return useForegroundManager; } private final AtomicReference<ForegroundManager> foregroundManager; ForegroundManager useForegroundManager = foregroundManager.get(); if (useForegroundManager == null) { } @Singleton
  90. @Orbycius @Provides public ForegroundManager provideForegroundManager(Context context) { useForegroundManager = new

    ForegroundManager((Application) context); if (!foregroundManager.compareAndSet(null, useForegroundManager)) { useForegroundManager = foregroundManager.get(); } } return useForegroundManager; } private final AtomicReference<ForegroundManager> foregroundManager; ForegroundManager useForegroundManager = foregroundManager.get(); if (useForegroundManager == null) { } @Singleton
  91. @Orbycius @Provides public ForegroundManager provideForegroundManager(Context context) { useForegroundManager = new

    ForegroundManager((Application) context); if (!foregroundManager.compareAndSet(null, useForegroundManager)) { useForegroundManager = foregroundManager.get(); } } return useForegroundManager; } private final AtomicReference<ForegroundManager> foregroundManager; ForegroundManager useForegroundManager = foregroundManager.get(); if (useForegroundManager == null) { } @Singleton
  92. @Orbycius public class PlayerActivity extends AppCompatActivity { @Inject Utility utility;

    @Override protected void onCreate(Bundle savedInstanceState) { .getStreamingComponent() .addSubcomponent(new StreamingModule(this)) .inject(this); } [...] } } // Check that a sub-class hasn't just done DI for us! if (utility == null) { StreamingModuleMain.getStreamingModuleHelper()
  93. @Orbycius public class PlayerActivity extends AppCompatActivity { @Inject Utility utility;

    @Override protected void onCreate(Bundle savedInstanceState) { .getStreamingComponent() .addSubcomponent(new StreamingModule(this)) .inject(this); } [...] } } // Check that a sub-class hasn't just done DI for us! if (utility == null) { StreamingModuleMain.getStreamingModuleHelper()
  94. @Orbycius public class PlayerActivity extends AppCompatActivity { @Inject Utility utility;

    @Override protected void onCreate(Bundle savedInstanceState) { .getStreamingComponent() .addSubcomponent(new StreamingModule(this)) .inject(this); } [...] } } // Check that a sub-class hasn't just done DI for us! if (utility == null) { StreamingModuleMain.getStreamingModuleHelper()
  95. @Orbycius public interface ModuleMain { void initialise(); void terminate(); BaseNavObjectRegistrar

    getNavObjectRegistrar(); } ModuleHelper getModuleHelper();
  96. @Orbycius public interface ModuleMain { void initialise(); void terminate(); BaseNavObjectRegistrar

    getNavObjectRegistrar(); } ModuleHelper getModuleHelper();
  97. @Orbycius public interface ModuleMain { void initialise(); void terminate(); BaseNavObjectRegistrar

    getNavObjectRegistrar(); } public interface ModuleHelper { void setCoreComponent(CoreComponent coreComponent); CoreComponent getCoreComponent(); } ModuleHelper getModuleHelper();
  98. Subcomponents @Orbycius

  99. App @Orbycius Streaming @Component(modules = [AppModule::class]) interface AppComponent { fun

    inject(mainActivity: MainActivity) fun plus( ) } @Subcomponent(modules = [StreamingModule::class]) interface StreamingSubcomponent { fun inject(activity: PlayerActivity) } streamingComponent: StreamingSubcomponent
  100. App @Orbycius Streaming @Component(modules = [AppModule::class]) interface AppComponent { fun

    inject(mainActivity: MainActivity) fun plus( ) } @Subcomponent(modules = [StreamingModule::class]) interface StreamingSubcomponent { fun inject(activity: PlayerActivity) } streamingComponent: StreamingSubcomponent
  101. App @Orbycius Streaming @Component(modules = [AppModule::class]) interface AppComponent { fun

    inject(mainActivity: MainActivity) fun plus( ) } @Subcomponent(modules = [StreamingModule::class]) interface StreamingSubcomponent { fun inject(activity: PlayerActivity) } streamingComponent: StreamingSubcomponent
  102. App @Orbycius Streaming .plus(streamingSubcomponent) .inject(this) @Component(modules = [AppModule::class]) interface AppComponent

    { fun inject(mainActivity: MainActivity) fun plus( ) } streamingComponent: StreamingSubcomponent appComponent
  103. App @Orbycius Streaming .plus(streamingSubcomponent) .inject(this) @Component(modules = [AppModule::class]) interface AppComponent

    { fun inject(mainActivity: MainActivity) fun plus( ) } streamingComponent: StreamingSubcomponent appComponent
  104. App @Orbycius Streaming .plus(streamingSubcomponent) .inject(this) @Component(modules = [AppModule::class]) interface AppComponent

    { fun inject(mainActivity: MainActivity) fun plus( ) } streamingComponent: StreamingSubcomponent appComponent
  105. Subcomponents @Orbycius

  106. Subcomponents @Orbycius

  107. @Orbycius @Component( modules = StreamingModule.class, dependencies = CoreComponent.class ) public

    interface StreamingComponent { }
  108. App @Orbycius Streaming Core interface CoreComponentProvider { CoreComponent provideCoreComponent(); }

    interface AppComponentProvider { AppComponent provideAppComponent(); } interface StreamingComponentProvider { StreamingComponent provideStComponent(); }
  109. App @Orbycius Streaming Core interface CoreComponentProvider { CoreComponent provideCoreComponent(); }

    interface AppComponentProvider { AppComponent provideAppComponent(); } interface StreamingComponentProvider { StreamingComponent provideStComponent(); } class SkySportsApplication extends Application implements CoreComponentProvider, AppComponentProvider { }
  110. @Orbycius interface AppComponentProvider { AppComponent provideAppComponent(); } class SkySportsApplication extends

    Application implements CoreComponentProvider, AppComponentProvider { private CoreComponent coreComponent; private AppComponent appComponent; @Override public CoreComponent provideCoreComponent() { if (coreComponent == null) { coreComponent = DaggerCoreComponent.builder() .commonModule(new CommonModule(this)) .build(); } return commonComponent; } }
  111. @Orbycius public class UKSportsApplication extends implements { private StreamingComponent streamingComponent;

    } SkySportsApplication StreamingComponentProvider .coreComponent(provideCoreComponent()) @Override public StreamingComponent provideStreamingComponent() { if (streamingComponent == null) { streamingComponent = DaggerStreamingComponent.builder() .streamingModule(new StreamingModule( .build(); } return streamingComponent; } provideAppComponent().getUser()
  112. @Orbycius public class UKSportsApplication extends implements { private StreamingComponent streamingComponent;

    } SkySportsApplication StreamingComponentProvider .coreComponent(provideCoreComponent()) @Override public StreamingComponent provideStreamingComponent() { if (streamingComponent == null) { streamingComponent = DaggerStreamingComponent.builder() .streamingModule(new StreamingModule( .build(); } return streamingComponent; } provideAppComponent().getUser()
  113. @Orbycius public class UKSportsApplication extends implements { private StreamingComponent streamingComponent;

    } SkySportsApplication StreamingComponentProvider .coreComponent(provideCoreComponent()) @Override public StreamingComponent provideStreamingComponent() { if (streamingComponent == null) { streamingComponent = DaggerStreamingComponent.builder() .streamingModule(new StreamingModule( .build(); } return streamingComponent; } provideAppComponent().getUser()
  114. @Orbycius public class UKSportsApplication extends implements { private StreamingComponent streamingComponent;

    } SkySportsApplication StreamingComponentProvider .coreComponent(provideCoreComponent()) @Override public StreamingComponent provideStreamingComponent() { if (streamingComponent == null) { streamingComponent = DaggerStreamingComponent.builder() .streamingModule(new StreamingModule( .build(); } return streamingComponent; } provideAppComponent().getUser()
  115. @Orbycius public class UKSportsApplication extends implements { private StreamingComponent streamingComponent;

    } SkySportsApplication StreamingComponentProvider .coreComponent(provideCoreComponent()) @Override public StreamingComponent provideStreamingComponent() { if (streamingComponent == null) { streamingComponent = DaggerStreamingComponent.builder() .streamingModule(new StreamingModule( .build(); } return streamingComponent; } provideAppComponent().getUser()
  116. @Orbycius public class PlayerActivity extends AppCompatActivity { @Inject Utility utility;

    @Inject User user; @Override protected void onCreate(Bundle savedInstanceState) { StreamingInjectHelper.provideStreamingComponent( getApplicationContext() ).inject(this); } }
  117. @Orbycius public class StreamingInjectHelper { public static StreamingComponent provideStreamingComponent(final Context

    context){ if (context instanceof StreamingComponentProvider) { return ((StreamingComponentProvider)context).provideStreamingComponent(); } else { throw new IllegalStateException("The context you have passed does not implement StreamingComponentProvider"); } } }
  118. Understand what you do Share knowledge @Orbycius

  119. @Orbycius KI S S

  120. @Orbycius K I S S Keep It Simple Stupid

  121. @Orbycius Too smart to fail?

  122. @Orbycius If the team can’t understand it, you already failed

  123. @Orbycius @Provides @IntoMap @IntKey(TableRow.CRICKET_ROW) SportsListViewHolderFactory provideCricketViewHolder() { return new CricketStandingViewHolderFactory();

    } @Provides @IntoMap @IntKey(TableRow.RUGBY_ROW) SportsListViewHolderFactory provideRugbyViewHolder() { return new RugbyStandingViewHolderFactory(); } @Provides @IntoMap @IntKey(TableRow.F1_ROW) SportsListViewHolderFactory provideF1DriverViewHolder() { return new F1DriverStandingViewHolderFactory(); }
  124. @Orbycius @Provides Map<Integer, SportsListViewHolderFactory> provideSportsListViewHolderFactoryMap() { Map<Integer, SportsListViewHolderFactory> map =

    new HashMap<>(); map.put(TableRow.F1_ROW, new F1DriverStandingViewHolderFactory()); map.put(TableRow.CRICKET_ROW, new CricketStandingViewHolderFactory()); map.put(TableRow.RUGBY_ROW, new RugbyStandingViewHolderFactory()); return map; }
  125. @Orbycius public class Config implements Parcelable { /** * Config

    Sections */ private final Map<String, ConfigSection> sections; }
  126. @Orbycius public class Config implements Parcelable { /** * Config

    Sections */ private final Map<String, ConfigSection> sections; } public interface ConfigSection extends Parcelable { /** * Name of the section to use as a key * * @return Name of the section to use for lookups */ String getSectionName(); }
  127. @Orbycius public class ConfigDeserialiser implements JsonDeserializer<Config> { /** * Deserialiser

    for individual sections, keyed on the sections they support * * Note that this means each deserialiser may appear more than once! */ private final Map<String, ConfigSectionDeserialiser> sectionDeserialisers; }
  128. @Orbycius public class ConfigDeserialiser implements JsonDeserializer<Config> { /** * Deserialiser

    for individual sections, keyed on the sections they support * * Note that this means each deserialiser may appear more than once! */ private final Map<String, ConfigSectionDeserialiser> sectionDeserialisers; } public interface ConfigSectionDeserialiser<T extends ConfigSection> { Collection<String> supportedParentKeys(); String sectionKey(); @NonNull T deserialiseSection(String key, JsonElement sectionSource, @Nullable T existingSection); T getDefaultSection(); }
  129. @Orbycius public class ForcedUpgradeDeserialiser implements ConfigSectionDeserialiser<ForcedUpgrade> { public static final

    String LATEST_VERSION = "lver"; public static final String MESSAGE_TITLE = "title"; public static final String MESSAGE_BODY = "message"; public static final String FORCE_UPGRADE = "shouldForce"; }
  130. @Orbycius public class ForcedUpgradeDeserialiser implements ConfigSectionDeserialiser<ForcedUpgrade> { public static final

    String LATEST_VERSION = "lver"; public static final String MESSAGE_TITLE = "title"; public static final String MESSAGE_BODY = "message"; public static final String FORCE_UPGRADE = "shouldForce"; } public class ForcedUpgrade implements ConfigSection { public static final String FORCED_UPGRADE = "shouldForce"; /** * Latest version code */ private int latestVersion; }
  131. @Orbycius “Deleted a lot of classes around config because let's

    be honest, is just a json so why over complicating it?”
  132. @Orbycius “Deleted a lot of classes around config because let's

    be honest, is just a json so why over complicating it?”
  133. Core @Orbycius

  134. Core @Orbycius do you need it?

  135. App @Orbycius Streaming Core User User

  136. App @Orbycius Streaming Core User User

  137. App @Orbycius Streaming Core User About

  138. App @Orbycius Streaming Core User About other_libraries

  139. App @Orbycius Streaming Core About other_libraries User User

  140. App @Orbycius Streaming Core User User

  141. App @Orbycius Streaming Core User User Streaming

  142. Treat each module* as a library @Orbycius *feature module

  143. Treat each module* as a library… maybe @Orbycius

  144. Testing @Orbycius

  145. Showcase App @Orbycius

  146. @Orbycius Feature 1 Feature 2 Feature 3 Feature 4 Feature

    5 Feature 6 Feature 7 Feature 8
  147. Showcase App @Orbycius

  148. Showcase App @Orbycius

  149. Sample Apps @Orbycius

  150. @Orbycius Feature 1 Feature 2 Feature 3 Feature 4 Feature

    5 Feature 6 Feature 7 Feature 8 Feature 9
  151. @Orbycius Resource conflicts

  152. App @Orbycius Streaming Core <TextView android:text="@string/test"/> <string name="test">My test</string> implementation

    project(path: ':core') implementation project(path: ':streaming')
  153. App @Orbycius Streaming Core <TextView android:text="@string/test"/> <string name="test">My test</string> implementation

    project(path: ':core') implementation project(path: ':streaming') My test
  154. App @Orbycius Streaming Core <TextView android:text="@string/test"/> <string name="test">My test</string> implementation

    project(path: ':core') implementation project(path: ':streaming') <string name="test">Core test</string>
  155. App @Orbycius Streaming Core <TextView android:text="@string/test"/> <string name="test">My test</string> implementation

    project(path: ':core') implementation project(path: ':streaming') Core test <string name="test">Core test</string>
  156. App @Orbycius Streaming Core <TextView android:text="@string/test"/> <string name="test">My test</string> <string

    name="test">Core test</string> implementation project(path: ':streaming') implementation project(path: ':core')
  157. App @Orbycius Streaming Core <TextView android:text="@string/test"/> <string name="test">My test</string> My

    test <string name="test">Core test</string> implementation project(path: ':streaming') implementation project(path: ':core')
  158. App @Orbycius Streaming Core <TextView android:text="@string/test"/> <string name="test">My test</string> <string

    name="test">Core test</string> implementation project(path: ':streaming') implementation project(path: ':core') <string name="test">App test</string>
  159. App @Orbycius Streaming Core <TextView android:text="@string/test"/> <string name="test">My test</string> App

    test <string name="test">Core test</string> implementation project(path: ':streaming') implementation project(path: ':core') <string name="test">App test</string>
  160. @Orbycius android { compileSdkVersion androidCompileSdkVersion defaultConfig { minSdkVersion androidMinSdkVersion targetSdkVersion

    androidTargetSdkVersion versionCode 1 versionName "1.0" } }
  161. @Orbycius android { compileSdkVersion androidCompileSdkVersion defaultConfig { minSdkVersion androidMinSdkVersion targetSdkVersion

    androidTargetSdkVersion versionCode 1 versionName "1.0" } resourcePrefix 'my_prefix' }
  162. @Orbycius <resources> <string name="test">My other test</string> </resources> Resource named '`test`'

    does not start with the project's resource prefix '`my_prefix`'; rename to '`my_prefixTest`' ? !
  163. @Orbycius <resources> <string name="test">My other test</string> </resources> Resource named '`test`'

    does not start with the project's resource prefix '`my_prefix`'; rename to '`my_prefixTest`' ? !
  164. @Orbycius Android Studio Scopes

  165. @Orbycius

  166. @Orbycius

  167. @Orbycius

  168. @Orbycius Recap

  169. @Orbycius App is the glue that holds all together

  170. @Orbycius Treat your feature modules as libraries

  171. @Orbycius “Everything should be as simple as possible, but not

    simpler” Albert Einstein
  172. @Orbycius THANKS https://github.com/marcosholgado/dagger-playground (blog) http://bit.ly/android-module