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

Pushing Dynamic Features your Users want as quick as they want them

Pushing Dynamic Features your Users want as quick as they want them

Many successful Android apps offer multiple features and components that work together to provide a great user experience. Although you want all prospective users to have the ability to install your app, this may not be possible due to limitations in storage and network connection, especially in emerging markets. What if you can deliver new features selectively post-installation, reducing the initial app size, and allowing you to target a wider audience? How did Kotlin enhance your reliability and quick prototyping before shipping a brand new dynamic feature? Now that Kotlin is the first language on the Android platform, you certainly know this was the right choice at the time.

What will the audience learn from this talk?
The good, the bad and lessons learnt we had along the way when building our first Dynamic Feature at Twitter for Android. Technical Design decisions we made, some architecture and implementation details (code samples in Kotlin language) and thinking in behind them. What is a dynamic feature? Why did we choose to make it? How did we make it? What difficulties did we have? What are the next steps?

Ffc500baeba9a1024e2c8273203c9f90?s=128

Raul Hernandez Lopez

September 20, 2019
Tweet

Transcript

  1. Pushing Dynamic Features Your Users Want, As Quick As They

    Want Them
  2. @raulhernandezl Raul Hernandez Lopez Software Engineer @ Twitter raulh82vlc

  3. What?

  4. None
  5. Why?

  6. Broadcasting with Guests “GO LIVE Together”

  7. Guests Call-In “GO LIVE Together”

  8. (...don’t want this to change) Twitter has 139M mDAU

  9. Twitter Android App - 26.84MB

  10. WebRTC library

  11. WebRTC library WebRTC is a free, open project that provides

    mobile applications with Real-Time Communications (RTC) capabilities via simple APIs.
  12. WebRTC library Communications Optimisation

  13. WebRTC library Communications Optimisation https://webrtc.org/

  14. WebRTC library adds 5-7 MB

  15. WebRTC library adds 5-7 MB 18.63 %

  16. Google advises that for every increase of 3MB in install

    size there is a 0.5% install failure rate.
  17. Emerging markets

  18. Need a reliable solution

  19. On-demand features?

  20. A/B testing

  21. How?

  22. Modularization

  23. Designing separate sections

  24. :feature-1 :app

  25. :feature-1 :app :feature-2 :feature-n

  26. Easier Code reuse

  27. Scale across teams

  28. Build time reduction

  29. None
  30. Current App modularization lib-webrtc periscope:lib features:lib :app:twitter

  31. App modularization lib-webrtc periscope:lib features:lib :app:twitter exclude 'lib/**/libjingle_peerconnection_so.so'

  32. App modularization lib-webrtc periscope:lib features:lib :app:twitter webrtc feature module exclude

    'lib/**/libjingle_peerconnection_so.so'
  33. MUST split feature into a separate module webrtc feature module

  34. Steps to start

  35. 1. Google Play Store handling Play Store WebRTC dynamic module

  36. Lollipop? :app:twitter (Base APK) hdpi & arm64 xhdpi & x86_64

    xhdpi & x86 lib_core ... No WebRTC dynamic module Play Store 2. Request to install a dynamic feature
  37. Lollipop? :app:twitter (Base APK) Yes x86_64 lib_core ... assets... hdpi

    WebRTC dynamic module Play Store 2. Request to install a dynamic feature
  38. Lollipop? :app:twitter (Base APK) Yes x86_64 lib_core ... assets... hdpi

    hdpi & arm64 xhdpi & x86_64 xhdpi & x86 lib_core ... No WebRTC dynamic module Play Store 2. Request to install a dynamic feature
  39. 3. Handle errors DOWNLOAD NETWORK, PERMISSIONS

  40. 3. Handle errors INSTALLATION INSUFFICIENT STORAGE

  41. 4. Handle success DOWNLOAD PROGRESS COMPLETION

  42. 4. Handle success INSTALLATION LOADING

  43. When? Initiate install only when feature is needed

  44. When? Initiate install only when feature is needed or Defer

    install when convenient
  45. Designs principles

  46. Managing activity states ActivityTracker

  47. Generic, to handle future modules as well onResume onPause Generic

    Implementation ActivityTracker Install Manager
  48. Integrates well with Google libraries onResume onPause unregister Split InstallManager

    register Play Core Library Generic Implementation ActivityTracker Install Manager
  49. Functionality waiting for subscribes observes RxJava streams Specific Implementation Integrates

    well with popular libraries onResume onPause unregister Split InstallManager register Play Core Library Generic Implementation ActivityTracker Install Manager
  50. subscribes observes Request Installation / Loading Get Install Manager Event

    Specific Implementation Install Manager Functionality waiting for Loader Easy to extend
  51. - NoDownloadInProgress - DownloadInProgress - Downloaded - Complete subscribes observes

    Request Installation / Loading Get Install Manager Event Specific Implementation Current State Install Manager Functionality waiting for Loader …and extend...
  52. [ 0 - 1.0 ] - NoDownloadInProgress - DownloadInProgress -

    Downloaded - Complete subscribes observes Request Installation / Loading Get Install Manager Event Specific Implementation Progress Current State Install Manager Functionality waiting for Loader …and extend... even… more
  53. Other teams friendly Dynamic features on Twitter for Android!

  54. Implementation

  55. None
  56. data classes

  57. sealed classes data classes

  58. sealed classes nested sealed classes data classes

  59. sealed classes smart casting nested sealed classes data classes

  60. sealed classes data classes smart casting nested sealed classes object

  61. build.gradle ... dynamicFeatures = ':lib:twitter:features:dynamic-features' Base app

  62. build.gradle ... apply plugin: 'com.android.dynamic-feature' Dynamic feature

  63. AndroidManifest.xml <manifest package="com.twitter.webrtcnative" ... <dist:module dist:instant="false" dist:onDemand="true" dist:title="@string/dynamic_feature_module_name"> <dist:fusing dist:include="true"/>

    </dist:module> <application android:hasCode="false" tools:ignore="AllowBackup,GoogleAppIndexingWarning,MissingApplicationIcon"> </application> </manifest>
  64. interface DynamicDeliveryInstallManager { fun installDynamicModule(moduleName: String) fun isDynamicModuleInstalled(moduleName: String): Boolean

    fun loadDynamicModule(moduleName: String) ... }
  65. interface DynamicDeliveryInstallManager { ... fun register(moduleName: String) fun unregister(moduleName: String)

    fun isRegistered(moduleName: String): Boolean ... }
  66. interface DynamicDeliveryInstallManager { ... fun observeDynamicModuleState(moduleName: String): Observable<DynamicDeliveryInstallManagerEvent> }

  67. override fun installDynamicModule(moduleName: String) { if (isDynamicModuleInstalled(moduleName)) { loadDynamicModule(moduleName) return

    } val request = SplitInstallRequest.newBuilder() .addModule(moduleName) .build() manager.startInstall(request) eventPublishSubject.onNext(DownloadStart(moduleName)) }
  68. override fun installDynamicModule(moduleName: String) { if (isDynamicModuleInstalled(moduleName)) { loadDynamicModule(moduleName) return

    } val request = SplitInstallRequest.newBuilder() .addModule(moduleName) .build() manager.startInstall(request) eventPublishSubject.onNext(DownloadStart(moduleName)) }
  69. override fun loadDynamicModule(moduleName: String) { val config = getConfigFromModuleName(moduleName) try

    { splitInstallDelegate.load(appContext, config) eventPublishSubject .onNext(LoadComplete(moduleName)) loadedModuleNames.add(moduleName) } catch (error: Error) { eventPublishSubject .onNext(Error.LoadError(moduleName, error)) } }
  70. override fun load( appContext: Context, config: DynamicFeatureConfig ) { if

    (config.binaryName.isNotEmpty()) { SplitInstallHelper .loadLibrary(appContext, config.binaryName) } }
  71. override fun loadDynamicModule(moduleName: String) { val config = getConfigFromModuleName(moduleName) try

    { splitInstallDelegate.load(appContext, config) eventPublishSubject .onNext(LoadComplete(moduleName)) loadedModuleNames.add(moduleName) } catch (error: Error) { eventPublishSubject .onNext(Error.LoadError(moduleName, error)) } }
  72. override fun observeDynamicModuleState(moduleName: String): Observable<DynamicDeliveryInstallManagerEvent> { if (loadedModuleNames.contains(moduleName)) { return

    Observable.just( DynamicDeliveryInstallManagerEvent.LoadComplete(moduleName)) } return eventPublishSubject .onErrorReturn { Error.UnknownError(moduleName, it) } .filter { it.moduleName == moduleName } .takeUntil { it is DynamicDeliveryInstallManagerEvent.LoadComplete } }
  73. override fun observeDynamicModuleState(moduleName: String): Observable<DynamicDeliveryInstallManagerEvent> { if (loadedModuleNames.contains(moduleName)) { return

    Observable.just( DynamicDeliveryInstallManagerEvent.LoadComplete(moduleName)) } return eventPublishSubject .onErrorReturn { Error.UnknownError(moduleName, it) } .filter { it.moduleName == moduleName } .takeUntil { it is DynamicDeliveryInstallManagerEvent.LoadComplete } }
  74. sealed class DynamicDeliveryInstallEvent(open val moduleName: String) { … sealed class

    Error(override val moduleName: String, open val throwable: Throwable) : DynamicDeliveryInstallManagerEvent(moduleName) { … data class LoadError(override val moduleName: String, override val throwable: Throwable) : Error(moduleName, throwable) } … }
  75. sealed class DynamicDeliveryInstallEvent(open val moduleName: String) { … data class

    Progress( override val moduleName: String, val progress: Float ) : DynamicDeliveryInstallManagerEvent(moduleName) … }
  76. sealed class DynamicDeliveryInstallEvent(open val moduleName: String) { … data class

    InstallComplete( override val moduleName: String ) : DynamicDeliveryInstallManagerEvent(moduleName) data class LoadComplete( override val moduleName: String ) : DynamicDeliveryInstallManagerEvent(moduleName) … }
  77. WEBRTC_MODULE_NAME = "webrtcnative"; WEBRTC_BINARY_NAME = "jingle_peerconnection_so"; @ApplicationScope @Provides @IntoMap @NotNull

    @DynamicDeliveryActivityKey(BroadcastFullscreenActivity.class) static List<String> provideBroadcastFullscreenActivityModules() { return ListBuilder.build( WEBRTC_MODULE_NAME ); } Dependency Injection
  78. override fun logEvent(event: DynamicDeliveryInstallManagerEvent) { when (event) { … is

    InstallComplete -> { scribeHelper .scribeDynamicDeliveryInstallSuccess(createItemProvider()) } … } Analytics
  79. Install only when the “GO LIVE Together” button is tapped

  80. Install only when the “GO LIVE Together” button is tapped

  81. Analytics

  82. Results

  83. No decline in user numbers

  84. Prevented 18.63% APK size increase (5 MB)

  85. “GO LIVE Together” on Twitter for Android

  86. Lessons learned

  87. Issues with Android’s beta features

  88. Gradle module issues

  89. KitKat / Older devices

  90. Collaboration

  91. Synchronisation issues

  92. Synchronisation issues

  93. Synchronisation issues

  94. Issues when developing

  95. Next Steps

  96. Defer installation

  97. Conditional installation

  98. Thank you.

  99. Questions? @raulhernandezl raulh82vlc