Composant de navigation Android. Des choses simples à faire soi-même

Bonjour à tous! Je veux parler des fonctionnalités du travail de Navigation Architecture Component , à cause desquelles j'ai eu une impression ambiguë de la bibliothèque.

Cet article n'est pas un guide étape par étape; il omet les détails de mise en œuvre pour se concentrer sur les points clés. Il existe de nombreux cas d'utilisation similaires sur Internet (il existe également des traductions) - ils vous aideront à vous familiariser avec la bibliothèque. Aussi, avant de lire, je propose d'étudier la documentation .

image

Je dois dire tout de suite que je considère certainement la bibliothèque utile et n'exclut pas la possibilité d'une mauvaise utilisation, mais j'ai probablement tout essayé avant d'écrire cet article.

Voici donc les scénarios dans la mise en œuvre dont les attentes pour la fonctionnalité ne coïncidaient pas avec la réalité de la mise en œuvre:

  • basculer entre les éléments de menu dans le tiroir de navigation
  • découverte d'une nouvelle Activité avec son graphe de navigation
  • passage de paramètres à startDestination

Basculer entre les éléments de menu


C'est l'une de ces fonctionnalités qui a influencé la décision d'utiliser le composant de navigation.

Il vous suffit de rendre l'identifiant de l'élément de menu identique

activity_main_drawer.xml
<?xml version="1.0" encoding="utf-8"?> <menu xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" tools:showIn="navigation_view"> <group android:checkableBehavior="single"> <item android:id="@+id/importFragment" android:icon="@drawable/ic_menu_camera" android:title="Import"/> <item android:id="@+id/galleryFragment" android:icon="@drawable/ic_menu_gallery" android:title="Gallery"/> <item android:id="@+id/slideshowFragment" android:icon="@drawable/ic_menu_slideshow" android:title="Slideshow"/> <!--    --> 


et id d'écran (destination dans le graphique de navigation)

mobile_navigation.xml
 <?xml version="1.0" encoding="utf-8"?> <navigation xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/mobile_navigation" app:startDestination="@id/importFragment"> <fragment android:id="@+id/importFragment" android:name="com.xiii.navigationapplication.ImportFragment" android:label="fragment_import" tools:layout="@layout/fragment_import"/> <fragment android:id="@+id/galleryFragment" android:name="com.xiii.navigationapplication.GalleryFragment" android:label="fragment_gallery" tools:layout="@layout/fragment_gallery"/> <fragment android:id="@+id/slideshowFragment" android:name="com.xiii.navigationapplication.SlideshowFragment" android:label="fragment_slideshow" tools:layout="@layout/fragment_slideshow"/> </navigation> 


alors vous devez associer le menu au contrôleur de navigation:

MainActivity.kt
 class MainActivity : AppCompatActivity() { private val navController by lazy(LazyThreadSafetyMode.NONE) { Navigation.findNavController(this, R.id.nav_host_fragment) } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) setSupportActionBar(toolbar) //   ""  toolbar NavigationUI.setupWithNavController(toolbar, navController, drawer_layout) //      nav_view.setupWithNavController(navController) } //     override fun onSupportNavigateUp() = navController.navigateUp() } 


La navigation dans le menu a fonctionné - n'est-ce pas un miracle?!

Navigation dans les menus

Faites attention au «hamburger» (icône de menu), lorsque vous basculez entre les éléments du menu, il change son état sur le bouton «retour». Ce comportement semblait inhabituel ( familier - comme dans l'application Play Market) et, pendant un certain temps, j'ai essayé de comprendre ce qui n'allait pas.

C’est tout! Après avoir lu la documentation sur les principes de navigation (à savoir, les points deux et trois ), j'ai réalisé que le «hamburger» n'est affiché que pour startDestination , ou plutôt de cette façon: le bouton de retour est affiché pour tout le monde sauf startDestination . La situation peut être modifiée en appliquant diverses astuces dans l'abonnement ( addOnNavigatedListener () ) pour modifier la destination , mais elles ne doivent même pas être décrites. Cela fonctionne comme ça, vous devez vous réconcilier.

Ouverture d'une nouvelle activité


L'activité peut agir en tant qu'hôte de navigation et, en même temps, dans le graphique de navigation peut agir en tant que destination . L'ouverture d'une activité sans graphique de navigation imbriqué fonctionne comme prévu , c'est-à-dire un appel:

 navController.navigate(R.id.editActivity) 

effectuera la transition (comme dans le cas des fragments) et ouvrira l'activité demandée.
Il est beaucoup plus intéressant de considérer le cas où l'activité cible elle-même agit comme hôte de navigation , c'est-à-dire l'option 2 de la documentation :

Schéma de navigation

À titre d'exemple, regardons une activité pour ajouter une note. Il contiendra le fragment principal avec les champs de saisie EditFragment ; ce sera startDestination dans le graphique de navigation. Supposons que lors de l'édition, nous devons joindre une photo, pour cela, nous irons dans PhotoFragment pour obtenir une photo de l'appareil photo. Le graphique de navigation ressemblera à ceci:

edit_navigation.xml
 <?xml version="1.0" encoding="utf-8"?> <navigation xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/edit_navigation" app:startDestination="@id/editFragment"> <fragment android:id="@+id/editFragment" android:name="com.xiii.navigationapplication.ui.edit.EditFragment" android:label="fragment_edit" tools:layout="@layout/fragment_edit"> <action android:id="@+id/action_editFragment_to_photoFragment" app:destination="@id/photoFragment"/> </fragment> <fragment android:id="@+id/photoFragment" android:name="com.xiii.navigationapplication.ui.edit.PhotoFragment" android:label="fragment_photo" tools:layout="@layout/fragment_photo"/> </navigation> 


EditActivity n'est pas très différent de MainActivity . La principale différence est qu'il n'y a pas de menu sur EditActivity :

EditActivity.kt
 class EditActivity : AppCompatActivity() { private val navController by lazy(LazyThreadSafetyMode.NONE) { Navigation.findNavController(this, R.id.nav_host_fragment) } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_edit) setSupportActionBar(toolbar) //    ""  toolbar NavigationUI.setupWithNavController(toolbar, navController) } override fun onSupportNavigateUp() = navController.navigateUp() fun takePhoto(view: View) { navController.navigate(R.id.action_editFragment_to_photoFragment) } } 


L'activité s'ouvre, la navigation à l'intérieur fonctionne:

Modifier la navigation de l'activité

Encore une fois, faites attention au bouton de navigation dans la barre d'outils - sur le EditFragment de départ, il n'y a pas de bouton "Retour à l'activité parent" (mais je voudrais). Du point de vue de la documentation, tout est légal ici: un nouveau graphe de navigation, une nouvelle valeur startDestination , le bouton "Retour" n'apparaît pas à startDestination , la fin.

Pour ceux qui veulent revenir à leur comportement habituel avec l'activité parentale , tout en conservant la fonctionnalité de basculement entre les fragments, je peux proposer cette approche béquille :

1. Spécifiez l'activité parent dans le manifeste
 <activity android:name=".EditActivity" android:parentActivityName=".MainActivity" android:theme="@style/AppTheme.NoActionBar"> <!-- Parent activity meta-data to support 4.0 and lower --> <meta-data android:name="android.support.PARENT_ACTIVITY" android:value=".MainActivity" /> </activity> 


2. Ajoutez un abonnement dans lequel nous remplacerons id startDestination
 class EditActivity : AppCompatActivity() { private val navController by lazy(LazyThreadSafetyMode.NONE) { Navigation.findNavController(this, R.id.nav_host_fragment) } private var isStartDestination = true override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_edit) setSupportActionBar(toolbar) val startDestinationId = navController.graph.startDestination //    id,  NavigationUI.ActionBarOnNavigatedListener     //      destination    startDestination navController.addOnNavigatedListener { controller, destination -> isStartDestination = destination.id == startDestinationId // R.id.fake_start_destination  id        controller.graph.startDestination = if (isStartDestination) R.id.fake_start_destination else startDestinationId } //    ""  toolbar NavigationUI.setupActionBarWithNavController(this, navController) } override fun onSupportNavigateUp(): Boolean { //  startDestination      Navigation Component return if (isStartDestination) super.onSupportNavigateUp() else navController.navigateUp() } fun takePhoto(view: View) { navController.navigate(R.id.action_editFragment_to_photoFragment) } } 


Un abonnement est nécessaire pour que pour NavigationUI.ActionBarOnNavigatedListener toutes les destinations ne soient pas startDestination . Ainsi, NavigationUI.ActionBarOnNavigatedListener ne masquera pas le bouton de navigation (reportez-vous à la source pour plus de détails). Ajoutez à cela le traitement onSupportNavigateUp () de manière régulière à startDestination et obtenez ce que nous voulions.

Il vaut la peine de dire que la solution est loin d'être idéale, ne serait-ce que parce que c'est une intervention non évidente dans le comportement de la bibliothèque. Je pense que des problèmes peuvent survenir lors de l'utilisation de liens profonds (je ne l'ai pas encore testé).

Passer des paramètres à startDestination


Le composant de navigation dispose d'un mécanisme pour transmettre des paramètres d'une destination à une autre. Il existe même un outil pour assurer la sécurité des types grâce à la génération de code (pas mal).

Nous allons maintenant analyser le cas, à cause duquel je n'ai pas pu mettre un solide cinq à cette fonction.

Revenons à EditActivity , un scénario assez familier lorsqu'une activité est utilisée pour créer et modifier des objets. Lorsque vous ouvrez un objet pour le modifier dans Activity, vous devez transférer, par exemple, l'ID de l'objet - faisons-le de manière régulière:

1. Ajoutez un paramètre au graphique pour EditActivity
J'ai ajouté le paramètre directement à l'élément racine du graphique (navigation), mais peut être ajouté au fragment cible. À partir de cela, seule la méthode d'obtention du paramètre changera.

 <?xml version="1.0" encoding="utf-8"?> <navigation xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/edit_navigation" app:startDestination="@id/editFragment"> <argument android:name="id" app:argType="integer"/> <fragment android:id="@+id/editFragment" android:name="com.xiii.navigationapplication.ui.edit.EditFragment" android:label="fragment_edit" tools:layout="@layout/fragment_edit"> <action android:id="@+id/action_editFragment_to_photoFragment" app:destination="@id/photoFragment"/> </fragment> <fragment android:id="@+id/photoFragment" android:name="com.xiii.navigationapplication.ui.edit.PhotoFragment" android:label="fragment_photo" tools:layout="@layout/fragment_photo"/> </navigation> 


2. Ajoutez des actions au graphique principal
J'ai ajouté des actions d'ajout et de modification à l'un des fragments, de sorte qu'elles ne seront disponibles qu'à partir de celui-ci.

 <?xml version="1.0" encoding="utf-8"?> <navigation xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/mobile_navigation" app:startDestination="@id/importFragment"> <fragment android:id="@+id/importFragment" android:name="com.xiii.navigationapplication.ImportFragment" android:label="fragment_import" tools:layout="@layout/fragment_import"> <action android:id="@+id/add" app:destination="@id/editActivity"> <argument android:name="id" app:argType="integer" android:defaultValue="0"/> </action> <action android:id="@+id/edit" app:destination="@id/editActivity"> <argument android:name="id" app:argType="integer"/> </action> </fragment> <fragment android:id="@+id/galleryFragment" android:name="com.xiii.navigationapplication.GalleryFragment" android:label="fragment_gallery" tools:layout="@layout/fragment_gallery"/> <fragment android:id="@+id/slideshowFragment" android:name="com.xiii.navigationapplication.SlideshowFragment" android:label="fragment_slideshow" tools:layout="@layout/fragment_slideshow"/> <activity android:id="@+id/editActivity" android:name="com.xiii.navigationapplication.EditActivity" android:label="activity_edit" tools:layout="@layout/activity_edit"/> </navigation> 


3. Préparez les paramètres et demandez la transition
Dans cet exemple, ImportFragmentDirections est la classe Safe- Args générée automatiquement.

 val direction = ImportFragmentDirections.edit(123 /* id  */) navController.navigate(direction) 


3. Obtenez l'id dans le fragment
 class EditFragment : Fragment() { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { //    ,  fragment     startDestination // val id = EditFragmentArgs.fromBundle(arguments) val id = EditFragmentArgs.fromBundle(requireActivity().intent.extras) return inflater.inflate(R.layout.fragment_edit, container, false) } } 


Vous avez certes prêté attention aux fonctionnalités d'obtention des paramètres dans EditFragment . Cela fonctionne car l'action d'édition (à partir du point 1) transmet des arguments à EditActivity , et pour sa part, pour une raison quelconque, il est avide de ne pas la transmettre au graphique (par exemple, en appelant navController.graph.setDefaultArguments () ). Cette fonction peut être contournée en préparant manuellement le contrôleur de navigation . Une façon est décrite sur StackOwerflow .

La plus grande difficulté se posera peut-être lors de l'utilisation simultanée de startDestination et de destination habituelle. En d'autres termes , lors de la transmission et de la transmission de paramètres à startDestination à partir de toute autre destination de ce graphique, le fragment devra déterminer indépendamment d'où extraire les paramètres: à partir d'arguments ou d'intention.extras. Il faut en tenir compte lors de la conception de transitions avec des paramètres de passage.



En résumé, je tiens à noter que je n'ai moi-même pas cessé d'utiliser la bibliothèque et, malgré les inconvénients énumérés de la fonctionnalité, je la trouve suffisamment utile pour être recommandée. J'espère vraiment que dans les prochaines versions, la situation changera, au moins avec le transfert des paramètres à startDestination .

Merci de votre attention. Votre code de travail!

Les sources de l'article sont publiées sur GitHub .

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


All Articles