$30 off During Our Annual Pro Sale. View Details »

Building a SuperApp

Ahmed El-Helw
December 31, 2020

Building a SuperApp

Lessons from merging multiple apps into one

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

Ahmed El-Helw

December 31, 2020
Tweet

More Decks by Ahmed El-Helw

Other Decks in Technology

Transcript

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

    View Slide

  2. Multiple Apps

    View Slide

  3. One App - a Super App

    View Slide

  4. View Slide

  5. View Slide

  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

    View Slide

  7. The World Before
    • Silos

    • Very little sharing between projects (almost no common libraries).

    • No mobile platform team

    • Lean Co
    f
    f
    ee

    View Slide

  8. How?

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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 {

    .
    .
    .


    }

    View Slide

  14. Librarisizing the App
    Dependencies
    apk
    app
    aar jar aar jar aar

    View Slide

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

    View Slide

  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

    View Slide

  17. Librarisizing the App
    • run it and make sure all is good

    • publish artifacts to company maven

    • try from new repo - it works!

    View Slide

  18. Repeat and we’re Good?

    View Slide

  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

    View Slide

  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

    View Slide

  21. Initial Complications
    • AndroidManifest con
    f
    l
    icts

    • minSdk version

    • FileProviders

    • Application subclass

    View Slide

  22. Initial Complications
    • Silent Manifest con
    f
    l
    icts

    • launch Activity

    • API keys

    • Application theme

    • Analytics

    • Push Noti
    f
    i
    cations

    View Slide

  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

    View Slide

  24. After lots of tinkering…

    View Slide

  25. It compiles!

    View Slide

  26. But it Insta-crashes.

    View Slide

  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 {

    /
    /
    . . .


    }


    }


    View Slide

  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 {

    /
    /
    . . .


    }


    }


    View Slide

  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 {

    /
    /
    . . .


    }


    }


    View Slide

  30. Application Subclasses
    • critical initialization logic

    • long living objects

    • (context as FooApplication).component.inject(this)

    View Slide

  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

    View Slide

  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 {

    /
    /
    . . .


    }


    }

    View Slide

  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


    }

    View Slide

  34. Application Singletons
    class App : MultiDexApplication() {


    override fun onCreate() {


    super.onCreate()


    RideHailAppInitializer.initialize(this)


    }


    }


    View Slide

  35. Application Singletons
    class App : MultiDexApplication() {


    override fun onCreate() {


    super.onCreate()


    RideHailAppInitializer.initialize(this)


    FoodInitializer.initialize(this)


    }


    }



    View Slide

  36. Application Singletons
    • Existing Application classes migrated to MiniApp containers

    • Consequently, all ui tests have to move to the container as well

    View Slide

  37. Compile and Run

    View Slide

  38. … and it works!

    View Slide

  39. Kinda

    View Slide

  40. RideHail Splash Screen





    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">




    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>


    View Slide

  41. Food Splash Screen


    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">




    android:id="@+id/splashGradientView"


    android:layout_width="match_parent"


    android:layout_height="match_parent"


    android:alpha="0"


    android:background="@color/careemGreen100"
    /
    >




    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"
    /
    >




    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>

    View Slide

  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

    View Slide

  43. Resource naming
    16dp
    <
    /
    dimen>


    72dp
    <
    /
    dimen>


    56dp
    <
    /
    dimen>


    0dp
    <
    /
    dimen>


    0dp
    <
    /
    dimen>
    60dp
    <
    /
    dimen>


    5dp
    <
    /
    dimen>


    8dp
    <
    /
    dimen>


    0dp
    <
    /
    dimen>


    56dp
    <
    /
    dimen>

    View Slide

  44. Resource Name spacing
    • Resources in general (layout, strings, dimens, themes, …)

    • Database names must be unique

    • SharedPreferences
    f
    i
    les should be unique

    View Slide

  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

    View Slide

  46. Library Versioning
    • Build order Manifest to the rescue

    • Helps control dependencies from a single place

    • Removes surprises

    View Slide

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


    }


    }


    }


    View Slide

  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"


    }


    View Slide

  49. Now What?

    View Slide

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

    • Why does every MiniApp initialize…

    • Firebase?

    • Experimentation framework?

    • Analytics providers?

    View Slide

  51. Questions
    • How will push noti
    f
    i
    cations work?

    • How do we handle deep links?

    View Slide

  52. Detour: Two Sets of Libraries

    View Slide

  53. Application Initialization
    class App : MultiDexApplication() {


    override fun onCreate() {


    super.onCreate()


    RideHailAppInitializer.initialize(this)


    FoodInitializer.initialize(this)


    }


    }



    View Slide

  54. MiniApp Library
    • Interfaces

    • Constants

    • sealed classes and enums

    View Slide

  55. MiniApp Library
    • Initializer interface

    View Slide

  56. Initializers
    interface Initializer {


    fun initialize(context: Context)


    }

    View Slide

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


    override fun initialize(context: Context) {


    initializers.forEach {


    it.initialize(this)


    }


    }


    }

    View Slide

  58. Initializers
    class OneTimeInitializer(private val initializer: Initializer) : Initializer {


    override fun initialize(context: Context) {

    .
    .
    .


    }


    }

    View Slide

  59. Initializing MiniApps
    class SuperApp : MultiDexApplication() {


    @Inject


    lateinit var listInitializer: ListInitializer


    override fun onCreate() {


    super.onCreate()


    setupAndInject()

    /
    /
    run all the initializers


    listInitializer.initialize(this)


    }


    }


    View Slide

  60. Initializing MiniApps
    class SuperApp : MultiDexApplication() {


    @Inject


    lateinit var listInitializer: ListInitializer


    override fun onCreate() {


    super.onCreate()


    setupAndInject()

    /
    /
    run all the initializers


    listInitializer.initialize(this)


    }


    }


    View Slide

  61. Initializing MiniApps
    class SuperApp : MultiDexApplication() {


    @Inject


    lateinit var listInitializer: ListInitializer


    override fun onCreate() {


    super.onCreate()


    setupAndInject()

    /
    /
    run all the initializers


    listInitializer.initialize(this)


    }


    }



    View Slide

  62. MiniApp Library
    • Initializer interface

    • MiniApp interface

    View Slide

  63. MiniApps
    interface MiniApp {


    /**


    * Provide an initializer for initializing the MiniApp.


    * Keep this as light as possible.

    *
    /


    fun provideInitializer(): Initializer?


    }

    View Slide

  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


    }


    }


    View Slide

  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

    View Slide

  66. Analytics
    interface AnalyticsAgent {


    fun setUserAttribute(name: String, value: Any?): Boolean


    fun logEvent(eventName: String,


    eventType: EventType = EventType.GENERAL,


    attributes: Map? = null): Boolean


    }

    View Slide

  67. MiniApp Library
    • Initializer interface

    • MiniApp interface

    • MiniApp Factory interface

    View Slide

  68. MiniAppFactory
    interface Dependencies {


    fun provideAnalytics(): AnalyticsProvider


    }
    interface MiniAppFactory {


    fun provideMiniApp(appContext: Context, dependencies: Dependencies): MiniApp


    }

    View Slide

  69. Container Library
    • Part of the SuperApp itself, but exported to maven

    • depends on “MiniApp Library”

    View Slide

  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

    View Slide

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


    }


    }

    View Slide

  72. 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


    }


    View Slide

  73. 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


    }


    View Slide

  74. Initializing MiniApps
    (application as? MiniAppProvider)
    ?
    .
    provideMiniApps()

    ?
    .
    get(MiniAppType.RideHail)

    ?
    .
    provideInitializer()
    ?
    .
    initialize(applicationContext)

    View Slide

  75. Initializing MiniApps
    (application as? MiniAppProvider)
    ?
    .
    provideMiniApps()

    ?
    .
    get(MiniAppType.RideHail)

    ?
    .
    provideInitializer()
    ?
    .
    initialize(applicationContext)

    View Slide

  76. Initializing MiniApps
    (application as? MiniAppProvider)
    ?
    .
    provideMiniApps()

    ?
    .
    get(MiniAppType.RideHail)

    ?
    .
    provideInitializer()
    ?
    .
    initialize(applicationContext)

    View Slide

  77. Initializing MiniApps
    (application as? MiniAppProvider)
    ?
    .
    provideMiniApps()

    ?
    .
    get(MiniAppType.RideHail)

    ?
    .
    provideInitializer()
    ?
    .
    initialize(applicationContext)

    View Slide

  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

    View Slide

  79. Api Changes
    • Let’s support push noti
    f
    i
    cations

    • server will send us an app_id for routing

    View Slide

  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)


    }


    }

    View Slide

  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)


    }


    }

    View Slide

  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)


    }


    }

    View Slide

  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)


    }


    }

    View Slide

  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


    }

    View Slide

  85. Binary Compatibility
    What do we get from Maven?

    View Slide

  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

    View Slide

  87. Binary Compatibility
    • Solutions

    • upgrade all dependent libraries on the new version before calling it

    • make the change backward compatible

    View Slide

  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


    }

    View Slide

  89. Backward Compatibility
    interface PushRecipientMarker {


    fun providePushRecipient(): PushMessageRecipient? = null


    }

    View Slide

  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)


    }


    }

    View Slide

  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)


    }


    }

    View Slide

  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


    }

    View Slide

  93. Wrapping Up

    View Slide

  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

    View Slide

  95. Moving Forward
    • Performance

    • What else should move to platform?

    • App size

    • Resources

    • Shared design system / UI library

    View Slide

  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?

    View Slide

  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.

    View Slide