Save 37% off PRO during our Black Friday Sale! »

Building a SuperApp

Building a SuperApp

Lessons from merging multiple apps into one

Given online at 360|AnDev on July 23rd, 2020.

F60e42d94f99f029b590206076dbd354?s=128

Ahmed El-Helw

December 31, 2020
Tweet

Transcript

  1. Ahmed El-Helw { helw.dev / @ahmedre } Building a SuperApp

    Lessons from merging multiple apps into one
  2. Multiple Apps

  3. One App - a Super App

  4. None
  5. None
  6. Why talk about this? • Refreshing way to think about

    Android development and architecture • Provides a new lens to view multiple Android teams in a company • Smaller teams can use it as a look into a potential future • It’s fun
  7. The World Before • Silos • Very little sharing between

    projects (almost no common libraries). • No mobile platform team • Lean Co f f ee
  8. How?

  9. • App will ship using the applicationId of our largest

    app • Apps can continue shipping their standalone apps until the business decides to remove them
  10. Two Options • Convert the RideHailing App into the SuperApp

    • pros: • easier to begin with, just add the other apps • cons: • lack of separation of concerns • lots of non-platform code • standalone app and MiniApp will diverge
  11. Two Options • Make a new git repository for our

    SuperApp • Be able to use our current largest app (ride hailing) from this new app • Repeat for other apps and build a shell interface. • Pros: • cleaner, more correct • standalone app and SuperApp implementations can converge • Cons: • more upfront cost
  12. Librarisizing the App • How is our app di f

    f erent from any random library module? • com.android.library vs com.android.application • Gradle con f i gurations relevant to apps and not to libraries
  13. Librarisizing the App apply plugin: 'com.android.application' apply plugin: 'kotlin-android' apply

    plugin: 'kotlin-kapt' apply plugin: 'net.ltgt.errorprone' android { compileSdkVersion deps.android.build.compileSdkVersion defaultConfig { minSdkVersion deps.android.build.minSdkVersion targetSdkVersion deps.android.build.targetSdkVersion versionCode 3010 versionName "3.0.1" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" multiDexEnabled true } signingConfigs { . . . } productFlavors { . . . } buildTypes { . . . } testOptions.unitTests.all { . . . } } dependencies { . . . } apply plugin: 'com.android.library' apply plugin: 'kotlin-android' apply plugin: 'kotlin-kapt' android { compileSdkVersion deps.android.build.compileSdkVersion defaultConfig { minSdkVersion deps.android.build.minSdkVersion targetSdkVersion deps.android.build.targetSdkVersion } } dependencies { . . . }
  14. Librarisizing the App Dependencies apk app aar jar aar jar

    aar
  15. Librarisizing the App Dependencies aar aar jar aar jar aar

    apk SuperApp apk Standalone lib
  16. Librarisizing the App • change app to be a com.android.library

    • create a new “container” module • should be com.android.application • should depend on app • move relevant code into container module
  17. Librarisizing the App • run it and make sure all

    is good • publish artifacts to company maven • try from new repo - it works!
  18. Repeat and we’re Good?

  19. Librarisizing the App aar apk Dependencies aar aar Ride Hail

    aar jar aar jar aar SuperApp Food aar jar aar jar aar … aar jar aar jar aar
  20. A two-MiniApp SuperApp • make sure our new app requires

    both set of dependencies • make a simple 2 button activity, one to launch each MiniApp
  21. Initial Complications • AndroidManifest con f l icts • minSdk

    version • FileProviders • Application subclass
  22. Initial Complications • Silent Manifest con f l icts •

    launch Activity • API keys • Application theme • Analytics • Push Noti f i cations
  23. Unwinding the Knots • SuperApp will be the shipping app,

    so… • minSdk overridden by SuperApp (gate mini apps until bump minSdk) • owns the Application subclass • owns other shared components • MiniApp con f l icting pieces move to their container
  24. After lots of tinkering…

  25. It compiles!

  26. But it Insta-crashes.

  27. Application Subclasses class App : MultiDexApplication() { lateinit var applicationComponent:

    ApplicationComponent override fun onCreate() { super.onCreate() initialize() applicationComponent = initializeInjector() } private fun initialize() { / / initialize Crash reporting, analytics, and app etc } private fun initializeInjector(): ApplicationComponent { / / . . . } }
  28. Application Subclasses class App : MultiDexApplication() { lateinit var applicationComponent:

    ApplicationComponent override fun onCreate() { super.onCreate() initialize() applicationComponent = initializeInjector() } private fun initialize() { / / initialize Crash reporting, analytics, and app etc } private fun initializeInjector(): ApplicationComponent { / / . . . } }
  29. Application Subclasses class App : MultiDexApplication() { lateinit var applicationComponent:

    ApplicationComponent override fun onCreate() { super.onCreate() initialize() applicationComponent = initializeInjector() } private fun initialize() { / / initialize Crash reporting, analytics, and app etc } private fun initializeInjector(): ApplicationComponent { / / . . . } }
  30. Application Subclasses • critical initialization logic • long living objects

    • (context as FooApplication).component.inject(this)
  31. Application Subclasses • Move initialization logic into a separate Initializer

    class • Move singleton object references to singleton classes • container is responsible for calling initialization logic
  32. Application Singletons object RideHailAppInitializer { lateinit var applicationComponent: ApplicationComponent fun

    initialize(context: Context) { initialize() applicationComponent = initializeInjector() } private fun initialize() { / / initialize Crash reporting, analytics, and app etc } private fun initializeInjector(context: Context): ApplicationComponent { / / . . . } }
  33. Application Singletons object RideHailAppInitializer { fun initialize(context: Context) { initialize()

    Injector.applicationComponent = initializeInjector() } private fun initialize() { / / initialize Crash reporting, analytics, and app etc } private fun initializeInjector(context: Context): ApplicationComponent { / / . . . } } object Injector { lateinit var applicationComponent: ApplicationComponent }
  34. Application Singletons class App : MultiDexApplication() { override fun onCreate()

    { super.onCreate() RideHailAppInitializer.initialize(this) } }
  35. Application Singletons class App : MultiDexApplication() { override fun onCreate()

    { super.onCreate() RideHailAppInitializer.initialize(this) FoodInitializer.initialize(this) } }
  36. Application Singletons • Existing Application classes migrated to MiniApp containers

    • Consequently, all ui tests have to move to the container as well
  37. Compile and Run

  38. … and it works!

  39. Kinda

  40. RideHail Splash Screen <?xml version="1.0" encoding="utf-8"?> <FrameLayout xmlns:android="http: / /

    schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" xmlns:tools="http: / / schemas.android.com/tools" xmlns:app="http: / / schemas.android.com/apk/res-auto" android:background="@color/careem_green_100"> <ImageView android:id="@+id/splash_logo" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center" android:alpha="0" tools:alpha="1" app:srcCompat="@drawable/careem_wink_logo_white" / > < / FrameLayout>
  41. Food Splash Screen <FrameLayout android:id="@+id/splashRootLayoutFl" xmlns:android="http: / / schemas.android.com/apk/res/android" xmlns:app="http:

    / / schemas.android.com/apk/res-auto" xmlns:tools="http: / / schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:background="@color/careemGreen100"> <View android:id="@+id/splashGradientView" android:layout_width="match_parent" android:layout_height="match_parent" android:alpha="0" android:background="@color/careemGreen100" / > <ProgressBar android:id="@+id/progressBar" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="bottom|center_horizontal" android:layout_marginBottom="@dimen/screen_vertical_margin_4x" android:visibility="gone" / > <ImageView android:id="@+id/logoImageView" android:layout_width="154dp" android:layout_height="88dp" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintBottom_toBottomOf="@id/guideline" android:layout_gravity="center" tools:src="@drawable/ic_careem_now" / > < / FrameLayout>
  42. Resource Name spacing • RideHail has R.layout.activity_splash • Food has

    R.layout.activity_splash • SuperApp gets… R.layout.activity_splash • which one? ¯\_(ツ)_/¯ • it just picks one. • This is also why appcompat, etc pre f i x
  43. Resource naming <dimen name="abc_action_bar_content_inset_material">16dp < / dimen> <dimen name="abc_action_bar_content_inset_with_nav">72dp <

    / dimen> <dimen name="abc_action_bar_default_height_material">56dp < / dimen> <dimen name="abc_action_bar_default_padding_end_material">0dp < / dimen> <dimen name="abc_action_bar_default_padding_start_material">0dp < / dimen> <dimen name="mtrl_bottomappbar_fabOffsetEndMode">60dp < / dimen> <dimen name="mtrl_bottomappbar_fab_cradle_margin">5dp < / dimen> <dimen name="mtrl_bottomappbar_fab_cradle_rounded_corner_radius">8dp < / dimen> <dimen name="mtrl_bottomappbar_fab_cradle_vertical_offset">0dp < / dimen> <dimen name="mtrl_bottomappbar_height">56dp < / dimen>
  44. Resource Name spacing • Resources in general (layout, strings, dimens,

    themes, …) • Database names must be unique • SharedPreferences f i les should be unique
  45. Library Versioning • Given two versions of the same artifact,

    Gradle picks the latest • this is generally ok • but sometimes it’s dangerous • ex minSdk 21 OkHttp/Retro f i t • ex breaking api changes
  46. Library Versioning • Build order Manifest to the rescue •

    Helps control dependencies from a single place • Removes surprises
  47. Library Versioning apply plugin: 'java-platform' def okhttp_version = "3.12.10" dependencies

    { constraints { / / okhttp / / minSdk 21 is required for 3.13.0+ api('com.squareup.okhttp3:okhttp') { version { strictly okhttp_version } } / / newer versions of Lottie break animations on customer and Now api('com.airbnb.android:lottie') { version { strictly '2.8.0' } } } }
  48. Library Versioning dependencies { api platform(“com.careem.superapp.core:platform:$platform_version”) / / okhttp &

    lottie implementation "com.squareup.okhttp3:okhttp" implementation "com.airbnb.android:lottie" }
  49. Now What?

  50. Questions • Does initializing every MiniApp on app start make

    sense? • Why does every MiniApp initialize… • Firebase? • Experimentation framework? • Analytics providers?
  51. Questions • How will push noti f i cations work?

    • How do we handle deep links?
  52. Detour: Two Sets of Libraries

  53. Application Initialization class App : MultiDexApplication() { override fun onCreate()

    { super.onCreate() RideHailAppInitializer.initialize(this) FoodInitializer.initialize(this) } }
  54. MiniApp Library • Interfaces • Constants • sealed classes and

    enums
  55. MiniApp Library • Initializer interface

  56. Initializers interface Initializer { fun initialize(context: Context) }

  57. Initializers class ListInitializer(private val initializers: List<Initializer>) : Initializer { override

    fun initialize(context: Context) { initializers.forEach { it.initialize(this) } } }
  58. Initializers class OneTimeInitializer(private val initializer: Initializer) : Initializer { override

    fun initialize(context: Context) { . . . } }
  59. Initializing MiniApps class SuperApp : MultiDexApplication() { @Inject lateinit var

    listInitializer: ListInitializer override fun onCreate() { super.onCreate() setupAndInject() / / run all the initializers listInitializer.initialize(this) } }
  60. Initializing MiniApps class SuperApp : MultiDexApplication() { @Inject lateinit var

    listInitializer: ListInitializer override fun onCreate() { super.onCreate() setupAndInject() / / run all the initializers listInitializer.initialize(this) } }
  61. Initializing MiniApps class SuperApp : MultiDexApplication() { @Inject lateinit var

    listInitializer: ListInitializer override fun onCreate() { super.onCreate() setupAndInject() / / run all the initializers listInitializer.initialize(this) } }
  62. MiniApp Library • Initializer interface • MiniApp interface

  63. MiniApps interface MiniApp { /** * Provide an initializer for

    initializing the MiniApp. * Keep this as light as possible. * / fun provideInitializer(): Initializer? }
  64. Initializing MiniApps class SuperApp : MultiDexApplication() { @Inject lateinit var

    miniApps: List<@JvmSuppressWildcards MiniApp> override fun onCreate() { super.onCreate() setupAndInject() / / run mini app initializers when needed } }
  65. Solving More Problems • Handle multiple analytics initialization • SuperApp

    should own analytics and provide an interface • Goal: Apps shouldn’t care what third parties we use
  66. Analytics interface AnalyticsAgent { fun setUserAttribute(name: String, value: Any?): Boolean

    fun logEvent(eventName: String, eventType: EventType = EventType.GENERAL, attributes: Map<String, Any>? = null): Boolean }
  67. MiniApp Library • Initializer interface • MiniApp interface • MiniApp

    Factory interface
  68. MiniAppFactory interface Dependencies { fun provideAnalytics(): AnalyticsProvider } interface MiniAppFactory

    { fun provideMiniApp(appContext: Context, dependencies: Dependencies): MiniApp }
  69. Container Library • Part of the SuperApp itself, but exported

    to maven • depends on “MiniApp Library”
  70. Container Library • Implementation of SuperApp libraries • Declare its

    own initializers where it makes sense • Base application subclass • used by SuperApp itself • used by standalone apps’ containers as well
  71. Initializers class CrashlyticsInitializer(private val isEnabled: Boolean) : Initializer { override

    fun initialize(context: Context) { val crashlytics: Crashlytics = Crashlytics.Builder() .core(CrashlyticsCore.Builder().disabled(!isEnabled).build()) .build() Fabric.with(context, crashlytics, CrashlyticsNdk()) } }
  72. Initializers abstract class BaseSuperApp : MultiDexApplication(), MiniAppProvider { @Inject lateinit

    var initializers: List<@JvmSuppressWildcards Initializer> @Inject lateinit var miniApps: Map<MiniAppType, @JvmSuppressWildcards MiniApp> override fun onCreate() { setupAndInject() / / run all the SuperApp initializers (analytics, crash reporting, etc) initializers.forEach { it.initialize(this) } super.onCreate() } override fun provideMiniApps(): Map<MiniAppType, MiniApp> = miniApps }
  73. Initializers abstract class BaseSuperApp : MultiDexApplication(), MiniAppProvider { @Inject lateinit

    var initializers: List<@JvmSuppressWildcards Initializer> @Inject lateinit var miniApps: Map<MiniAppType, @JvmSuppressWildcards MiniApp> override fun onCreate() { setupAndInject() / / run all the SuperApp initializers (analytics, crash reporting, etc) initializers.forEach { it.initialize(this) } super.onCreate() } override fun provideMiniApps(): Map<MiniAppType, MiniApp> = miniApps }
  74. Initializing MiniApps (application as? MiniAppProvider) ? . provideMiniApps() ? .

    get(MiniAppType.RideHail) ? . provideInitializer() ? . initialize(applicationContext)
  75. Initializing MiniApps (application as? MiniAppProvider) ? . provideMiniApps() ? .

    get(MiniAppType.RideHail) ? . provideInitializer() ? . initialize(applicationContext)
  76. Initializing MiniApps (application as? MiniAppProvider) ? . provideMiniApps() ? .

    get(MiniAppType.RideHail) ? . provideInitializer() ? . initialize(applicationContext)
  77. Initializing MiniApps (application as? MiniAppProvider) ? . provideMiniApps() ? .

    get(MiniAppType.RideHail) ? . provideInitializer() ? . initialize(applicationContext)
  78. Implications • MiniApps can test on their own • just

    depend on the container library! • no-op packages make it easy to mock out full MiniApps
  79. Api Changes • Let’s support push noti f i cations

    • server will send us an app_id for routing
  80. Initializers class SuperMessagingService : FirebaseMessagingService() { . . . override

    fun onMessageReceived(remoteMessage: RemoteMessage) { super.onMessageReceived(remoteMessage) val messageType = remoteMessage.data["app_id"] val receiver = miniApps.entries.firstOrNull { it.key.pushNotificationAppId = = messageType } ? . value receiver ? . providePushRecipient() ? . onMessageReceived(remoteMessage) } }
  81. Initializers class SuperMessagingService : FirebaseMessagingService() { . . . override

    fun onMessageReceived(remoteMessage: RemoteMessage) { super.onMessageReceived(remoteMessage) val messageType = remoteMessage.data["app_id"] val receiver = miniApps.entries.firstOrNull { it.key.pushNotificationAppId = = messageType } ? . value receiver ? . providePushRecipient() ? . onMessageReceived(remoteMessage) } }
  82. Initializers class SuperMessagingService : FirebaseMessagingService() { . . . override

    fun onMessageReceived(remoteMessage: RemoteMessage) { super.onMessageReceived(remoteMessage) val messageType = remoteMessage.data["app_id"] val receiver = miniApps.entries.firstOrNull { it.key.pushNotificationAppId = = messageType } ? . value receiver ? . providePushRecipient() ? . onMessageReceived(remoteMessage) } }
  83. Initializers class SuperMessagingService : FirebaseMessagingService() { . . . override

    fun onMessageReceived(remoteMessage: RemoteMessage) { super.onMessageReceived(remoteMessage) val messageType = remoteMessage.data["app_id"] val receiver = miniApps.entries.firstOrNull { it.key.pushNotificationAppId = = messageType } ? . value receiver ? . providePushRecipient() ? . onMessageReceived(remoteMessage) } }
  84. Initializers interface MiniApp { /** * Provide an initializer for

    initializing the MiniApp. * Keep this as light as possible. * / fun provideInitializer(): Initializer? /** * Provide logic to handle a push message. * Push messages received here will only be those that have an app_id of * this particular app (unless it's the only app) * / fun providePushRecipient(): PushMessageRecipient? = null }
  85. Binary Compatibility What do we get from Maven?

  86. Binary Compatibility • issues can occur when • compile against

    one version of the library • at runtime, a di f f erent version is present • these versions are not backward compatible
  87. Binary Compatibility • Solutions • upgrade all dependent libraries on

    the new version before calling it • make the change backward compatible
  88. Backward Compatibility interface MiniApp { /** * Provide an initializer

    for initializing the MiniApp. * Keep this as light as possible. * / fun provideInitializer(): Initializer? /** * Provide logic to handle a push message. * Push messages received here will only be those that have an app_id of * this particular app (unless it's the only app) * / fun providePushRecipient(): PushMessageRecipient? = null }
  89. Backward Compatibility interface PushRecipientMarker { fun providePushRecipient(): PushMessageRecipient? = null

    }
  90. Backward Compatibility class SuperMessagingService : FirebaseMessagingService() { . . .

    override fun onMessageReceived(remoteMessage: RemoteMessage) { super.onMessageReceived(remoteMessage) val messageType = remoteMessage.data["app_id"] val receiver = miniApps.entries.firstOrNull { it.key.pushNotificationAppId = = messageType } ? . value (receiver as? PushRecipientMarker) ? . providePushRecipient() ? . onMessageReceived(remoteMessage) } }
  91. Backward Compatibility class SuperMessagingService : FirebaseMessagingService() { . . .

    override fun onMessageReceived(remoteMessage: RemoteMessage) { super.onMessageReceived(remoteMessage) val messageType = remoteMessage.data["app_id"] val receiver = miniApps.entries.firstOrNull { it.key.pushNotificationAppId = = messageType } ? . value (receiver as? PushRecipientMarker) ? . providePushRecipient() ? . onMessageReceived(remoteMessage) } }
  92. Some time later… @Deprecated( message = "Please do not use

    this. The method here is already on MiniApp", level = DeprecationLevel.ERROR ) interface PushRecipientMarker { fun providePushRecipient(): PushMessageRecipient? = null }
  93. Wrapping Up

  94. Things that would have saved us time • Look for

    modularization opportunities • sharing is closer than you might imagine • helps lead to cleaner code • opens up opportunities for open source in the future • Consider a shared design library early on • Establish regular conversations with Android devs throughout the company
  95. Moving Forward • Performance • What else should move to

    platform? • App size • Resources • Shared design system / UI library
  96. Moving Forward • How do we speed up MiniApp development?

    • How do we improve our testing story? • How do we do large refactors across all the MiniApps? • How do we enforce good programming practices?
  97. Ahmed El-Helw (helw.dev / @ahmedre) We were asked to merge

    our apps,
 so all are reachable in just a tap,
 we didn’t have a lot of time,
 so here’s a summary through a rhyme. Convert the apps into libraries,
 resolving manifest ambiguities,
 library dependencies can be a mess,
 Build order manifests make the pain less.
 Handling singletons was next in line,
 Casting to an application is no longer f i ne,
 the Application may not be the same,
 and so our dependencies we need to tame. After all was said and done,
 everything was great under the sun,
 well except the storm brewing about,
 resource con f l icts causing many a doubt.
 
 Once everything agreed to coexist,
 we could walk down our wish list,
 start o f f with building an API,
 in the KDocs we try not to lie.
 Backward compatibility we must do,
 in order to save a day or two,
 of updating MiniApps one by one,
 can be tedious and not quite fun.
 
 Work continues on platform turf,
 with a focus on speed and perf,
 not to mention quality of code,
 which is a never ending winding road.
 All of this is not to ignore,
 critical features to our core,
 tokens, login, and identity too,
 the home screen developed by the crew.
 
 Work like this needs a single team,
 combining our silos was always a dream,
 one that we’re realizing day by day,
 improving the process since release in May.
 
 I hope smaller companies share aars,
 design systems and utility jars,
 communication is an absolute must,
 that and action help build trust.
 
 That is all I have to say,
 Hope you enjoy the rest of your day.