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

Ae9f854b103d510aebe4975a6f6fe514?s=128

Rémi Pradal

October 23, 2018
Tweet

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. plusieurs apk pour une app ? wtf!!! 3

  4. Le Projet COSMO COntrôle et Service en MObilité • 40

    personnes (10 développeurs front) • Des développements depuis deux ans 4
  5. L’enjeux structurant Comment être capable de déployer certaines fonctionnalités sans

    avoir à tester l’ensemble du scope fonctionnel ? 5
  6. L’enjeux structurant Feature 1 Feature 2 Feature 3 Feature 4

    app.apk Feature 1 Feature 2 Feature 3 Feature 4 Feature 5 6
  7. 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
  8. Une appli multi apk : comment faire ? 8

  9. 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
  10. 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
  11. 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
  12. L’architecture COSMO Dans le cadre de ce talk, on se

    limite à 3 apps, 4 modules Gradle: 12 app-main app-control app-regul
  13. 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
  14. 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
  15. 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
  16. 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
  17. 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
  18. Mutualiser des sources : common • Éviter de dupliquer du

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

    { release { minifyEnabled true shrinkResources true proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } } 19
  20. 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
  21. 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
  22. Déployer uniquement les changements : Manifeste // continuous-deployment.conf [...] 47:

    app-regul 48: app-control, app-main 22
  23. partager des données : Content provider IStationRepository MainActivity ControlActivity ControlStationRepository

    MainStationRepository StationContentProvider 23 app-common app-main app-control
  24. 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
  25. 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
  26. // 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
  27. // 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
  28. Synchroniser un état : OrderedBroadcast app-main app-regul app-control Acknowledgement Acknowledgement

    Background work Disconnect action Disconnect action Background work Disconnect action 28
  29. Le bilan 29

  30. 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
  31. 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
  32. 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
  33. 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
  34. Des questions ? Toki RAOSETA Oui.sncf Rémi Pradal Octo Technology

    @RemiPradal https://github.com/rpradal 34
  35. Annexe: OrderedBroadcast en détail 35

  36. // 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
  37. // 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
  38. // AndroidManifest app-regul <uses-permission android:name="${disconnectPermission}" /> <receiver android:name=".disconnect.DisconnectBroadcastReceiver"> <intent-filter> <action

    android:name="${disconnectAction}" /> </intent-filter> </receiver> 38
  39. Synchroniser un état : Orderedroadcast app-main app-regul app-control Disconnect action

    39
  40. // 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
  41. // 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
  42. Synchroniser un état : Orderedroadcast app-main app-regul app-control Background work

    Disconnect action Background work Disconnect action 42
  43. // 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
  44. Synchroniser un état : Orderedroadcast app-main app-regul app-control Acknowledgement 44

  45. // 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
  46. private fun checkRemainingConsumers() { when { } } consumerSet.isEmpty() ||

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