Lorsque vous concevez la navigation pour votre application, vous pouvez choisir d'accéder à une destination au lieu d'une autre en fonction d'une logique conditionnelle. Par exemple, un utilisateur peut suivre un lien profond vers une destination qui nécessite d'être connecté. De même, dans un jeu, vous pouvez avoir différentes destinations selon que le joueur gagne ou perd.
Connexion d'un utilisateur
Dans cet exemple, un utilisateur tente d'accéder à un écran de profil qui nécessite une authentification. Comme cette action nécessite une authentification, l'utilisateur doit être redirigé vers un écran de connexion s'il n'est pas déjà authentifié.
Dans cet exemple, le graphique de navigation peut se présenter comme suit :
Pour l'authentification, l'application doit accéder à login_fragment
, où l'utilisateur peut saisir un nom d'utilisateur et un mot de passe pour s'authentifier. Si l'authentification est acceptée, l'utilisateur est renvoyé à l'écran profile_fragment
. Si elle n'est pas acceptée, une Snackbar
indique à l'utilisateur que ses identifiants ne sont pas valides.
Si l'utilisateur revient à l'écran de profil sans se connecter, il est redirigé vers l'écran main_fragment
.
Voici le graphique de navigation pour cette application :
<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/nav_graph"
app:startDestination="@id/main_fragment">
<fragment
android:id="@+id/main_fragment"
android:name="com.google.android.conditionalnav.MainFragment"
android:label="fragment_main"
tools:layout="@layout/fragment_main">
<action
android:id="@+id/navigate_to_profile_fragment"
app:destination="@id/profile_fragment"/>
</fragment>
<fragment
android:id="@+id/login_fragment"
android:name="com.google.android.conditionalnav.LoginFragment"
android:label="login_fragment"
tools:layout="@layout/login_fragment"/>
<fragment
android:id="@+id/profile_fragment"
android:name="com.google.android.conditionalnav.ProfileFragment"
android:label="fragment_profile"
tools:layout="@layout/fragment_profile"/>
</navigation>
MainFragment
contient un bouton sur lequel l'utilisateur peut cliquer pour afficher son profil.
Si l'utilisateur souhaite afficher l'écran du profil, il doit d'abord s'authentifier. Cette interaction est modélisée à l'aide de deux fragments distincts, mais elle dépend de l'état de l'utilisateur partagé. Ces informations d'état ne relèvent pas de la responsabilité de l'un ou l'autre de ces deux fragments et sont mieux conservées dans un UserViewModel
partagé.
Ce ViewModel
est partagé entre les fragments en déterminant sa portée par rapport à l'activité, qui implémente ViewModelStoreOwner
. Dans l'exemple suivant, requireActivity()
renvoie MainActivity
, car MainActivity
héberge ProfileFragment
:
Kotlin
class ProfileFragment : Fragment() { private val userViewModel: UserViewModel by activityViewModels() ... }
Java
public class ProfileFragment extends Fragment { private UserViewModel userViewModel; @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); userViewModel = new ViewModelProvider(requireActivity()).get(UserViewModel.class); ... } ... }
Les données utilisateur dans UserViewModel
sont exposées via LiveData
. Par conséquent, pour déterminer où naviguer, vous devez observer ces données. Lorsque vous accédez à ProfileFragment
, l'application affiche un message de bienvenue si les données utilisateur sont présentes. Si les données utilisateur sont null
, vous accédez à LoginFragment
, car l'utilisateur doit s'authentifier avant de voir son profil. Définissez la logique de décision dans votre fichier ProfileFragment
, comme illustré dans l'exemple suivant :
Kotlin
class ProfileFragment : Fragment() { private val userViewModel: UserViewModel by activityViewModels() override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) val navController = findNavController() userViewModel.user.observe(viewLifecycleOwner, Observer { user -> if (user != null) { showWelcomeMessage() } else { navController.navigate(R.id.login_fragment) } }) } private fun showWelcomeMessage() { ... } }
Java
public class ProfileFragment extends Fragment { private UserViewModel userViewModel; @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); userViewModel = new ViewModelProvider(requireActivity()).get(UserViewModel.class); final NavController navController = Navigation.findNavController(view); userViewModel.user.observe(getViewLifecycleOwner(), (Observer<User>) user -> { if (user != null) { showWelcomeMessage(); } else { navController.navigate(R.id.login_fragment); } }); } private void showWelcomeMessage() { ... } }
Si les données utilisateur sont null
lorsqu'elles atteignent ProfileFragment
, elles sont redirigées vers LoginFragment
.
Vous pouvez utiliser NavController.getPreviousBackStackEntry()
pour récupérer la valeur NavBackStackEntry
de la destination précédente, qui encapsule l'état propre à NavController
pour la destination. LoginFragment
utilise la valeur SavedStateHandle
de la NavBackStackEntry
précédente pour définir une valeur initiale indiquant si l'utilisateur a réussi ou non à se connecter. Il s'agit de l'état à afficher dans le cas où l'utilisateur appuie immédiatement sur le bouton Retour du système. Définir cet état à l'aide de SavedStateHandle
garantit que l'état persiste jusqu'à l'arrêt du processus.
Kotlin
class LoginFragment : Fragment() { companion object { const val LOGIN_SUCCESSFUL: String = "LOGIN_SUCCESSFUL" } private val userViewModel: UserViewModel by activityViewModels() private lateinit var savedStateHandle: SavedStateHandle override fun onViewCreated(view: View, savedInstanceState: Bundle?) { savedStateHandle = findNavController().previousBackStackEntry!!.savedStateHandle savedStateHandle.set(LOGIN_SUCCESSFUL, false) } }
Java
public class LoginFragment extends Fragment { public static String LOGIN_SUCCESSFUL = "LOGIN_SUCCESSFUL" private UserViewModel userViewModel; private SavedStateHandle savedStateHandle; @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { userViewModel = new ViewModelProvider(requireActivity()).get(UserViewModel.class); savedStateHandle = Navigation.findNavController(view) .getPreviousBackStackEntry() .getSavedStateHandle(); savedStateHandle.set(LOGIN_SUCCESSFUL, false); } }
Une fois que l'utilisateur a saisi un nom d'utilisateur et un mot de passe, ceux-ci sont transmis à UserViewModel
pour l'authentification. Si l'authentification réussit, UserViewModel
stocke les données de l'utilisateur. LoginFragment
met ensuite à jour la valeur LOGIN_SUCCESSFUL
sur SavedStateHandle
et se retire de la pile "Retour".
Kotlin
class LoginFragment : Fragment() { companion object { const val LOGIN_SUCCESSFUL: String = "LOGIN_SUCCESSFUL" } private val userViewModel: UserViewModel by activityViewModels() private lateinit var savedStateHandle: SavedStateHandle override fun onViewCreated(view: View, savedInstanceState: Bundle?) { savedStateHandle = findNavController().previousBackStackEntry!!.savedStateHandle savedStateHandle.set(LOGIN_SUCCESSFUL, false) val usernameEditText = view.findViewById(R.id.username_edit_text) val passwordEditText = view.findViewById(R.id.password_edit_text) val loginButton = view.findViewById(R.id.login_button) loginButton.setOnClickListener { val username = usernameEditText.text.toString() val password = passwordEditText.text.toString() login(username, password) } } fun login(username: String, password: String) { userViewModel.login(username, password).observe(viewLifecycleOwner, Observer { result -> if (result.success) { savedStateHandle.set(LOGIN_SUCCESSFUL, true) findNavController().popBackStack() } else { showErrorMessage() } }) } fun showErrorMessage() { // Display a snackbar error message } }
Java
public class LoginFragment extends Fragment { public static String LOGIN_SUCCESSFUL = "LOGIN_SUCCESSFUL" private UserViewModel userViewModel; private SavedStateHandle savedStateHandle; @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { userViewModel = new ViewModelProvider(requireActivity()).get(UserViewModel.class); savedStateHandle = Navigation.findNavController(view) .getPreviousBackStackEntry() .getSavedStateHandle(); savedStateHandle.set(LOGIN_SUCCESSFUL, false); EditText usernameEditText = view.findViewById(R.id.username_edit_text); EditText passwordEditText = view.findViewById(R.id.password_edit_text); Button loginButton = view.findViewById(R.id.login_button); loginButton.setOnClickListener(v -> { String username = usernameEditText.getText().toString(); String password = passwordEditText.getText().toString(); login(username, password); }); } private void login(String username, String password) { userViewModel.login(username, password).observe(viewLifecycleOwner, (Observer<LoginResult>) result -> { if (result.success) { savedStateHandle.set(LOGIN_SUCCESSFUL, true); NavHostFragment.findNavController(this).popBackStack(); } else { showErrorMessage(); } }); } private void showErrorMessage() { // Display a snackbar error message } }
Notez que toutes les logiques d'authentification sont conservées dans UserViewModel
. Ce point est important, car il n'appartient pas à LoginFragment
ni à ProfileFragment
de déterminer la façon dont les utilisateurs sont authentifiés. Encapsuler votre logique dans un ViewModel
facilite non seulement le partage, mais également les tests. Si votre logique de navigation est complexe, vérifiez spécifiquement cette logique au moyen de tests. Consultez le guide de l'architecture des applications pour en savoir plus sur la structuration de l'architecture de votre application autour des composants testables.
Dans ProfileFragment
, la valeur LOGIN_SUCCESSFUL
stockée dans SavedStateHandle
peut être observée dans la méthode onCreate()
. Lorsque l'utilisateur revient à ProfileFragment
, la valeur LOGIN_SUCCESSFUL
est vérifiée. Si la valeur est false
, l'utilisateur peut être redirigé vers MainFragment
.
Kotlin
class ProfileFragment : Fragment() { ... override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val navController = findNavController() val currentBackStackEntry = navController.currentBackStackEntry!! val savedStateHandle = currentBackStackEntry.savedStateHandle savedStateHandle.getLiveData<Boolean>(LoginFragment.LOGIN_SUCCESSFUL) .observe(currentBackStackEntry, Observer { success -> if (!success) { val startDestination = navController.graph.startDestination val navOptions = NavOptions.Builder() .setPopUpTo(startDestination, true) .build() navController.navigate(startDestination, null, navOptions) } }) } ... }
Java
public class ProfileFragment extends Fragment { ... @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); NavController navController = NavHostFragment.findNavController(this); NavBackStackEntry navBackStackEntry = navController.getCurrentBackStackEntry(); SavedStateHandle savedStateHandle = navBackStackEntry.getSavedStateHandle(); savedStateHandle.getLiveData(LoginFragment.LOGIN_SUCCESSFUL) .observe(navBackStackEntry, (Observer<Boolean>) success -> { if (!success) { int startDestination = navController.getGraph().getStartDestination(); NavOptions navOptions = new NavOptions.Builder() .setPopUpTo(startDestination, true) .build(); navController.navigate(startDestination, null, navOptions); } }); } ... }
Si l'utilisateur a réussi à se connecter, ProfileFragment
affiche un message de bienvenue.
La technique utilisée ici pour vérifier le résultat vous permet de distinguer deux cas différents :
- Le cas initial, où l'utilisateur n'est pas connecté et doit être invité à se connecter.
- Le cas où l'utilisateur n'est pas connecté, car il a choisi de ne pas se connecter (résultat
false
).
En distinguant ces cas d'utilisation, vous pouvez éviter de demander plusieurs fois à l'utilisateur de se connecter. C'est à vous de décider de la logique métier à adopter en cas d'échec. Vous pouvez par exemple afficher une boîte de dialogue en superposition qui explique pourquoi l'utilisateur doit se connecter. Vous pouvez aussi terminer l'ensemble de l'activité ou rediriger l'utilisateur vers une destination qui ne nécessite pas de connexion, comme dans l'exemple de code précédent.