Navigation conditionnelle

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 :

Un flux de connexion est géré indépendamment du flux de navigation principal de l'application.
Figure 1. Un flux de connexion est géré indépendamment du flux de navigation principal de l'application.

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.