Slide 1

Slide 1 text

Ahmed El-Helw { helw.dev / @ahmedre } Building a SuperApp Lessons from merging multiple apps into one

Slide 2

Slide 2 text

Multiple Apps

Slide 3

Slide 3 text

One App - a Super App

Slide 4

Slide 4 text

No content

Slide 5

Slide 5 text

No content

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

The World Before • Silos • Very little sharing between projects (almost no common libraries). • No mobile platform team • Lean Co f f ee

Slide 8

Slide 8 text

How?

Slide 9

Slide 9 text

• App will ship using the applicationId of our largest app • Apps can continue shipping their standalone apps until the business decides to remove them

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

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 { . . . }

Slide 14

Slide 14 text

Librarisizing the App Dependencies apk app aar jar aar jar aar

Slide 15

Slide 15 text

Librarisizing the App Dependencies aar aar jar aar jar aar apk SuperApp apk Standalone lib

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

Librarisizing the App • run it and make sure all is good • publish artifacts to company maven • try from new repo - it works!

Slide 18

Slide 18 text

Repeat and we’re Good?

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

Initial Complications • AndroidManifest con f l icts • minSdk version • FileProviders • Application subclass

Slide 22

Slide 22 text

Initial Complications • Silent Manifest con f l icts • launch Activity • API keys • Application theme • Analytics • Push Noti f i cations

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

After lots of tinkering…

Slide 25

Slide 25 text

It compiles!

Slide 26

Slide 26 text

But it Insta-crashes.

Slide 27

Slide 27 text

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 { / / . . . } }

Slide 28

Slide 28 text

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 { / / . . . } }

Slide 29

Slide 29 text

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 { / / . . . } }

Slide 30

Slide 30 text

Application Subclasses • critical initialization logic • long living objects • (context as FooApplication).component.inject(this)

Slide 31

Slide 31 text

Application Subclasses • Move initialization logic into a separate Initializer class • Move singleton object references to singleton classes • container is responsible for calling initialization logic

Slide 32

Slide 32 text

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 { / / . . . } }

Slide 33

Slide 33 text

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 }

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

Application Singletons • Existing Application classes migrated to MiniApp containers • Consequently, all ui tests have to move to the container as well

Slide 37

Slide 37 text

Compile and Run

Slide 38

Slide 38 text

… and it works!

Slide 39

Slide 39 text

Kinda

Slide 40

Slide 40 text

RideHail Splash Screen < / FrameLayout>

Slide 41

Slide 41 text

Food Splash Screen < / FrameLayout>

Slide 42

Slide 42 text

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

Slide 43

Slide 43 text

Resource naming 16dp < / dimen> 72dp < / dimen> 56dp < / dimen> 0dp < / dimen> 0dp < / dimen> 60dp < / dimen> 5dp < / dimen> 8dp < / dimen> 0dp < / dimen> 56dp < / dimen>

Slide 44

Slide 44 text

Resource Name spacing • Resources in general (layout, strings, dimens, themes, …) • Database names must be unique • SharedPreferences f i les should be unique

Slide 45

Slide 45 text

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

Slide 46

Slide 46 text

Library Versioning • Build order Manifest to the rescue • Helps control dependencies from a single place • Removes surprises

Slide 47

Slide 47 text

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' } } } }

Slide 48

Slide 48 text

Library Versioning dependencies { api platform(“com.careem.superapp.core:platform:$platform_version”) / / okhttp & lottie implementation "com.squareup.okhttp3:okhttp" implementation "com.airbnb.android:lottie" }

Slide 49

Slide 49 text

Now What?

Slide 50

Slide 50 text

Questions • Does initializing every MiniApp on app start make sense? • Why does every MiniApp initialize… • Firebase? • Experimentation framework? • Analytics providers?

Slide 51

Slide 51 text

Questions • How will push noti f i cations work? • How do we handle deep links?

Slide 52

Slide 52 text

Detour: Two Sets of Libraries

Slide 53

Slide 53 text

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

Slide 54

Slide 54 text

MiniApp Library • Interfaces • Constants • sealed classes and enums

Slide 55

Slide 55 text

MiniApp Library • Initializer interface

Slide 56

Slide 56 text

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

Slide 57

Slide 57 text

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

Slide 58

Slide 58 text

Initializers class OneTimeInitializer(private val initializer: Initializer) : Initializer { override fun initialize(context: Context) { . . . } }

Slide 59

Slide 59 text

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

Slide 60

Slide 60 text

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

Slide 61

Slide 61 text

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

Slide 62

Slide 62 text

MiniApp Library • Initializer interface • MiniApp interface

Slide 63

Slide 63 text

MiniApps interface MiniApp { /** * Provide an initializer for initializing the MiniApp. * Keep this as light as possible. * / fun provideInitializer(): Initializer? }

Slide 64

Slide 64 text

Initializing MiniApps class SuperApp : MultiDexApplication() { @Inject lateinit var miniApps: List<@JvmSuppressWildcards MiniApp> override fun onCreate() { super.onCreate() setupAndInject() / / run mini app initializers when needed } }

Slide 65

Slide 65 text

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

Slide 66

Slide 66 text

Analytics interface AnalyticsAgent { fun setUserAttribute(name: String, value: Any?): Boolean fun logEvent(eventName: String, eventType: EventType = EventType.GENERAL, attributes: Map? = null): Boolean }

Slide 67

Slide 67 text

MiniApp Library • Initializer interface • MiniApp interface • MiniApp Factory interface

Slide 68

Slide 68 text

MiniAppFactory interface Dependencies { fun provideAnalytics(): AnalyticsProvider } interface MiniAppFactory { fun provideMiniApp(appContext: Context, dependencies: Dependencies): MiniApp }

Slide 69

Slide 69 text

Container Library • Part of the SuperApp itself, but exported to maven • depends on “MiniApp Library”

Slide 70

Slide 70 text

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

Slide 71

Slide 71 text

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()) } }

Slide 72

Slide 72 text

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

Slide 73

Slide 73 text

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

Slide 74

Slide 74 text

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

Slide 75

Slide 75 text

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

Slide 76

Slide 76 text

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

Slide 77

Slide 77 text

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

Slide 78

Slide 78 text

Implications • MiniApps can test on their own • just depend on the container library! • no-op packages make it easy to mock out full MiniApps

Slide 79

Slide 79 text

Api Changes • Let’s support push noti f i cations • server will send us an app_id for routing

Slide 80

Slide 80 text

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) } }

Slide 81

Slide 81 text

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) } }

Slide 82

Slide 82 text

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) } }

Slide 83

Slide 83 text

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) } }

Slide 84

Slide 84 text

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 }

Slide 85

Slide 85 text

Binary Compatibility What do we get from Maven?

Slide 86

Slide 86 text

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

Slide 87

Slide 87 text

Binary Compatibility • Solutions • upgrade all dependent libraries on the new version before calling it • make the change backward compatible

Slide 88

Slide 88 text

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 }

Slide 89

Slide 89 text

Backward Compatibility interface PushRecipientMarker { fun providePushRecipient(): PushMessageRecipient? = null }

Slide 90

Slide 90 text

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) } }

Slide 91

Slide 91 text

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) } }

Slide 92

Slide 92 text

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 }

Slide 93

Slide 93 text

Wrapping Up

Slide 94

Slide 94 text

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

Slide 95

Slide 95 text

Moving Forward • Performance • What else should move to platform? • App size • Resources • Shared design system / UI library

Slide 96

Slide 96 text

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?

Slide 97

Slide 97 text

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.