Bedingte Navigation

Beim Entwerfen einer Navigation für Ihre Anwendung möchten Sie möglicherweise basierend auf bedingter Logik zu einem Ziel oder einem anderen navigieren. Beispielsweise kann ein Nutzer einem Deeplink zu einem Ziel folgen, bei dem er angemeldet sein muss, oder Sie haben in einem Spiel andere Ziele für den Fall, dass der Spieler gewinnt oder verliert.

Nutzeranmeldung

In diesem Beispiel versucht ein Nutzer, einen Profilbildschirm aufzurufen, der eine Authentifizierung erfordert. Da für diese Aktion eine Authentifizierung erforderlich ist, sollte der Nutzer zu einem Anmeldebildschirm weitergeleitet werden, falls er noch nicht authentifiziert ist.

Die Navigationsgrafik für dieses Beispiel könnte etwa so aussehen:

Ein Anmeldevorgang wird unabhängig vom Hauptnavigationsablauf der App durchgeführt.
Abbildung 1: Der Anmeldevorgang wird unabhängig vom Hauptnavigationsablauf der App durchgeführt.

Zur Authentifizierung muss die Anwendung den login_fragment aufrufen, wo der Nutzer einen Nutzernamen und ein Passwort zur Authentifizierung eingeben kann. Wird die Einladung angenommen, wird der Nutzer zum Bildschirm profile_fragment zurückgeleitet. Wenn sie nicht akzeptiert werden, wird der Nutzer mithilfe eines Snackbar darüber informiert, dass seine Anmeldedaten ungültig sind. Wenn der Nutzer ohne Anmeldung zum Profilbildschirm zurückkehrt, wird er zum Bildschirm main_fragment weitergeleitet.

Hier ist die Navigationsgrafik für diese App:

<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 enthält eine Schaltfläche, über die der Nutzer sein Profil aufrufen kann. Wenn der Nutzer den Profilbildschirm sehen möchte, muss er sich zuerst authentifizieren. Diese Interaktion wird mit zwei separaten Fragmenten modelliert, hängt jedoch von einem gemeinsamen Nutzerstatus ab. Für diese Statusinformationen sind keines der beiden Fragmente verantwortlich und sie befinden sich angemessener in einem gemeinsamen UserViewModel. Diese ViewModel wird von den Fragmenten gemeinsam genutzt, indem sie der Aktivität zugeordnet wird, die ViewModelStoreOwner implementiert. Im folgenden Beispiel wird requireActivity() in MainActivity aufgelöst, da MainActivity ProfileFragment hostet:

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);
        ...
    }
    ...
}

Die Nutzerdaten in UserViewModel werden über LiveData freigegeben. Diese Daten sollten Sie bei der Entscheidung berücksichtigen, wohin Sie navigieren möchten. Beim Aufrufen von ProfileFragment zeigt die Anwendung eine Willkommensnachricht an, wenn die Nutzerdaten vorhanden sind. Wenn die Nutzerdaten null sind, rufen Sie LoginFragment auf, da sich der Nutzer authentifizieren muss, bevor sein Profil angezeigt wird. Definieren Sie die Entscheidungslogik in Ihrem ProfileFragment, wie im folgenden Beispiel gezeigt:

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() {
        ...
    }
}

Wenn die Nutzerdaten beim Erreichen von ProfileFragment den Status null haben, werden sie zur LoginFragment weitergeleitet.

Mit NavController.getPreviousBackStackEntry() können Sie den NavBackStackEntry für das vorherige Ziel abrufen, der den NavController-spezifischen Status für das Ziel enthält. LoginFragment verwendet den SavedStateHandle des vorherigen NavBackStackEntry, um einen Anfangswert festzulegen, der angibt, ob sich der Nutzer erfolgreich angemeldet hat. Dieser Status wird zurückgegeben, wenn Nutzende sofort auf die Schaltfläche „Zurück“ klicken. Wenn Sie diesen Status mit SavedStateHandle festlegen, wird sichergestellt, dass der Status auch nach dem Beenden des Prozesses bestehen bleibt.

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);
    }
}

Sobald der Nutzer einen Nutzernamen und ein Passwort eingibt, werden diese zur Authentifizierung an den UserViewModel übergeben. Wenn die Authentifizierung erfolgreich ist, werden die Nutzerdaten im UserViewModel gespeichert. Das LoginFragment aktualisiert dann den LOGIN_SUCCESSFUL-Wert auf SavedStateHandle und löst sich selbst aus dem Back-Stack.

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
    }
}

Die gesamte Logik für die Authentifizierung wird in UserViewModel gespeichert. Dies ist wichtig, da weder LoginFragment noch ProfileFragment dafür verantwortlich sind, zu bestimmen, wie Nutzer authentifiziert werden. Wenn Sie Ihre Logik in eine ViewModel kapseln, ist sie nicht nur einfacher zu teilen, sondern auch einfacher zu testen. Wenn Ihre Navigationslogik komplex ist, sollten Sie sie insbesondere durch Tests überprüfen. Weitere Informationen zur Strukturierung Ihrer App-Architektur um testbare Komponenten finden Sie im Leitfaden zur Anwendungsarchitektur.

Im ProfileFragment kann der in SavedStateHandle gespeicherte LOGIN_SUCCESSFUL-Wert in der Methode onCreate() beobachtet werden. Wenn der Nutzer zu ProfileFragment zurückkehrt, wird der Wert LOGIN_SUCCESSFUL geprüft. Wenn der Wert false ist, kann der Nutzer zurück zu MainFragment weitergeleitet werden.

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);
                    }
                });
    }

    ...
}

Wenn sich der Nutzer erfolgreich angemeldet hat, zeigt ProfileFragment eine Willkommensnachricht an.

Mit der hier verwendeten Technik zum Überprüfen des Ergebnisses können Sie zwischen zwei verschiedenen Fällen unterscheiden:

  • Der erste Fall, bei dem der Nutzer nicht angemeldet ist und zur Anmeldung aufgefordert werden sollte.
  • Der Nutzer ist nicht angemeldet, weil er sich nicht angemeldet hat (aufgrund von false).

Wenn Sie diese Anwendungsfälle unterscheiden, vermeiden Sie, dass Nutzer wiederholt zur Anmeldung aufgefordert werden. Die Geschäftslogik für den Umgang mit Fehlern bleibt Ihnen überlassen und kann, wie im vorherigen Codebeispiel der Fall, das Einblenden eines Overlays beinhalten, das erklärt, warum sich der Nutzer anmelden muss, die gesamte Aktivität abschließen oder den Nutzer zu einem Ziel weiterleiten, das keine Anmeldung erfordert.