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

    Talk about •What's Dynamic Feature Module •How to create Dynamic

    Feature Module •How to test Dynamic Feature Module •etc...
  2. 5.

    Dynamic Feature Module •Dynamic delivery of modules using App Bundle

    •Modules can be installed and removed later •Can reduce app size
  3. 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
  4. 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
  5. 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...
  6. 11.
  7. 12.
  8. 13.
  9. 14.
  10. 15.
  11. 16.
  12. 18.

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

    21 targetSdkVersion 29 } } dependencies { implementation project(':app') }
  13. 25.

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

    ":feature_two"] implementation project(':app') implementation project(':app')
  14. 30.

    // in dynamic feature module class FeatureActivity : AppCompatActivity() {

    override fun attachBaseContext(base: Context) { super.attachBaseContext(base) SplitCompat.installActivity(this) } // ... }
  15. 31.

    // in app module class MainActivity : AppCompatActivity() { override

    fun attachBaseContext(base: Context) { super.attachBaseContext(base) SplitCompat.installActivity(this) } // ... }
  16. 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 */ }
  17. 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
  18. 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
  19. 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
  20. 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
  21. 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) }
  22. 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)
  23. 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) }
  24. 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) }
  25. 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 } } }
  26. 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 ) } } }
  27. 47.
  28. 55.

    private val listener = SplitInstallStateUpdatedListener( onRequiresConfirmation = { state ->

    // ... }, onInstalled = { state -> // ... } ) Observe installation state
  29. 56.

    lifecycleScope.launch { manager.requestProgressFlow().collect { state -> when (state.status()) { SplitInstallSessionStatus.INSTALLED

    -> { Timber.d("SplitInstallSessionStatus.INSTALLED") } // ... } } } Observe installation state using Flow
  30. 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
  31. 59.
  32. 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>
  33. 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
  34. 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
  35. 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
  36. 68.

    Internal App Sharing •Can upload debug build aab ‣ Debug

    signing, any VersionCode •Debugger can be attached •Easy and quick
  37. 69.
  38. 72.
  39. 73.
  40. 74.
  41. 80.

    Navigation Component DFM •Implement screen transitions effortlessly ‣ It also

    installs the module •Customizable •Still alpha version only •http://goo.gle/dynamic-feature-nav
  42. 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>
  43. 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>
  44. 87.
  45. 89.

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

    override fun onFailed(errorCode: Int) { } override fun onProgress(status: Int, bytesDownloaded: Long, bytesTotal: Long) { } }
  46. 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) }
  47. 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) } } } ) }
  48. 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
  49. 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
  50. 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
  51. 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
  52. 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
  53. 101.

    Specify module name •Only the module name is needed •For

    example: features:sample ‣ Specify only "sample" val request = SplitInstallRequest.newBuilder() .addModule("sample").build()
  54. 102.

    R classes •R classes are not merged, so packages are

    different •Be careful of the packages when importing R classes
  55. 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
  56. 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
  57. 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)
  58. 108.
  59. 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
  60. 111.