Dynamic Feature Module in Practice / DroidKaigi 2020

Dynamic Feature Module in Practice / DroidKaigi 2020

80a3a3857a55f154d23acb705eff72cc?s=128

star_zero

March 04, 2020
Tweet

Transcript

  1. Dynamic Feature Module in Practice DroidKaigi 2020 2020/02/21 - 実践

    Dynamic Feature Module -
  2. About me •Kenji Abe •Cookpad Inc. •Google Developers Expert for

    Android •Twitter: @STAR_ZERO
  3. Talk about •What's Dynamic Feature Module •How to create Dynamic

    Feature Module •How to test Dynamic Feature Module •etc...
  4. What's Dynamic Feature Module

  5. Dynamic Feature Module •Dynamic delivery of modules using App Bundle

    •Modules can be installed and removed later •Can reduce app size
  6. Delivery option •On-demand delivery ‣ Modules are not included at

    install time ‣ Install modules later as needed •At-install delivery ‣ Modules are included at install time ‣ Can be removed later
  7. Delivery option •Conditional delivery ‣ Install modules only on devices

    that match the conditions ‣ Device features, User country, API Level •Google Play Instant ‣ "Try Now" button on the Google Play Store ‣ I will not talk about it this time
  8. Use cases •Installing features that are not used by the

    majority of users as needed ‣ Posting feature in the app that most user only browse ‣ Help and support features •Remove no longer needed feature ‣ Register user account ‣ Onboarding flow •etc...
  9. Create Dynamic Feature Module

  10. File → New → New Module...

  11. None
  12. None
  13. None
  14. None
  15. None
  16. None
  17. Dynamic Feature Module

  18. apply plugin: 'com.android.dynamic-feature' android { compileSdkVersion 29 defaultConfig { minSdkVersion

    21 targetSdkVersion 29 } } dependencies { implementation project(':app') }
  19. apply plugin: 'com.android.application' android { // ... dynamicFeatures = [":feature"]

    }
  20. <manifest package="com.example.dfm.feature"> <dist:module dist:instant="false" dist:title="@string/title_feature"> <dist:delivery> <dist:on-demand /> </dist:delivery> <dist:fusing

    dist:include="true" /> </dist:module> </manifest>
  21. <manifest package="com.example.dfm.feature"> <dist:module dist:instant="false" dist:title="@string/title_feature"> <dist:delivery> <dist:on-demand /> </dist:delivery> <dist:fusing

    dist:include="true" /> </dist:module> </manifest> On demand delivery
  22. <manifest package="com.example.dfm.feature"> <dist:module dist:instant="false" dist:title="@string/title_feature"> <dist:delivery> <dist:install-time /> </dist:delivery> <dist:fusing

    dist:include="true" /> </dist:module> </manifest> Include at install time
  23. Modules dependencies

  24. app feature_one feature_two Library module implementation project(':feature_one') implementation project(':feature_two')

  25. app feature_one feature_two Using Dynamic Feature Module dynamicFeatures = [":feature_one",

    ":feature_two"] implementation project(':app') implementation project(':app')
  26. Install / Remove

  27. // app/build.gradle dependencies { api "com.google.android.play:core:1.6.4" }

  28. <!-- AndroidManifest --> <manifest xmlns:android="http://schemas.android.com/apk/res/android"> <application android:name="com.google.android.play.core.splitcompat.SplitCompatApplication"> <!-- ... -->

    </application> </manifest>
  29. <!-- AndroidManifest --> <manifest xmlns:android="http://schemas.android.com/apk/res/android"> <application android:name="com.google.android.play.core.splitcompat.SplitCompatApplication"> <!-- ... -->

    </application> </manifest> // Application class class App : SplitCompatApplication() { // ... } OR
  30. // in dynamic feature module class FeatureActivity : AppCompatActivity() {

    override fun attachBaseContext(base: Context) { super.attachBaseContext(base) SplitCompat.installActivity(this) } // ... }
  31. // in app module class MainActivity : AppCompatActivity() { override

    fun attachBaseContext(base: Context) { super.attachBaseContext(base) SplitCompat.installActivity(this) } // ... }
  32. val manager = SplitInstallManagerFactory.create(context) if (manager.installedModules.contains("feature")) { // Already Installed

    } val request = SplitInstallRequest.newBuilder() .addModule("feature") .build() manager.startInstall(request) .addOnSuccessListener { /* Success install request */ } .addOnFailureListener { /* Failure install request */ }
  33. val manager = SplitInstallManagerFactory.create(context) if (manager.installedModules.contains("feature")) { // Already Installed

    } val request = SplitInstallRequest.newBuilder() .addModule("feature") .build() manager.startInstall(request) .addOnSuccessListener { /* Success install request */ } .addOnFailureListener { /* Failure install request */ } SplitInstallManager
  34. val manager = SplitInstallManagerFactory.create(context) if (manager.installedModules.contains("feature")) { // Already Installed

    } val request = SplitInstallRequest.newBuilder() .addModule("feature") .build() manager.startInstall(request) .addOnSuccessListener { /* Success install request */ } .addOnFailureListener { /* Failure install request */ } Check if module is already Installed
  35. val manager = SplitInstallManagerFactory.create(context) if (manager.installedModules.contains("feature")) { // Already Installed

    } val request = SplitInstallRequest.newBuilder() .addModule("feature") .build() manager.startInstall(request) .addOnSuccessListener { /* Success install request */ } .addOnFailureListener { /* Failure install request */ } Specify module name to install
  36. val manager = SplitInstallManagerFactory.create(context) if (manager.installedModules.contains("feature")) { // Already Installed

    } val request = SplitInstallRequest.newBuilder() .addModule("feature") .build() manager.startInstall(request) .addOnSuccessListener { /* Success start install */ } .addOnFailureListener { /* Failure start install */ } Start to install
  37. private val listener = SplitInstallStateUpdatedListener { state -> when (state.status())

    { SplitInstallSessionStatus.INSTALLED -> // ... // ... } } override fun onStart() { super.onStart() manager.registerListener(listener) } override fun onStop() { super.onStop() manager.unregisterListener(listener) }
  38. private val listener = SplitInstallStateUpdatedListener { state -> when (state.status())

    { SplitInstallSessionStatus.INSTALLED -> // ... // ... } } override fun onStart() { super.onStart() manager.registerListener(listener) } override fun onStop() { super.onStop() manager.unregisterListener(listener) } Observe installation state (SplitInstallSessionState#status)
  39. private val listener = SplitInstallStateUpdatedListener { state -> when (state.status())

    { SplitInstallSessionStatus.INSTALLED -> // ... // ... } } override fun onStart() { super.onStart() manager.registerListener(listener) } override fun onStop() { super.onStop() manager.unregisterListener(listener) }
  40. private val listener = SplitInstallStateUpdatedListener { state -> when (state.status())

    { SplitInstallSessionStatus.INSTALLED -> // ... // ... } } override fun onStart() { super.onStart() manager.registerListener(listener) } override fun onStop() { super.onStop() manager.unregisterListener(listener) }
  41. •PENDING •REQUIRES_USER_CONFIRMATION •DOWNLOADING •DOWNLOADED •INSTALLING •INSTALLED •FAILED •CANCELING •CANCELED

  42. •PENDING •REQUIRES_USER_CONFIRMATION •DOWNLOADING •DOWNLOADED •INSTALLING •INSTALLED •FAILED •CANCELING •CANCELED SplitInstallStateUpdatedListener

    { state -> when (state.status()) { SplitInstallSessionStatus.DOWNLOADING -> { val total = state.totalBytesToDownload() val downloaded = state.bytesDownloaded() // Show progress } } }
  43. •PENDING •REQUIRES_USER_CONFIRMATION •DOWNLOADING •DOWNLOADED •INSTALLING •INSTALLED •FAILED •CANCELING •CANCELED Requires

    user's confirmation if module size exceeds 10MB
  44. •PENDING •REQUIRES_USER_CONFIRMATION •DOWNLOADING •DOWNLOADED •INSTALLING •INSTALLED •FAILED •CANCELING •CANCELED SplitInstallStateUpdatedListener

    { state -> when (state.status()) { SplitInstallSessionStatus.REQUIRES_USER_CONFIRMATION -> { manager.startConfirmationDialogForResult( state, activity, REQUEST_CODE ) } } }
  45. •PENDING •REQUIRES_USER_CONFIRMATION •DOWNLOADING •DOWNLOADED •INSTALLING •INSTALLED •FAILED •CANCELING •CANCELED

  46. Module title

  47. None
  48. <!-- feature/AndroidManifest.xml --> <dist:module dist:instant="false" dist:title="@string/title_feature"> <!-- ... --> </dist:module>

  49. •PENDING •REQUIRES_USER_CONFIRMATION •DOWNLOADING •DOWNLOADED •INSTALLING •INSTALLED •FAILED •CANCELING •CANCELED

  50. •PENDING •REQUIRES_USER_CONFIRMATION •DOWNLOADING •DOWNLOADED •INSTALLING •INSTALLED •FAILED •CANCELING •CANCELED Module

    Installation completed Can use module resources
  51. manager.deferredInstall(listOf("feature")) Deferred install

  52. manager.deferredUninstall(listOf("feature")) Remove

  53. play:core-ktx

  54. lifecycleScope.launch { try { manager.requestInstall(listOf("feature")) } catch (e: SplitInstallException) {

    // ... } } Request for installation using Coroutines
  55. private val listener = SplitInstallStateUpdatedListener( onRequiresConfirmation = { state ->

    // ... }, onInstalled = { state -> // ... } ) Observe installation state
  56. lifecycleScope.launch { manager.requestProgressFlow().collect { state -> when (state.status()) { SplitInstallSessionStatus.INSTALLED

    -> { Timber.d("SplitInstallSessionStatus.INSTALLED") } // ... } } } Observe installation state using Flow
  57. Conditional delivery

  58. Conditional delivery •Install modules only on devices that match the

    conditions ‣ Hardware and software features ‣ User country ‣ API Level •Can be installed later even if conditions do not match
  59. None
  60. <dist:module> <dist:delivery> <dist:install-time> <dist:conditions> <dist:device-feature dist:name="android.hardware.nfc" /> </dist:conditions> </dist:install-time> </dist:delivery>

    </dist:module>
  61. <dist:module> <dist:delivery> <dist:install-time> <dist:conditions> <dist:device-feature dist:name="android.hardware.nfc" /> </dist:conditions> </dist:install-time> </dist:delivery>

    </dist:module> Require NFC feature
  62. <dist:module> <dist:delivery> <dist:install-time> <dist:conditions> <dist:user-countries dist:exclude="true"> <dist:country dist:code="JP" /> </dist:user-countries>

    <dist:min-sdk dist:value="24"/> </dist:conditions> </dist:install-time> </dist:delivery> <dist:fusing dist:include="true" /> </dist:module>
  63. <dist:module> <dist:delivery> <dist:install-time> <dist:conditions> <dist:user-countries dist:exclude="true"> <dist:country dist:code="JP" /> </dist:user-countries>

    <dist:min-sdk dist:value="24"/> </dist:conditions> </dist:install-time> </dist:delivery> <dist:fusing dist:include="true" /> </dist:module> Exclude Japan
  64. <dist:module> <dist:delivery> <dist:install-time> <dist:conditions> <dist:user-countries dist:exclude="true"> <dist:country dist:code="JP" /> </dist:user-countries>

    <dist:min-sdk dist:value="24"/> </dist:conditions> </dist:install-time> </dist:delivery> <dist:fusing dist:include="true" /> </dist:module> API Level 24 or more
  65. Test / Debug

  66. Test / Debug •Google Play internal test •Internal App Sharing

    •FakeSplitInstallManager
  67. Google Play internal test •Release build aab file is required

    ‣ Needs release signing and correct VersionCode •It takes time and effort •Can not debug
  68. Internal App Sharing •Can upload debug build aab ‣ Debug

    signing, any VersionCode •Debugger can be attached •Easy and quick
  69. None
  70. Drag and drop .aab file

  71. Download URL

  72. None
  73. None
  74. None
  75. List of installed modules

  76. deferredInstall deferredUninstall Run immediately

  77. Internal App Sharing •Triple-T/gradle-play-publisher ‣ https://github.com/Triple-T/gradle-play-publisher ‣ Upload and launch

    to Play Store by gradle command
  78. FakeSplitInstallManager •Offline testing of module installation •Use FakeSplitInstallManager instead of

    SplitInstallManager •https://link.medium.com/J0eOwvXds4
  79. Navigation Component Dynamic Feature Module

  80. Navigation Component DFM •Implement screen transitions effortlessly ‣ It also

    installs the module •Customizable •Still alpha version only •http://goo.gle/dynamic-feature-nav
  81. dependencies { implementation "androidx.navigation:navigation-dynamic-features-fragment:2.3.0-alpha01" }

  82. <!-- navigation/nav_graph.xml --> <navigation> <fragment> <action android:id="@+id/action_on_demand" app:destination="@id/on_demand_activity" /> </fragment>

    <activity android:id="@+id/on_demand_activity" android:name="com.example.feature.SampleActivity" app:moduleName="feature" /> </navigation>
  83. <!-- navigation/nav_graph.xml --> <navigation> <fragment> <action android:id="@+id/action_on_demand" app:destination="@id/on_demand_activity" /> </fragment>

    <activity android:id="@+id/on_demand_activity" android:name="com.example.feature.SampleActivity" app:moduleName="feature" /> </navigation>
  84. <androidx.fragment.app.FragmentContainerView android:id="@+id/container" android:name="androidx.navigation.dynamicfeatures.fragment.DynamicNavHostFragment" android:layout_width="0dp" android:layout_height="0dp" app:defaultNavHost="true" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent"

    app:navGraph="@navigation/nav_graph" />
  85. <androidx.fragment.app.FragmentContainerView android:id="@+id/container" android:name="androidx.navigation.dynamicfeatures.fragment.DynamicNavHostFragment" android:layout_width="0dp" android:layout_height="0dp" app:defaultNavHost="true" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent"

    app:navGraph="@navigation/nav_graph" />
  86. findNavController().navigate(R.id.action_on_demand)

  87. None
  88. Customize installation screen

  89. class DynamicProgressFragment : AbstractProgressFragment() { override fun onCancelled() { }

    override fun onFailed(errorCode: Int) { } override fun onProgress(status: Int, bytesDownloaded: Long, bytesTotal: Long) { } }
  90. findNavController().apply { val fragmentNavigator = navigatorProvider[DynamicFragmentNavigator::class] val graphNavigator = navigatorProvider[DynamicGraphNavigator::class]

    graphNavigator.installDefaultProgressDestination { fragmentNavigator.createDestination().apply { className = DynamicProgressFragment::class.java.name id = R.id.dfn_progress_fragment } } navigate(R.id.action_on_demand) }
  91. Customize installation flow

  92. val installMonitor = DynamicInstallMonitor() findNavController().navigate( R.id.action_on_demand, null, null, DynamicExtras.Builder().setInstallMonitor(installMonitor).build() )

  93. val installMonitor = DynamicInstallMonitor() findNavController().navigate( R.id.action_on_demand, null, null, DynamicExtras.Builder().setInstallMonitor(installMonitor).build() )

  94. if (installMonitor.isInstallRequired) { installMonitor.status.observe( lifecycleOwner, object: Observer<SplitInstallSessionState> { override fun

    onChanged(sessionState: SplitInstallSessionState) { when (sessionState.status()) { SplitInstallSessionStatus.INSTALLED -> { findNavController().navigate(R.id.action_on_demand) } // ... } if (sessionState.hasTerminalStatus()) { installMonitor.status.removeObserver(this) } } } ) }
  95. if (installMonitor.isInstallRequired) { installMonitor.status.observe( lifecycleOwner, object: Observer<SplitInstallSessionState> { override fun

    onChanged(sessionState: SplitInstallSessionState) { when (sessionState.status()) { SplitInstallSessionStatus.INSTALLED -> { findNavController().navigate(R.id.action_on_demand) } // ... } if (sessionState.hasTerminalStatus()) { installMonitor.status.removeObserver(this) } } } ) } ture if need to install module
  96. if (installMonitor.isInstallRequired) { installMonitor.status.observe( lifecycleOwner, object: Observer<SplitInstallSessionState> { override fun

    onChanged(sessionState: SplitInstallSessionState) { when (sessionState.status()) { SplitInstallSessionStatus.INSTALLED -> { findNavController().navigate(R.id.action_on_demand) } // ... } if (sessionState.hasTerminalStatus()) { installMonitor.status.removeObserver(this) } } } ) } Observe installation state LiveData
  97. if (installMonitor.isInstallRequired) { installMonitor.status.observe( lifecycleOwner, object: Observer<SplitInstallSessionState> { override fun

    onChanged(sessionState: SplitInstallSessionState) { when (sessionState.status()) { SplitInstallSessionStatus.INSTALLED -> { findNavController().navigate(R.id.action_on_demand) } // ... } if (sessionState.hasTerminalStatus()) { installMonitor.status.removeObserver(this) } } } ) } Navigate when installed
  98. if (installMonitor.isInstallRequired) { installMonitor.status.observe( lifecycleOwner, object: Observer<SplitInstallSessionState> { override fun

    onChanged(sessionState: SplitInstallSessionState) { when (sessionState.status()) { SplitInstallSessionStatus.INSTALLED -> { findNavController().navigate(R.id.action_on_demand) } // ... } if (sessionState.hasTerminalStatus()) { installMonitor.status.removeObserver(this) } } } ) } Remove observer when installation completes
  99. Development Tips

  100. Screen navigation •app module cannot directly refer to classes in

    dynamic feature modules •Needs use reflection ‣ Be cautious when using Proguard / R8 •Navigation Component helps
  101. Specify module name •Only the module name is needed •For

    example: features:sample ‣ Specify only "sample" val request = SplitInstallRequest.newBuilder() .addModule("sample").build()
  102. R classes •R classes are not merged, so packages are

    different •Be careful of the packages when importing R classes
  103. <!-- app/string.xml --> <resources> <string name="base_string">Base</string> </resources> app com.example.app feature

    com.example.app.feature <!-- feature/string.xml --> <resources> <string name="dynamic_string">Dynamic</string> </resources> com.example.app.feature.R com.example.app.R
  104. import com.example.app.feature.R val dynamic = getString(R.string.dynamic_string) val base = getString(R.string.base_string)

  105. import com.example.app.feature.R val dynamic = getString(R.string.dynamic_string) val base = getString(R.string.base_string)

    ❌ Unresolved reference
  106. import com.example.app.feature.R val dynamic = getString(R.string.dynamic_string) val base = getString(R.string.base_string)

    import com.example.app.feature.R val dynamic = getString(R.string.dynamic_string) val base = getString(com.example.app.R.string.base_string) ⭕ OK
  107. // in app module typealias appString = com.example.app.R.string // in

    feature module import com.example.app.feature.R import com.example.app.appString val dynamic = getString(R.string.dynamic_string) val base = getString(appString.base_string)
  108. Conclusion

  109. Conclusion •Dynamic Feature Module is not so hard •Useful tools

    for Testing/Debugging are available •Dynamic Navigation Component is so useful •There are still bugs that you may encounter
  110. Let's try Dynamic Feature Module

  111. Thank You!