Upgrade to PRO for Only $50/Year—Limited-Time Offer! 🔥

Pushing Dynamic Features your Users want as qui...

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?

Raul Hernandez Lopez

September 20, 2019
Tweet

More Decks by Raul Hernandez Lopez

Other Decks in Technology

Transcript

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

    mobile applications with Real-Time Communications (RTC) capabilities via simple APIs.
  2. Google advises that for every increase of 3MB in install

    size there is a 0.5% install failure rate.
  3. 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
  4. Lollipop? :app:twitter (Base APK) Yes x86_64 lib_core ... assets... hdpi

    WebRTC dynamic module Play Store 2. Request to install a dynamic feature
  5. 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
  6. Generic, to handle future modules as well onResume onPause Generic

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

    register Play Core Library Generic Implementation ActivityTracker Install Manager
  8. 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
  9. subscribes observes Request Installation / Loading Get Install Manager Event

    Specific Implementation Install Manager Functionality waiting for Loader Easy to extend
  10. - 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...
  11. [ 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
  12. 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>
  13. 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)) }
  14. 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)) }
  15. 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)) } }
  16. override fun load( appContext: Context, config: DynamicFeatureConfig ) { if

    (config.binaryName.isNotEmpty()) { SplitInstallHelper .loadLibrary(appContext, config.binaryName) } }
  17. 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)) } }
  18. 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 } }
  19. 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 } }
  20. 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) } … }
  21. sealed class DynamicDeliveryInstallEvent(open val moduleName: String) { … data class

    Progress( override val moduleName: String, val progress: Float ) : DynamicDeliveryInstallManagerEvent(moduleName) … }
  22. 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) … }
  23. 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
  24. override fun logEvent(event: DynamicDeliveryInstallManagerEvent) { when (event) { … is

    InstallComplete -> { scribeHelper .scribeDynamicDeliveryInstallSuccess(createItemProvider()) } … } Analytics