Permis de conduire une voiture ou pourquoi les applications doivent être à activité unique

image


À AppsConf 2018 , qui a eu lieu les 8 et 9 octobre, j'ai fait une présentation sur la création d'applications Android dans une seule activité. Bien que le sujet soit bien connu, il existe de nombreux préjugés concernant un tel choix - la salle bondée et le nombre de questions après le discours le confirment. Afin de ne pas attendre l'enregistrement vidéo, j'ai décidé de faire un article avec une transcription du discours.



Ce que je vais dire


  1. Pourquoi et pourquoi devrais-je passer à une seule activité
  2. Une approche universelle pour résoudre des tâches que vous avez l'habitude de résoudre sur plusieurs activités
  3. Exemples de tâches commerciales standard
  4. Goulots d'étranglement où le code est généralement soutenu au lieu de tout faire honnêtement

Pourquoi une activité unique est-elle appropriée?


Cycle de vie



Tous les développeurs Android connaissent le schéma de démarrage à froid de l'application. OnCreate est d'abord appelé sur la classe Application, puis le cycle de vie de la première activité entre en action.
S'il y a plusieurs activités dans notre application (et il y a la plupart de ces applications), les événements suivants se produisent:


App.onCreate() ActivityA.onCreate() ActivityA.onStart() ActivityA.onResume() ActivityA.onPause() ActivityB.onCreate() ActivityB.onStart() ActivityB.onResume() ActivityA.onStop() 

Il s'agit du journal de lancement abstrait de l'activité B de l'activité A. Une ligne vide est le moment où le lancement d'un nouvel écran a été appelé. À première vue, tout va bien. Mais si nous nous tournons vers la documentation, cela deviendra clair: pour garantir que l'écran est visible à l'utilisateur et qu'il puisse interagir avec lui, ce n'est possible qu'après avoir appelé onResume sur chaque écran:


 App.onCreate() ActivityA.onCreate() ActivityA.onStart() ActivityA.onResume() <-------- ActivityA.onPause() ActivityB.onCreate() ActivityB.onStart() ActivityB.onResume() <-------- ActivityA.onStop() 

Le problème est qu'un tel journal n'aide pas à comprendre le cycle de vie de l'application. Lorsque l'utilisateur est toujours à l'intérieur, et lorsqu'il a déjà basculé vers une autre application ou minimisé la nôtre et ainsi de suite. Et cela est nécessaire lorsque nous voulons lier la logique métier au LC de l'application, par exemple, conserver une connexion socket pendant que l'utilisateur est dans l'application et la fermer lors de la fermeture


Dans une application à activité unique, tout est simple - LC Activity devient une application LC. Tout ce dont vous avez besoin pour n'importe quelle logique est facile à lier à l'état de l'application.


Écrans de lancement


En tant qu'utilisateur, je suis souvent tombé sur le fait qu'un appel du répertoire téléphonique (et c'est clairement le lancement d'une activité distincte) ne se produit pas après avoir cliqué sur un contact. On ne sait pas à quoi cela est lié, mais ceux à qui j'ai essayé de passer sans succès ont dit avoir reçu l'appel et entendu le bruit des pas. Dans le même temps, mon smartphone est depuis longtemps dans ma poche.



Le problème est que le démarrage d'une activité est un processus complètement asynchrone! Il n'y a aucune garantie de démarrage instantané, et pire encore, nous ne pouvons pas contrôler le processus. Absolument.


Dans l'application Single-Activity, en collaboration avec le gestionnaire de fragments, nous pouvons contrôler le processus.
transaction.commit() - changera d'écran de manière asynchrone, ce qui vous permettra d'ouvrir ou de fermer plusieurs écrans à la fois.
transaction.commitNow() - bascule l'écran de manière synchrone, si vous n'avez pas besoin de l'ajouter à la pile.
fragmentManager.executePendingTransactions () `vous permet d'exécuter toutes les transactions précédemment lancées maintenant.


Analyse de la pile d'écran


Imaginez que la logique métier de votre application dépend de la profondeur actuelle de la pile d'écrans (par exemple, les restrictions d'imbrication). Ou, à la fin d'un processus, vous devez revenir à un certain écran, et s'il y en a plusieurs identiques, à celui le plus proche de la racine (le début de la chaîne).
Comment obtenir une pile d'activité? Quels paramètres faut-il spécifier lors du démarrage de l'écran?



Soit dit en passant, sur la magie des options de lancement d'activité:


  • vous pouvez spécifier des indicateurs de lancement dans Intent (et également les mélanger ensemble et les modifier à partir de différents endroits);
  • vous pouvez ajouter des paramètres de lancement dans le manifeste, car toutes les activités doivent y être décrites;
  • ajoutez ici des filtres d'intention pour gérer le déclenchement externe;
  • et enfin penser à MultiTasks, lorsque les activités peuvent s'exécuter dans différentes "tâches".

Ensemble, cela crée de la confusion et des problèmes avec le débogage du support. Vous ne pouvez jamais dire avec certitude exactement comment l'écran a été lancé et comment il a affecté la pile.


Dans une application à activité unique, tous les écrans passent uniquement par des transactions de fragments. Vous pouvez analyser la pile actuelle d'écrans et de transactions enregistrées.
Dans la démonstration de la bibliothèque Cicerone , vous pouvez voir comment l'état actuel de la pile est affiché dans la barre d'outils.


image


Remarque: dans la dernière version, les bibliothèques de support bloquaient l'accès au tableau de fragments à l'intérieur du gestionnaire de fragments, mais si vous le souhaitez vraiment, ce problème peut toujours être résolu.


Une seule activité à l'écran


Dans les applications réelles, nous devrons certainement combiner des écrans «logiques» en une seule activité, alors vous ne pouvez pas écrire une application réelle UNIQUEMENT sur l'activité. La dualité de l'approche est toujours mauvaise, car le même problème peut être résolu de différentes manières (quelque part, la disposition est directement dans l'activité, et quelque part, l'activité n'est qu'un conteneur).


Ne gardez pas les activités


Cet indicateur de test vous permet vraiment de trouver des bogues dans l'application, mais le comportement qu'il reproduit ne se produit JAMAIS en réalité! Il n’arrive pas que le processus de candidature soit maintenu et à ce moment l’activité, bien qu’elle ne soit pas active, meurt! Les activités ne peuvent mourir qu'avec le processus de demande. Si l'application est affichée à l'utilisateur et que le système ne dispose pas de suffisamment de ressources, tout autour de vous mourra (autres applications, services et même le lanceur inactifs), et votre application vivra jusqu'au bout, et si elle doit mourir, elle sera entièrement.
Vous pouvez vérifier.


Héritage


Historiquement, il y a une énorme quantité de logique inutile dans Activity qui n'est probablement pas utile pour vous. Par exemple, tout ce dont vous avez besoin pour travailler avec les loaders , l' actionBar , le action menu etc. Cela rend la classe elle-même assez massive et lourde.


Des animations


Peut-être que n'importe qui peut créer une animation de décalage simple lors du basculement entre les activités. Ici, il convient de préciser que vous devez effectuer une remise sur l'asynchronie du lancement de l'activité, dont nous avons parlé plus tôt.
Si vous avez besoin de quelque chose de plus intéressant, vous pouvez vous rappeler de tels exemples d'animations de transition qui sont faites sur Activity:


image


Mais il y a un gros problème: la personnalisation de cette animation est presque impossible. Il est peu probable que cela plaise aux concepteurs et au client.


Avec des fragments, tout est différent. Nous pouvons descendre directement au niveau de la hiérarchie de la vue et faire n'importe quelle animation que vous pouvez imaginer! Preuve directe ici :


image


Si vous regardez le code source, vous constaterez que cela se fait sur une mise en page régulière. Oui, le code est correct là-bas, mais l'animation est toujours assez difficile, et avoir une telle opportunité est toujours un plus. Si deux activités sont commutées, l'application n'a pas de conteneur commun où vous pouvez effectuer de telles transitions.


Changer la configuration à la volée


Ce point ne figurait pas dans mon discours, mais il est également très important. Si vous avez une fonctionnalité permettant de changer la langue à l'intérieur de l'application, alors avec plusieurs activités, il sera assez problématique de la mettre en œuvre, si, entre autres, vous n'avez pas besoin de redémarrer l'application, mais restez au même endroit où l'utilisateur se trouvait au moment d'appeler la fonctionnalité.


Dans une application à activité unique, il suffit de modifier les paramètres régionaux installés dans le contexte de l'application et d'appeler recreate() sur l'activité, le reste du système fera tout lui-même.


En fin de compte


Google dispose d'une solution de navigation, dont la documentation indique explicitement qu'il est conseillé d'écrire des applications à activité unique.


À ce stade, j'espère que vous ne doutez pas que l'approche classique avec plusieurs activités contient un certain nombre de lacunes, qu'il est habituel de fermer les yeux, se cachant derrière la tendance générale du mécontentement d'Android.


Si c'est le cas, alors pourquoi l'activité unique n'est-elle pas encore une norme de développement?


Ici, je vais citer mon bon ami:



Démarrage d'un nouveau projet sérieux, tout lead a peur de gaffer et évite les décisions risquées. C'est correct. Mais je vais essayer de fournir un plan complet pour la transition vers une activité unique.


Passer à une seule activité



Si vous étudiez cette application, vous pouvez déterminer à partir des animations et du comportement caractéristiques qu'elle est écrite sur plusieurs activités. Je peux me tromper, et tout a été fait même sur des vues personnalisées, mais cela n'affectera pas notre raisonnement.


Maintenant attention! Nous le faisons comme ceci:



Nous n'avons apporté que deux modifications: nous avons ajouté la classe AppActivity et remplacé toutes les activités par FlowFragment. Examinez chaque changement plus en détail.


De quoi AppActivity est responsable:


  • contient uniquement un conteneur pour les fragments
    • est le point d'initialisation des objets de l'interface utilisateur de l'étendue (cela se faisait auparavant dans Application, ce qui est faux, car, par exemple, les objets Service dans notre application n'ont certainement pas besoin de tels objets)
    • est un fournisseur d'applications
    • apporte tous les avantages de l'activité unique.

Qu'est-ce que FlowFragment :


  • fait exactement la même chose que l'activité, au lieu de laquelle il a été créé.

Nouvelle navigation


La principale différence avec l'ancienne approche est la navigation.



Auparavant, le développeur avait le choix: lancer une nouvelle activité ou une transaction fragmentaire dans l'actuelle. Le choix n'a pas disparu, mais les méthodes ont changé - nous devons maintenant décider si nous devons commencer la fragmentation du fragment dans AppActivity ou à l'intérieur du FlowFragment actuel.



De même avec le traitement du bouton Retour. Auparavant, l'activité transmettait l'événement au fragment actuel et, si elle ne le traitait pas, elle prenait la décision elle-même. Maintenant, AppActivity transmet l'événement au FlowFragment actuel, et qui, à son tour, le transmet au fragment actuel.


Transfert des résultats entre les écrans


Pour les développeurs inexpérimentés, la question du transfert de données entre les écrans est le principal problème de la nouvelle approche, car auparavant il était possible d'utiliser la fonctionnalité startActivityForResult ()!


Pas la première année, diverses approches architecturales de l'écriture d'applications ont été discutées. La tâche principale reste en même temps la séparation de l'interface utilisateur et de la couche de données et de la logique métier. De ce point de vue, startActivityForResult () rompt le canon, car les données entre les écrans d' une application sont transférées du côté entité de la couche d'interface utilisateur. J'insiste sur le fait qu'il ne s'agit que d' une seule application, car nous avons une couche de données commune, des modèles communs de portée mondiale, etc. Nous n'utilisons pas ces opportunités et nous nous enfonçons dans le cadre d'un bundle (sérialisation, taille, etc.).
Mon conseil : n'utilisez pas startActivityForResult () dans l'application! Utilisez-le uniquement aux fins prévues - pour exécuter des applications externes et en obtenir des résultats.


Comment alors lancer un écran avec un choix pour un autre écran? Il y a trois options:


  1. Targetfragment
  2. Eventbus
  3. modèle de jet

TargetFragment - une option "prête à l'emploi", mais le même transfert de données sur le côté de la couche d'interface utilisateur. Mauvaise option.


EventBus - si vous pouvez vous mettre d'accord sur une équipe et - plus important encore - contrôler les arrangements, vous pouvez alors implémenter le transfert de données entre les écrans sur le bus de données global. Mais comme il s'agit d'une décision dangereuse, la conclusion est une mauvaise option.


Modèle réactif - cette approche implique la présence de rappels et plus encore. La façon dont vous les mettez en œuvre est décidée par l'équipe de chaque projet. Mais cette approche est optimale, car elle permet de contrôler ce qui se passe et ne permet pas d'utiliser le code à d'autres fins. Notre choix!


Résumé


J'adore les nouvelles approches lorsqu'elles sont simples et présentent des avantages évidents. J'espère que c'est le cas dans ce cas. Les avantages sont décrits dans la première partie et vous devez juger de la difficulté. Il suffit de remplacer toutes les activités par FlowFragment, en gardant toute la logique inchangée. Modifiez un peu le code de navigation et pensez à travailler avec le transfert de données entre écrans, si ce n'est déjà fait.


Pour montrer la simplicité de l'approche, j'ai moi-même fait basculer l'application ouverte sur Single-Activity, et cela n'a pris que quelques heures (bien sûr, il vaut la peine de considérer que ce n'est pas un héritage ancien, et tout est plus ou moins bien avec l'architecture là-bas).


Que s'est-il passé?


Voyons comment résoudre des problèmes standard dans une nouvelle approche.


BottomNavigationBar et NavigationDrawer


En utilisant la règle simple que nous remplaçons toutes les activités par FlowFragment, le menu latéral sera maintenant dans un fragment et basculera les fragments imbriqués dedans:



Similaire à BottomNavigationBar.
Il est beaucoup plus intéressant que nous puissions investir du FlowFragment dans d'autres, car ce sont encore des fragments ordinaires!



Cette option se trouve dans GitFox .


C'est la possibilité de simplement combiner certains fragments au sein d'autres qui permet de créer une interface utilisateur dynamique pour différents appareils sans aucun problème: tablettes + smartphones.


Scopes DI


Si vous avez un flux d'achats de produits sur plusieurs écrans et que vous devez afficher le nom du produit sur chaque écran, vous l'avez probablement déjà mis dans une activité distincte qui stocke le produit et le fournit aux écrans.
Ce sera la même chose avec FlowFragment - il contiendra une échelle DI avec des modèles pour tous les écrans imbriqués. Cette approche élimine le contrôle compliqué de la durée de vie de l'oscilloscope en le liant à la durée de vie du FlowFragment.




Si vous avez utilisé des filtres dans le manifeste pour lancer un écran spécifique sur le lien profond, vous pourriez avoir des problèmes pour démarrer l'activité, dont j'ai parlé dans la première partie. Dans la nouvelle approche, tous les liens profonds tombent dans AppActivity.onNewIntent. De plus, selon les données obtenues, il y a une transition vers l'écran requis (ou une chaîne d'écrans. Je propose de regarder une telle fonctionnalité dans Chicheron ).



Mort de processus


Si l'application est écrite sur plusieurs activités, vous devez savoir que lorsque l'application se termine, puis lorsque le processus est restauré, l'utilisateur sera sur la dernière activité et toutes les précédentes ne seront restaurées que lorsqu'elles leur seront retournées.



Si vous n'en tenez pas compte à l'avance, des problèmes peuvent survenir. Par exemple, si la portée nécessaire sur la dernière activité ouverte sur la précédente, personne ne la recréera. Que faire Apportez-le à la classe Application? Est-ce que plusieurs points ouvrent des étendues?


Tout est plus simple avec les fragments, car ils sont à l'intérieur d'une activité ou d'un autre FlowFragment, et tout conteneur sera restauré AVANT de recréer le fragment.



Nous pouvons discuter d'autres tâches pratiques dans les commentaires, sinon il y a une chance que l'article se révèle trop volumineux.


Et maintenant la partie la plus intéressante.


Goulots d'étranglement (vous devez vous souvenir et réfléchir).


Voici les choses importantes que vous devriez penser dans n'importe quel projet, mais tout le monde est tellement habitué à les «déchirer» dans les projets de plusieurs activités qu'il vaut la peine de rappeler et de dire comment les résoudre correctement dans la nouvelle approche. Et le premier sur la liste


Rotation de l'écran


C'est l' histoire la plus terrible pour les fans qui pleurnichent qu'Android recrée Activity lorsque l'écran est tourné. La méthode de solution la plus populaire consiste à corriger l'orientation portrait. De plus, cette proposition n'est plus faite par les développeurs, mais par les managers effrayés par des phrases telles que " maintenir un virage est très difficile et coûte plusieurs fois plus cher ".
Nous ne discuterons pas du bien-fondé d'une telle décision. Une autre chose est importante: fixer la rotation ne dispense pas de la mort d'Activité! Étant donné que les mêmes processus se produisent avec de nombreux autres événements: mode partagé, lorsque plusieurs applications sont affichées à l'écran, connexion d'un moniteur externe, modification de la configuration de l'application à la volée, etc.


De plus, la rotation de l'écran vous permet de vérifier la bonne "élasticité" de la mise en page, donc dans notre équipe de Saint-Pétersbourg, nous ne désactivons pas la rotation dans tous les assemblages de vente, même si ce n'est pas dans la version finale. Sans oublier les bugs typiques qui seront toujours détectés lors de la vérification.


De nombreuses solutions ont déjà été écrites pour le tournage, à partir de Moxy et se terminant par diverses implémentations MVVM. Ne le rendez pas plus difficile qu'autre chose.


Prenons un autre cas intéressant.
Imaginez une application de catalogue de produits. Nous le faisons en une seule activité. Partout le mode portrait est fixe, mais le client souhaite une fonctionnalité lorsque, lors de la visualisation de la galerie de photos, l'utilisateur peut les regarder en orientation paysage. Comment soutenir cela?


Quelqu'un offrira la première béquille :


 <activity android:name=".AppActivity" android:configChanges="orientation" /> 

 override fun onConfigurationChanged(newConfig: Configuration?) { if (newConfig?.orientation == Configuration.ORIENTATION_LANDSCAPE) { //ignore } else { super.onConfigurationChanged(newConfig) } } 

Ainsi, nous ne pouvons pas appeler super.onConfigurationChanged(newConfig) , mais le traiter nous-mêmes et faire pivoter uniquement la vue nécessaire à l'écran.
Mais avec l'API 23, le projet plantera avec une SuperNotCalledException , donc un mauvais choix .


Les déclarations ci-dessus ont fait une erreur:
J'ai été raisonnablement corrigé dans les commentaires que l'ajout d'Android: configChanges = "orientation | screenSize" est suffisant, puis vous pouvez appeler super et Activity ne sera pas recréé lors du virage. Il est utile de l'utiliser lorsqu'une WebView ou une carte est à l'écran et qu'il faut beaucoup de temps pour l'initialiser, et vous voulez éviter cela.
Cela aidera à résoudre le cas décrit avec la galerie, mais le message principal de cette section: n'ignorez pas la recréation de l'activité , cela peut se produire dans de nombreux autres cas.


Quelqu'un pourrait suggérer une autre solution:


 <activity android:name=".AppActivity" android:screenOrientation="portrait" /> <activity android:name=".RotateActivity" /> 

Mais de cette façon, nous nous éloignons de l'approche à activité unique pour résoudre un problème simple et nous priver de tous les avantages de l'approche. C'est une béquille, et une béquille est toujours un mauvais choix .


Voici la bonne solution:


 <activity android:name=".AppActivity" android:configChanges="orientation" /> 

 override fun onResume() { super.onResume() activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR } override fun onPause() { super.onPause() activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT } 

C'est-à-dire que lorsque le fragment est ouvert, l'application commence à «tourner» et lorsqu'il revient, il est à nouveau corrigé. D'après mon expérience, c'est ainsi que fonctionne l' application AirBnB . Si vous ouvrez une vue des photos du logement, le traitement des virages est activé, mais en orientation paysage, vous pouvez faire glisser la photo vers le bas pour quitter la galerie. En dessous, l'écran précédent sera visible en orientation paysage, ce que vous ne trouverez généralement pas, car immédiatement après avoir quitté la galerie, l'écran se transformera en portrait et sera fixé.



C'est là que la préparation en temps opportun pour les tours d'écran aidera.


Barre d'état transparente


Seule l'activité peut fonctionner avec la barre système, mais maintenant nous n'en avons qu'une, vous devez donc toujours spécifier


 <item name="android:windowTranslucentStatus">true</item> 

Mais sur certains écrans, il n'est pas nécessaire de "ramper" en dessous, et vous devez afficher tout le contenu ci-dessous. Le drapeau vient à la rescousse


 android:fitsSystemWindows="true" 

qui indique la disposition que vous ne devez pas dessiner sous la barre système. Mais si vous le spécifiez dans la disposition du fragment, puis essayez d'afficher le fragment via la transaction dans le gestionnaire de fragments, vous serez déçu ... cela ne fonctionnera pas!
La réponse est rapidement google
Je vous recommande fortement de vous familiariser avec une réponse vraiment complète et de nombreux liens utiles.
Une solution rapide et fonctionnelle ( mais pas la bonne ) consiste à envelopper la mise en page dans CoordinatorLayout


 <android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent" android:fitsSystemWindows="true"> </android.support.design.widget.CoordinatorLayout> 

Une meilleure solution permet également de traiter le clavier.


Modifier la disposition lorsque le clavier apparaît


Lorsque le clavier quitte, la disposition doit changer afin que les éléments importants de l'interface utilisateur ne restent pas hors de portée. Et si auparavant nous pouvions spécifier différents modes de réaction pour le clavier pour différentes activités, nous devons maintenant le faire dans une seule activité. Il est donc nécessaire d'utiliser


 android:windowSoftInputMode="adjustResize" 

Si vous utilisez l'approche de la section précédente pour traiter une barre d'état transparente, vous trouverez une erreur malheureuse: si un fragment a réussi à se glisser sous la barre d'état, puis lorsque le clavier apparaît, il rétrécit au-dessus et en dessous, car la barre d'état et le clavier à l'intérieur du système fonctionnent. SystemWindows .


Faites attention au titre



Que faire Lisez la documentation!Et assurez-vous de voir les Chris Banes parler de WindowInsets .


L'utilisation de WindowInsets permettra


  • trouver la bonne hauteur d'état de la barre (et non le code dur 51dp)
  • préparer l'application pour toute découpe dans les écrans des nouveaux smartphones
  • connaître la hauteur du clavier (c'est réel!)
  • Recevez des événements et répondez à l'apparence du clavier.

Tout le monde apprend WindowInsets!


Écran d'accueil


Si quelqu'un d'autre n'est pas au courant, l'écran canonique Splash n'est pas le premier écran de l'application qui charge les données, mais ce que l'utilisateur voit au démarrage jusqu'à ce que le contenu de l'activité ait le temps de s'afficher. Il existe de nombreux articles sur ce sujet.



, Single-Activity, Splash screen. , deep-link Splash screen .



, , , .


, . Single-Activity. - , , .
...
Intent, , ...
Et ensuite? :


  • , «». «», . , !
  • , …

, . ? — «» «» .


Que faire , .


Activity!
, : , — .
— , ( Activity), .


Activity — . Activity, . .



Conclusion


() , Activity, Android-. , .


: Google . — , , Activity .


, , , ! Je vous remercie!

Source: https://habr.com/ru/post/fr426617/


All Articles