Why we failed at modularizing Sky Sports. An honest retrospective

8123b9ca408d9b35d0cf955feb32cfb8?s=47 Marcos
April 04, 2019

Why we failed at modularizing Sky Sports. 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.

8123b9ca408d9b35d0cf955feb32cfb8?s=128

Marcos

April 04, 2019
Tweet

Transcript

  1. 10.
  2. 11.
  3. 13.

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

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

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

    { ... apply from: 'international.gradle' } germany { ... apply from: 'germany.gradle' } italia { ... apply from: 'italy.gradle' } } @Orbycius App
  5. 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
  6. 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
  7. 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
  8. 26.

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

    colours... again Tue Oct 17 09:36:44 2017 +0100
  9. 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
  10. 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
  11. 29.

    Long build times Change one break many Painful to work

    with It doesn’t scale … @Orbycius
  12. 37.
  13. 38.
  14. 39.
  15. 40.
  16. 41.
  17. 42.
  18. 46.
  19. 74.
  20. 75.
  21. 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
  22. 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
  23. 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
  24. 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()
  25. 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()
  26. 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()
  27. 97.

    @Orbycius public interface ModuleMain { void initialise(); void terminate(); BaseNavObjectRegistrar

    getNavObjectRegistrar(); } public interface ModuleHelper { void setCoreComponent(CoreComponent coreComponent); CoreComponent getCoreComponent(); } ModuleHelper getModuleHelper();
  28. 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
  29. 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
  30. 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
  31. 102.

    App @Orbycius Streaming .plus(streamingSubcomponent) .inject(this) @Component(modules = [AppModule::class]) interface AppComponent

    { fun inject(mainActivity: MainActivity) fun plus( ) } streamingComponent: StreamingSubcomponent appComponent
  32. 103.

    App @Orbycius Streaming .plus(streamingSubcomponent) .inject(this) @Component(modules = [AppModule::class]) interface AppComponent

    { fun inject(mainActivity: MainActivity) fun plus( ) } streamingComponent: StreamingSubcomponent appComponent
  33. 104.

    App @Orbycius Streaming .plus(streamingSubcomponent) .inject(this) @Component(modules = [AppModule::class]) interface AppComponent

    { fun inject(mainActivity: MainActivity) fun plus( ) } streamingComponent: StreamingSubcomponent appComponent
  34. 108.

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

    interface AppComponentProvider { AppComponent provideAppComponent(); } interface StreamingComponentProvider { StreamingComponent provideStComponent(); }
  35. 109.

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

    interface AppComponentProvider { AppComponent provideAppComponent(); } interface StreamingComponentProvider { StreamingComponent provideStComponent(); } class SkySportsApplication extends Application implements CoreComponentProvider, AppComponentProvider { }
  36. 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; } }
  37. 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()
  38. 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()
  39. 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()
  40. 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()
  41. 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()
  42. 116.

    @Orbycius public class PlayerActivity extends AppCompatActivity { @Inject Utility utility;

    @Inject User user; @Override protected void onCreate(Bundle savedInstanceState) { StreamingInjectHelper.provideStreamingComponent( getApplicationContext() ).inject(this); } }
  43. 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"); } } }
  44. 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(); }
  45. 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; }
  46. 125.

    @Orbycius public class Config implements Parcelable { /** * Config

    Sections */ private final Map<String, ConfigSection> sections; }
  47. 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(); }
  48. 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; }
  49. 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(); }
  50. 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"; }
  51. 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; }
  52. 131.

    @Orbycius “Deleted a lot of classes around config because let's

    be honest, is just a json so why over complicating it?”
  53. 132.

    @Orbycius “Deleted a lot of classes around config because let's

    be honest, is just a json so why over complicating it?”
  54. 146.
  55. 150.

    @Orbycius Feature 1 Feature 2 Feature 3 Feature 4 Feature

    5 Feature 6 Feature 7 Feature 8 Feature 9
  56. 153.
  57. 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>
  58. 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>
  59. 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')
  60. 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')
  61. 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>
  62. 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>
  63. 161.

    @Orbycius android { compileSdkVersion androidCompileSdkVersion defaultConfig { minSdkVersion androidMinSdkVersion targetSdkVersion

    androidTargetSdkVersion versionCode 1 versionName "1.0" } resourcePrefix 'my_prefix' }
  64. 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`' ? !
  65. 163.

    App @Orbycius Streaming <TextView android:text="@string/test"/> <string name="st_test">My test</string> implementation project(path:

    ':streaming') implementation project(path: ':core') <string name="st_test">App test</string>
  66. 164.

    App @Orbycius Streaming <TextView android:text="@string/test"/> <string name="_st_test">My test</string> implementation project(path:

    ':streaming') implementation project(path: ':core') <string name="st_test">App test</string>
  67. 165.

    App @Orbycius Streaming <TextView android:text="@string/test"/> <string name="_st_test">My test</string> implementation project(path:

    ':streaming') implementation project(path: ':core') <string name="st_test">App test</string>
  68. 167.
  69. 168.
  70. 169.
  71. 174.