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

Exploring Dynamic Feature Modules

Exploring Dynamic Feature Modules

Dynamic Feature Modules were announced in 2018 as a new way to modularize and deliver Android apps. It presents a way to decouple features entirely from one another, and avoid a "monoapp" which depends on all code in one place. It has benefits to compile times, app installation size, and enables instant apps - but the feature has had little buzz since then, unknown adoption, and seemingly goes unmaintained now.

Let's change that!

In this talk we'll spend a bit of time reintroducing the dynamic feature Gradle plugin and explain what it's used for, showcase how to extend an existing monoapp with dynamic features, start a new app from scratch and include your existing monoapp as a feature, and uncover all of the gotchas and oddities while doing it.

Alec Strong

January 31, 2023
Tweet

More Decks by Alec Strong

Other Decks in Programming

Transcript

  1. Agenda • Getting Started • Cash App Modularization • Starting

    fresh with dynamic feature modules • Implementation and gotchas
  2. application library dynamic feature api Implements Date App Size #

    Commits Release 2016-02-28 11.42 MB 5771 2.10.0 app
  3. app application library dynamic feature api Implements Date App Size

    # Commits Release 2016-06-15 9.77 MB 6638 2.11.0 keypad
  4. application library dynamic feature api Implements Date App Size #

    Commits Release 2016-09-13 10.45 MB 7520 2.13.2 app app app keypad wire
  5. application library dynamic feature api Implements Date App Size #

    Commits Release 2016-12-19 11.88 MB 8460 2.14.0 app keypad wire protos
  6. application library dynamic feature api Implements Date App Size #

    Commits Release 2017-04-26 13.42 MB 9604 2.15.0 app keypad keypad protos db api address- typeahead scannerview scannerview- sample preferences signature scrubbing
  7. application library dynamic feature api Implements Date App Size #

    Commits Release 2017-04-26 13.42 MB 9604 2.15.1 app protos db api ui-utility sample app
  8. application library dynamic feature api Implements Date App Size #

    Commits Release 2017-08-14 14.94 MB 10585 2.18.1 app protos db api ui-utility sample app backend- utility backend util-hell presenters
  9. application library dynamic feature api Implements Date App Size #

    Commits Release 2017-08-14 14.94 MB 10585 2.18.1 40 modules!
  10. application library dynamic feature api Implements Date App Size #

    Commits Release 2019-01-18 26.65 MB 16588 2.43.0 app ui-utility sample app backend-utility util-hell protos db api backend presenters viewmodels protos db api backend presenters viewmodels
  11. application library dynamic feature api Implements Date App Size #

    Commits Release 2019-01-18 26.65 MB 16588 2.43.0 app protos db api ui-utility sample app backend-utility backend util-hell presenters viewmodels Investing protos db api backend presenters viewmodels
  12. application library dynamic feature api Implements Date App Size #

    Commits Release 2019-01-18 26.65 MB 16588 2.43.0 ui-utility sample app backend-utility util-hell presenters viewmodels Investing protos db api backend presenters viewmodels app backend protos db api
  13. Scheduled Payments proto db api backen presenter application library dynamic

    feature api Implements Date App Size # Commits Release 2019-03-08 25.28 MB 17102 2.46.0 db api ui-utility sample app backend-utility util-hell presenters viewmodels backend:impl protos app backend:api Investing protos db api backend presenters viewmodels
  14. application library dynamic feature api Implements Date App Size #

    Commits Release 2019-03-27 25.28 MB 17105 2.46.1 db api ui-utility sample app backend-utility util-hell presenters viewmodels backend:impl protos app backend:api Investing proto db api backen presenter Scheduled Payments proto db api backen presenter common-hell
  15. application library dynamic feature api Implements Date App Size #

    Commits Release 2019-03-27 25.28 MB 17105 2.46.1 80 modules!
  16. application library dynamic feature api Implements Date App Size #

    Commits Release 2019-07-25 26.34 MB 19733 2.55.0 db api ui-utility sample app backend-utility util-hell presenters viewmodels backend:impl protos app backend:api Investing proto db api backen presenter Scheduled Payments proto db api backen presenter common-hell Boost proto db api backen presenter Card Drawer proto db api backen presenter
  17. application library dynamic feature api Implements Date App Size #

    Commits Release 2019-09-26 27.03 MB 21478 2.59.0 db api ui-utility sample app backend-utility util-hell presenters viewmodels backend:impl protos app backend:api Investing proto db api backen presenter Scheduled Payments proto db api backen presenter common-hell Boost proto db api backen presenter Card Drawer proto db api backen presenter Lending proto db api backen presenter Support proto db api backen presenter qr-codes proto db api backen presenter Google pay proto db api backen presenter
  18. application library dynamic feature api Implements Date App Size #

    Commits Release 2019-10-17 27.01 MB 22025 3.0.1 db api ui-utility sample app backend-utility util-hell presenters viewmodels backend:impl app backend:api Investing proto db api backen presenter Scheduled Payments proto db api backen presenter common-hell Boost proto db api backen presenter Card Drawer proto db api backen presenter Lending proto db api backen presenter Support proto db api backen presenter qr-codes proto db api backen presenter Google pay proto db api backen presenter protos
  19. application library dynamic feature api Implements Date App Size #

    Commits Release 2020-01-08 37.25 MB 24223 3.5.1 db api ui-utility sample app backend-utility util-hell presenters viewmodels backend:impl app backend:api Investing proto db api backen presenter Scheduled Payments proto db api backen presenter common-hell Boost proto db api backen presenter Card Drawer proto db api backen presenter Lending proto db api backen presenter Support proto db api backen presenter qr-codes proto db api backen presenter Google pay proto db api backen presenter protos recurring proto db api backen presenter liveness-check proto db api backen presenter
  20. application library dynamic feature api Implements Date App Size #

    Commits Release 2020-03-15 41.60 MB 26076 3.10.0 db api ui-utility sample app backend-utility util-hell presenters viewmodels backend:impl app backend:api Investing proto db api backen presenter Scheduled Payments proto db api backen presenter common-hell Boost proto db api backen presenter Card Drawer proto db api backen presenter Lending proto db api backen presenter Support proto db api backen presenter qr-codes proto db api backen presenter Google pay proto db api backen presenter protos recurring proto db api backen presenter liveness-check proto db api backen presenter app-messages proto db api backen presenter bitcoin proto db api backen presenter
  21. application library dynamic feature api Implements Date App Size #

    Commits Release 2020-03-15 41.60 MB 26076 3.10.0 200 modules!
  22. application library dynamic feature api Implements Date App Size #

    Commits Release 2022-08-19 81.43 MB 55805 3.72.0 db api ui-utility sample app backend- utility util-hell presenters viewmodel backend:imp app backend:api Investin proto db api backe presente Scheduled proto db api backe presente common-hell Boost proto db api backe presente Card Drawer proto db api backe presente Lending proto db api backe presente Support proto db api backe presente qr- proto db api backe presente Google pay proto db api backe presente protos recurring proto db api backe presente liveness-check proto db api backe presente app-messages proto db api backe presente bitcoin proto db api backe presente
  23. application library dynamic feature api Implements Date App Size #

    Commits Release 2022-08-19 81.43 MB 55805 3.72.0 800 modules!
  24. application library dynamic feature api Implements Date App Size #

    Commits Release 2022-08-19 81.43 MB 55805 3.72.0
  25. application library dynamic feature api Implements Date App Size #

    Commits Release 2022-08-19 81.43 MB 55805 3.72.0
  26. application library dynamic feature api Implements Date App Size #

    Commits Release 2022-08-19 81.43 MB 55805 3.72.0 db api ui-utility sample app backend- utility util-hell presenters viewmodel backend:imp app backend:api Investin proto db api backe presente Scheduled proto db api backe presente common-hell Boost proto db api backe presente Card Drawer proto db api backe presente Lending proto db api backe presente Support proto db api backe presente qr- proto db api backe presente Google pay proto db api backe presente protos recurring proto db api backe presente liveness-check proto db api backe presente app-messages proto db api backe presente bitcoin proto db api backe presente
  27. application library dynamic feature api Implements app db api ui-utility

    sample app backend- utility util-hell presenters viewmodel backend:imp backend:api Investin proto db api backe presente Scheduled proto db api backe presente common-hell Boost proto db api backe presente Card Drawer proto db api backe presente Lending proto db api backe presente Support proto db api backe presente qr- proto db api backe presente Google pay proto db api backe presente protos recurring proto db api backe presente liveness-check proto db api backe presente app-messages proto db api backe presente bitcoin proto db api backe presente
  28. application library dynamic feature api Implements app Card feature1 feature2

    Inv Schedul Boost Le Su qr- Google recurrin liveness- app- bitcoin
  29. application library dynamic feature api Implements app Card Drawer proto

    db api backe presente Inv Schedul Boost Le Su qr- Google recurrin liveness- app- bitcoin feature1 feature2
  30. application library dynamic feature api Implements app Inv Schedul Boost

    Le Su qr- Google recurrin liveness- app- bitcoin
  31. application library dynamic feature api Implements app Inv Schedul Boost

    Le Su qr- Google recurrin liveness- app- bitcoin app2
  32. > Task :boxapp:checkInternalDebugLibraries FAILED FAILURE: Build failed with an exception.

    * What went wrong: Execution failed for task ‘:boxapp:checkInternalDebugLibraries'. > [:app, :app] all package the same library [androidx.lifecycle:lifecycle-service].
  33. Installation did not succeed. The application could not be installed:

    INSTALL_PARSE_FAILED_MANIFEST_MALFORMED List of apks: [0] '/Users/astrong/Development/Android/cash-android/cash-os/build/intermediates/apk/internal/debug/cash-os-internal debug.apk' [1] '/Users/astrong/Development/Android/cash-android/app/build/intermediates/apk/internal/debug/app-internal-debug. [2] '/Users/astrong/Development/Android/cash-android/bigboxfeature/build/intermediates/apk/debug/bigboxfeature-deb [3] '/Users/astrong/Development/Android/cash-android/bigboxfeature/api/build/intermediates/apk/debug/api-debug.apk [4] '/Users/astrong/Development/Android/cash-android/smallboxfeature/build/intermediates/apk/debug/smallboxfeature debug.apk' Installation failed due to: 'Failed to commit install session 1726590139 with command package install-commit 1726590139. E INSTALL_PARSE_FAILED_MANIFEST_MALFORMED: Failed parse during installPackageLI: /data/app/vmdl1726590139.tmp/base Binary XML file line #163): <meta-data> requires an android:value or android:resource attribute' Retry Failed to launch an application on all devices
  34. > Task :cash-os:installInternalDebug Generating APKs for device 'Pixel_4a_API_32(AVD) - 12'

    for :cash-os:internalDebug The APKs have been extracted in the directory: /var/folders/9g/ 4r7qgd5s1096hyk8r_nzsw940000gn/T/apkSelect1137704977225312066 Installing APKs 'base-xxhdpi.apk, base-master.apk, base-en.apk, base-arm64_v8a.apk' on 'Pixel_4a_API_32(AVD) - 12' for :cash-os:internalDebug [SplitApkInstallerBase]: Failed to commit install session 1207711244 with command cmd package install-commit 1207711244. Error: INSTALL_PARSE_FAILED_MANIFEST_MALFORMED: Failed parse during installPackageLI: /data/app/vmdl1207711244.tmp/base.apk (at Binary XML file line #168): <meta-data> requires an android:value or android:resource attribute Unable to install /var/folders/9g/4r7qgd5s1096hyk8r_nzsw940000gn/T/ apkSelect1137704977225312066/base-xxhdpi.apk,/var/folders/9g/ 4r7qgd5s1096hyk8r_nzsw940000gn/T/apkSelect1137704977225312066/base-master.apk,/var/ folders/9g/4r7qgd5s1096hyk8r_nzsw940000gn/T/apkSelect1137704977225312066/base- en.apk,/var/folders/9g/4r7qgd5s1096hyk8r_nzsw940000gn/T/apkSelect1137704977225312066/ base-arm64_v8a.apk com.android.ddmlib.InstallException: Failed to commit install session 1207711244 with command cmd package install-commit 1207711244. Error: INSTALL_PARSE_FAILED_MANIFEST_MALFORMED: Failed parse during installPackageLI: /data/ app/vmdl1207711244.tmp/base.apk (at Binary XML file line #168): <meta-data> requires an android:value or android:resource attribute
  35. Execution failed for task ':cash-os:processInternalDebugResources'. > A failure occurred while

    executing com.android.build.gradle.internal.res.LinkApplicationAndroidResourcesTask$TaskAction > Android resource linking failed warn: removing resource com.squareup.cash.beta.debug:string/ pi2_governmentid_back_button without required default value. /Users/astrong/Development/Android/cash-android/cash-os/build/intermediates/ packaged_manifests/internalDebug/AndroidManifest.xml:1479: error: resource style/ Theme.PlayCore.Transparent (aka com.squareup.cash.beta.debug:style/ Theme.PlayCore.Transparent) not found. error: failed processing manifest.
  36. <resources> <!-- This whole file is just to make manifest

    merging work for feature modules --> <!-- See https://medium.com/androiddevelopers/a-patchwork-plaid-monolith-to- modularized-app-60235d9f212e --> <style name="Theme.PlayCore.Transparent" /> </resources>
  37. boxapp Row(modifier = Modifier.weight(1f)) { Column( modifier = Modifier.padding(10.dp), verticalArrangement

    = Arrangement.spacedBy(10.dp) ) { boxFeatures().forEach { it.Tile() } } }
  38. cashapp class CashFeature : BoxAppFeature { private val navigator get()

    = Navigator.INSTANCE @Composable override fun Tile() { Box( Modifier .size(width = 200.dp, height = 60.dp) .background(Color(255, 222, 133)) .padding(20.dp) .clickable { navigator.startActivity( "com.squareup.cash.beta.debug", "com.squareup.cash.ui.MainActivity" ) } ) { Text("Cash App!") } } }
  39. cashapp class CashFeature : BoxAppFeature { private val navigator get()

    = Navigator.INSTANCE @Composable override fun Tile() { Box( Modifier .size(width = 200.dp, height = 60.dp) .background(Color(255, 222, 133)) .padding(20.dp) .clickable { navigator.startActivity( "com.squareup.cash.beta.debug", "com.squareup.cash.ui.MainActivity" ) } ) { Text("Cash App!") } } }
  40. boxapp Row(modifier = Modifier.weight(1f)) { Column( modifier = Modifier.padding(10.dp), verticalArrangement

    = Arrangement.spacedBy(10.dp) ) { boxFeatures().forEach { it.Tile() } } }
  41. boxapp Row(modifier = Modifier.weight(1f)) { Column( modifier = Modifier.padding(10.dp), verticalArrangement

    = Arrangement.spacedBy(10.dp) ) { arrayOf( Class.forName("com.squareup.cash.CashFeature").newInstance() as BoxAppFeature, ) .forEach { it.Tile() } } }
  42. feature class BigBoxFeature : BoxAppFeature { private val navigator get()

    = Navigator.INSTANCE @Composable override fun Tile() { Box( Modifier .size(width = 200.dp, height = 60.dp) .background(Color(255, 222, 133)) .padding(20.dp) .clickable { navigator.goTo { BigBoxScreen() } } ) { Text("Big Box Feature!") } } }
  43. feature internal class BigBoxScreen @Inject constructor() : BigBoxMainScreen { val

    widgets = mutableListOf<@Composable () -> Unit>() override fun registerWidget(widget: @Composable () -> Unit) { widgets.add(widget) } @Composable fun view() { Column() { Spacer(modifier = Modifier.weight(1f)) Text(text = "Big Box Main Screen!") Spacer(modifier = Modifier.weight(1f)) widgets.forEach { it() Spacer(modifier = Modifier.weight(1f)) } } } }
  44. feature internal class BigBoxScreen @Inject constructor() : BigBoxMainScreen { val

    widgets = mutableListOf<@Composable () -> Unit>() override fun registerWidget(widget: @Composable () -> Unit) { widgets.add(widget) } @Composable fun view() { Column() { Spacer(modifier = Modifier.weight(1f)) Text(text = "Big Box Main Screen!") Spacer(modifier = Modifier.weight(1f)) widgets.forEach { it() Spacer(modifier = Modifier.weight(1f)) } } } }
  45. cashapp class CashApp : Application { @Inject lateinit var featureInterfaces:

    FeatureInterfaces override fun onCreate() { featureInterfaces.whenInstalled<BigBoxMainScreen> { it.registerWidget { MooncakeTheme( theme = this.moonCakeLight() ) { BalanceBox( instrumentManager, moneyFormatterFactory.createStandard(), ) } } } }
  46. cashapp class CashApp : Application { @Inject lateinit var featureInterfaces:

    FeatureInterfaces override fun onCreate() { featureInterfaces.whenInstalled<BigBoxMainScreen> { it.registerWidget { MooncakeTheme( theme = this.moonCakeLight() ) { BalanceBox( instrumentManager, moneyFormatterFactory.createStandard(), ) } } } }
  47. cashapp class CashFeature : BoxAppFeature { private val navigator get()

    = Navigator.INSTANCE @Composable override fun Tile() { Box( Modifier .size(width = 200.dp, height = 60.dp) .background(Color(255, 222, 133)) .padding(20.dp) .clickable { navigator.startActivity( "com.squareup.cash.beta.debug", "com.squareup.cash.ui.MainActivity" ) } ) { Text("Cash App!") } } }
  48. cashapp class CashFeature @Inject constructor( private val navigator: Navigator )

    : BoxAppFeature { @Composable override fun Tile() { Box( Modifier .size(width = 200.dp, height = 60.dp) .background(Color(255, 222, 133)) .padding(20.dp) .clickable { navigator.startActivity( "com.squareup.cash.beta.debug", "com.squareup.cash.ui.MainActivity" ) } ) { Text("Cash App!") } } }
  49. boxapp @Component( modules = [BoxModule::class] ) interface BoxComponent { fun

    navigator(): Navigator fun featureInterfaces(): FeatureInterfaces @Component.Builder interface Builder { @BindsInstance fun navigator(navigator: Navigator): Builder fun build(): BoxComponent } }
  50. feature class BigBoxEntry( private val component: BoxComponent ): FeatureEntry {

    @Inject internal lateinit var featureRegistry: FeatureRegistry @Inject internal lateinit var bigBoxScreen: BigBoxScreen @Inject internal lateinit var featureInterfaces: FeatureInterfaces override fun start() { val bigBoxComponent = DaggerBigBoxComponent.builder() .boxComponent(component) .build() bigBoxComponent.inject(this) featureInterfaces.install<BigBoxMainScreen>(bigBoxScreen) featureRegistry.register(bigBoxComponent.bigBoxFeature()) } }
  51. feature class BigBoxEntry( private val component: BoxComponent ): FeatureEntry {

    @Inject internal lateinit var featureRegistry: FeatureRegistry @Inject internal lateinit var bigBoxScreen: BigBoxScreen @Inject internal lateinit var featureInterfaces: FeatureInterfaces override fun start() { val bigBoxComponent = DaggerBigBoxComponent.builder() .boxComponent(component) .build() bigBoxComponent.inject(this) featureInterfaces.install<BigBoxMainScreen>(bigBoxScreen) featureRegistry.register(bigBoxComponent.bigBoxFeature()) } }
  52. feature class BigBoxEntry( private val component: BoxComponent ): FeatureEntry {

    @Inject internal lateinit var featureRegistry: FeatureRegistry @Inject internal lateinit var bigBoxScreen: BigBoxScreen @Inject internal lateinit var featureInterfaces: FeatureInterfaces override fun start() { val bigBoxComponent = DaggerBigBoxComponent.builder() .boxComponent(component) .build() bigBoxComponent.inject(this) featureInterfaces.install<BigBoxMainScreen>(bigBoxScreen) featureRegistry.register(bigBoxComponent.bigBoxFeature()) } }
  53. boxapp override fun onCreate(savedInstanceState: Bundle?) { features.forEach { it.newInstance(component).start() }

    } private val features = arrayOf( Class.forName(“app.cash.boxapp.bigboxfeature.BigBoxEntry"), Class.forName(“app.cash.CashAppEntry") ).map { @Suppress("UNCHECKED_CAST") it.getConstructor(BoxComponent::class.java) as Constructor<FeatureEntry> }
  54. boxapp val splitInstallManager = SplitInstallManagerFactory.create(context) val request = SplitInstallRequest .newBuilder()

    .addModule("big-box-feature") .build() var mySessionId = 0 splitInstallManager .startInstall(request) .addOnSuccessListener { sessionId -> mySessionId = sessionId } .addOnFailureListener { exception -> ... }
  55. boxapp val listener = SplitInstallStateUpdatedListener { state -> if (state.sessionId()

    == mySessionId && state.status() == INSTALLED) { (Class.forName("app.cash.boxapp.bigboxfeature.BigBoxEntry") .getConstructor(BoxComponent::class.java) as Constructor<FeatureEntry>) .newInstance(component).start() } } splitInstallManager.registerListener(listener)