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

Why We Failed At Modularizing Our App. An Honest Retrospective.

Marcos
September 25, 2019

Why We Failed At Modularizing Our App. An Honest Retrospective.

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.

Marcos

September 25, 2019
Tweet

More Decks by Marcos

Other Decks in Technology

Transcript

  1. THE FOLLOWING TALK HAS BEEN APPROVED FOR APPROPIATE AUDIENCES BY

    THE DEVELOPERS ASSOCIATION OF AMERICA THE TALK ADVERTISED HAS BEEN RATED PG-25 DEVELOPERS STRONGLY CAUTIONED SOME MATERIAL MAY BE INAPPROPRIATE FOR DEVELOPERS UNDER 25 SOME AWFUL CODE AND BAD PRACTISES
  2. productFlavors { uk { ... apply from: 'uk.gradle' } international

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

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

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

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

    { ... apply from: 'international.gradle' } germany { ... apply from: 'germany.gradle' } italia { ... apply from: 'italy.gradle' } } App src main germany uk italia international @Orbycius
  7. Author: Marcos Holgado <…@sky.uk> Date: Bug fix for tab colours...

    again Tue Oct 17 09:36:44 2017 +0100 @Orbycius
  8. 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 @Orbycius
  9. 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 @Orbycius
  10. Long build times Change one break many Painful to work

    with It doesn’t scale … @Orbycius
  11. Feature 1 Feature 2 Feature 3 Feature 4 Feature 5

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

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

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

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

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

    Feature 6 Feature 7 Feature 8 @Orbycius
  17. @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 @Orbycius
  18. @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 @Orbycius
  19. @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 @Orbycius
  20. 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() @Orbycius
  21. 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() @Orbycius
  22. 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() @Orbycius
  23. public interface ModuleMain { void initialise(); void terminate(); BaseNavObjectRegistrar getNavObjectRegistrar();

    } public interface ModuleHelper { void setCoreComponent(CoreComponent coreComponent); CoreComponent getCoreComponent(); } ModuleHelper getModuleHelper(); @Orbycius
  24. App 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 @Orbycius
  25. App 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 @Orbycius
  26. App 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 @Orbycius
  27. App Streaming .plus(streamingSubcomponent) .inject(this) @Component(modules = [AppModule::class]) interface AppComponent {

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

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

    fun inject(mainActivity: MainActivity) fun plus( ) } streamingComponent: StreamingSubcomponent appComponent @Orbycius
  30. App Streaming Core interface CoreComponentProvider { CoreComponent provideCoreComponent(); } interface

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

    AppComponentProvider { AppComponent provideAppComponent(); } interface StreamingComponentProvider { StreamingComponent provideStComponent(); } class SkySportsApplication extends Application implements CoreComponentProvider, AppComponentProvider { } @Orbycius
  32. 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; } } @Orbycius
  33. 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() @Orbycius
  34. 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() @Orbycius
  35. 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() @Orbycius
  36. 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() @Orbycius
  37. 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() @Orbycius
  38. public class PlayerActivity extends AppCompatActivity { @Inject Utility utility; @Inject

    User user; @Override protected void onCreate(Bundle savedInstanceState) { StreamingInjectHelper.provideStreamingComponent( getApplicationContext() ).inject(this); } } @Orbycius
  39. 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"); } } } @Orbycius
  40. @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(); } @Orbycius
  41. @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; } @Orbycius
  42. public class Config implements Parcelable { /** * Config Sections

    */ private final Map<String, ConfigSection> sections; } @Orbycius
  43. 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(); } @Orbycius
  44. 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; } @Orbycius
  45. 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(); } @Orbycius
  46. 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"; } @Orbycius
  47. 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; } @Orbycius
  48. “Deleted a lot of classes around config because, let's be

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

    honest, is just a json so why over complicating it?” @Orbycius
  50. Feature 1 Feature 2 Feature 3 Feature 4 Feature 5

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

    Feature 6 Feature 7 Feature 8 Feature 9 @Orbycius
  52. App 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> @Orbycius
  53. App 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> @Orbycius
  54. App 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') @Orbycius
  55. App 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') @Orbycius
  56. App 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> @Orbycius
  57. App 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> @Orbycius
  58. <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`' ? ! @Orbycius
  59. <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`' ? ! @Orbycius