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

Une application, plusieurs apk : pourquoi faire, et comment ?

Une application, plusieurs apk : pourquoi faire, et comment ?

Présentation jouée lors du PAUG le 23/10/18

Rémi Pradal

October 23, 2018
Tweet

More Decks by Rémi Pradal

Other Decks in Programming

Transcript

  1. Une application, plusieurs apk : pourquoi faire, et comment ?

    PAUG 23/10/18 Toki Raoseta Rémi Pradal
  2. Ce qu’on va voir pendant ce talk 1. Présentation du

    projet et de ses problématiques 2. Quels outils pour une archi multi apk ? 3. Notre avis après deux ans 2
  3. Le Projet COSMO COntrôle et Service en MObilité • 40

    personnes (10 développeurs front) • Des développements depuis deux ans 4
  4. L’enjeux structurant Feature 1 Feature 2 Feature 3 Feature 4

    app.apk Feature 1 Feature 2 Feature 3 Feature 4 Feature 5 6
  5. La proposition Feature 1 Feature 2 subapp1.apk Feature 3 Feature

    4 subapp2.apk Visible comme une unique application par les utilisateurs Feature 3 Feature 4 Feature 5 7
  6. Déployer plusieurs apk ? Inenvisageable de demander aux utilisateurs de

    s’assurer eux même que l’ensemble des apk nécessaire sont installés sur leur téléphone 9
  7. MAM, MDM et stores publics - Accessible par n’importe qui

    - Installations et mises à jour seulement sur action de l’utilisateur - Store privé - Gestion de droits d’accès pour certaines app - Idem MAM - Possible de forcer l’installation et les MaJ des applications - Contrôle complet de l’appareil Store public Mobile Application Manager Mobile Device Manager 10
  8. L’architecture COSMO • 16 modules Gradle • 5 apk générés

    • Une interprétation de la clean architecture 11 Quelques caractéristiques de l’archi réellement en place
  9. L’architecture COSMO Dans le cadre de ce talk, on se

    limite à 3 apps, 4 modules Gradle: 12 app-main app-control app-regul
  10. Navigation transparente grace aux uri schemes app-main app-regul app-control Dispose

    d’une icone dans le launcher Android Pas d’icone dans le launcher Pas d’icone dans le launcher Peut être lancée via une uri scheme cosmo://regul Peut être lancée via une uri scheme cosmo://control 13
  11. Navigation transparente grace aux uri schemes <activity android:name=".ControlActivity"> <intent-filter> <action

    android:name="android.intent.action.VIEW" /> <category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.BROWSABLE" /> <data android:scheme="cosmo" android:host="control" /> </intent-filter> </activity> 14
  12. Navigation transparente grace aux uri schemes Les différentes apps sont

    responsables de leur redirection interne Activity landpage Activity choix du train Activity contrôle de billet app-main app-control cosmo:// control Redirection transparente en fonction du contexte 15
  13. Harmoniser les dépendances : Gradle // build.gradle Cosmo retrofitVersion =

    '2.1.0' retrofit = "com.squareup.retrofit2:retrofit:$retrofitVersion" retrofitConvertJackson = "com.squareup.retrofit2:converter-jackson:$retrofitVersion" //build.gradle app-main api parent.ext.retrofit api parent.ext.retrofitConvertJackson 16
  14. Mutualiser des sources : common . ├── layout │ ├──

    item_course.xml │ ├── view_course_search_loading.xml ├── values │ ├── applications-execution-context.xml │ ├── attrs.xml │ ├── colors.xml │ ├── config-values.xml │ ├── dimens.xml │ ├── integers.xml │ ├── strings.xml │ ├── styles.xml └── xml └── filepaths.xml . ├── AbstractBaseActivity.kt ├── analytics │ └── AnalyticsHelper.kt ├── network │ └── AddAppVersionHeaderInterceptor.kt ├── provider │ └── BaseContentProvider.kt ├── utils │ ├── AmountExtensions.kt │ ├── NetworkStatusMonitor.kt │ └── StringSanitizer.kt └── views ├── AutoCompleteActionTextView.kt ├── ClosableImeAutoCompleteTextView.kt └── MinDurationProgressBar.kt 17
  15. Mutualiser des sources : common • Éviter de dupliquer du

    code • Risque d’embarquer du code inutile dependencies { implementation project(':common') } 18
  16. Mutualiser des sources : common Optimize Shrink Obfuscate ... buildTypes

    { release { minifyEnabled true shrinkResources true proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } } 19
  17. Mutualiser des ressources : Gradle // permissions.gradle android { defaultConfig

    { def final stationChangePermission = "cosmo.permission.CHANGE_STATION" resValue "string", "station_change_permission", stationChangePermission manifestPlaceholders = manifestPlaceholders + [stationChangePermission: stationChangePermission] } } // build.gradle app-main apply from: '../cosmo-gradle/permissions.gradle' • <permission android:name="${stationChangePermission}" /> • context.getString(R.string.station_change_permission) 20
  18. les tâches personnalisées task cosmoMinimalTest( dependsOn: [ 'app-main:testDevDebugUnitTest', 'app-control:testDevDebugUnitTest', 'app-regul:testDevDebugUnitTest',

    'app-common-check:test', ] ) task cosmoCheckCodeQuality( dependsOn: [ 'cosmoMinimalTest', 'cosmoCodeCoverage', 'cosmoCheckStyle' ] ) 21
  19. partager des données : Content provider IStationRepository MainActivity ControlActivity ControlStationRepository

    MainStationRepository StationContentProvider 23 app-common app-main app-control
  20. partager des données : Content provider // Androidmanifest.xml app-main <provider

    android:name=".provider.station.StationContentProvider" android:authorities="@string/station_content_provider_authority" android:exported="true" android:permission="${contentSharePermission}"/> 24
  21. Ecouter/Propager une information : Broadcast • Changement de l’état du

    réseau • Exemple de la réception de notification FCM app-main Push notification FCM app-regul app-control ... Contenu notif broadcasté à toutes les apps 25
  22. // app-main class CosmoFirebaseMessagingService : FirebaseMessagingService() { override fun onMessageReceived(remoteMessage:

    RemoteMessage) { val messsageData = remoteMessage.data val type = messsageData[NOTIFICATION_TYPE] val body = messsageData[NOTIFICATION_BODY] val intent = buildBroadcastIntent(type, body) val permission = getString(R.string.notification_permission) sendBroadcast(intent, permission) } } 26
  23. // AndroidManifest.xml app-control <uses-permission android:name="${notificationPermission}" /> // app-control class ControlMessageReceiver

    : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { handleIntent(intent) } } <receiver android:name=".message.receiver.ControlMessageReceiver"> <intent-filter> <action android:name="${notificationAction}" /> </intent-filter> </receiver> 27
  24. Synchroniser un état : OrderedBroadcast app-main app-regul app-control Acknowledgement Acknowledgement

    Background work Disconnect action Disconnect action Background work Disconnect action 28
  25. Nos points d’attention techniques • ContentProvider Beaucoup de boilerplate (Cursor…)

    • Message broadcast Attention à la priority dans la cas où de nombreux messages sont envoyés • Versionning des interactions entre les apps Dans le cas où des vieilles applications peuvent cohabiter : mettre en place un versionning sur les uri des ContentProvider, les Uri Scheme etc... 30
  26. Nos points d’attention Fonctionnels • Réfléchir bien en amont au

    découpage de l’application : déplacer une feature d’un apk à un autre est très compliqué, minimiser les interactions entre chaque application • Sensibiliser l’ensemble de l’équipe (pas seulement les devs) au découpage choisi pour éviter les incompréhensions 31
  27. Promesse remplie ? Impact sur le travail des testeurs Nécessité

    de faire des tests de compatibilité entre les différentes versions. Certains scopes fonctionnels sont bel et bien isolés et n’ont pas besoin d’être testés à chaque livraison 32
  28. Conclusion & Takeaways Techniquement réalisable grâce aux fonctionnalités de “partage

    inter-app” Content Provider Message broadcast Uri scheme Entraîne des lourdeurs d’implémentation et méthodologiques 33
  29. Des questions ? Toki RAOSETA Oui.sncf Rémi Pradal Octo Technology

    @RemiPradal https://github.com/rpradal 34
  30. // app-main context.sendOrderedBroadcast(broadcastIntent /* intent */, broadcastPermission /* receiverPermission */,

    receiver /* resultReceiver */, handler /* scheduler */, 0 /* initialCode */, null /* initialData*/, null /* initialExtras */) handlerThread = HandlerThread(THREAD_NAME, Process.THREAD_PRIORITY_BACKGROUND).apply { start() handler = Handler(looper) } 36
  31. // app-main context.sendOrderedBroadcast(broadcastIntent /* intent */, broadcastPermission /* receiverPermission */,

    receiver /* resultReceiver */, handler /* scheduler */, 0 /* initialCode */, null /* initialData*/, null /* initialExtras */) val broadcastIntent = Intent().apply { action = broadcastAction putExtra(FINISHED_INTENT_EXTRA, buildFinishedAction(broadcastAction)) putExtra(FAILED_INTENT_ACTION_EXTRA, buildFailedAction(broadcastAction)) putExtra(BROADCAST_PERMISSION_EXTRA, broadcastPermission) } 37
  32. // DisconnectBroadcastReceiver app-regul override fun onReceive(context: Context, intent: Intent) {

    performTask(context, intent) } // DisconnectBroadcastReceiver app-regul override fun onReceive(context: Context, intent: Intent) { registerConsumerReceiver() performBackgroundTask(context, intent) } 40
  33. // DisconnectBroadcastReceiver app-regul override fun onReceive(context: Context, intent: Intent) {

    registerConsumerReceiver() performBackgroundTask(context, intent) } private fun registerConsumerReceiver() { val resultExtras = getResultExtras(true /* makeMap */) insertConsumerReceiverList(resultExtras, buildReceiverName()) setResultExtras(resultExtras) } fun insertConsumerReceiverList(bundle: Bundle, consumerReceiver: String) { val initialList = bundle.getStringArrayList(CONSUMER_RECEIVER_LIST_EXTRA) ?: arrayListOf<String>() initialList.add(consumerReceiver) bundle.putStringArrayList(CONSUMER_RECEIVER_LIST_EXTRA, initialList) } 41
  34. // app-main context.sendOrderedBroadcast(broadcastIntent /* intent */, broadcastPermission /* receiverPermission */,

    receiver /* resultReceiver */, handler /* scheduler */, 0 /* initialCode */, null /* initialData*/, null /* initialExtras */) val receiver = object : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { val consumerReceiverList = extractConsumerReceiverList(getResultExtras( true /* makeMap */)) consumerReceiverList?.let { // save it.toSet() checkRemainingConsumers() } } } 43
  35. // app-regul val intent = Intent().apply { action = FINISHED_INTENT_ACTION_EXTRA

    /* FAILED_INTENT_ACTION_EXTRA */ putExtra(CONSUMER_RECEIVER_EXTRA, buildReceiverName()) } context.sendBroadcast(intent, BROADCAST_PERMISSION_EXTRA) // app-main val finishedReceiver = object : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { val consumerReceiverList = extractConsumerReceiverList(getResultExtras(true /* makeMap */)) consumerReceiverList.toSet().firstOrNull()?.let { consumer -> finishedConsumerSet.add(consumer) checkRemainingConsumers() } } } 45
  36. private fun checkRemainingConsumers() { when { } } consumerSet.isEmpty() ||

    finishedConsumerSet.containsAll(consumerSet) -> { onTaskFinished() } // at least one consumer has failed !Collections.disjoint(consumerSet, failedConsumerSet) -> { onTaskFailed() } 46