Nawigacja warunkowa

Projektując nawigację do aplikacji, możesz pewnie posługiwać się trasami dojazdu do jednego z innych miejsc docelowych, korzystając z logiki warunkowej. Użytkownik może na przykład kliknąć precyzyjny link do miejsca docelowego, które wymaga zalogowania się, lub mieć w grze różne miejsca docelowe, w których gracz wygrywa lub przegrywa.

Logowanie użytkownika

W tym przykładzie użytkownik próbuje przejść do ekranu profilu, który wymaga uwierzytelnienia. To działanie wymaga uwierzytelnienia, więc użytkownik powinien zostać przekierowany na ekran logowania, jeśli nie został jeszcze uwierzytelniony.

Wykres nawigacyjny w tym przykładzie może wyglądać np. tak:

proces logowania jest obsługiwany niezależnie od głównego procesu nawigacji w aplikacji.
Rysunek 1. Proces logowania jest obsługiwany niezależnie od głównego procesu nawigacji w aplikacji.

Aby uwierzytelnić się, aplikacja musi przejść do interfejsu login_fragment, gdzie użytkownik może wpisać nazwę użytkownika i hasło w celu uwierzytelnienia. Jeśli użytkownik zaakceptuje zaproszenie, zostanie przekierowany z powrotem na ekran profile_fragment. Jeśli nie zostaną zaakceptowane, użytkownik zostanie poinformowany o nieprawidłowych danych logowania za pomocą Snackbar. Jeśli użytkownik wróci do ekranu profilu bez logowania się, zostanie przekierowany na ekran main_fragment.

Oto wykres nawigacyjny tej aplikacji:

<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 zawiera przycisk, który użytkownik może kliknąć, aby wyświetlić swój profil. Jeśli użytkownik chce zobaczyć ekran profilu, musi najpierw się uwierzytelnić. Ta interakcja jest modelowana z wykorzystaniem 2 osobnych fragmentów, ale zależy od współdzielonego stanu użytkownika. Za informacje o stanie nie odpowiada żaden z tych 2 fragmentów, lecz lepiej przechowywany jest we wspólnym obiekcie UserViewModel. Ten obiekt ViewModel jest współużytkowany między fragmentami przez ograniczenie go do działania, co korzysta z metody ViewModelStoreOwner. W tym przykładzie requireActivity() przyjmuje wartość MainActivity, bo MainActivity hostuje 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);
        ...
    }
    ...
}

Dane użytkownika w usłudze UserViewModel są udostępniane przez interfejs LiveData, dlatego przy podejmowaniu decyzji o nawigacji warto obserwować te dane. Gdy otworzysz aplikację ProfileFragment, aplikacja wyświetli wiadomość powitalną, jeśli zawiera ona dane użytkownika. Jeśli dane użytkownika to null, przejdź do LoginFragment, ponieważ użytkownik musi się uwierzytelnić, aby zobaczyć jego profil. Zdefiniuj logikę decydującą w elemencie ProfileFragment, jak pokazano w tym przykładzie:

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

Jeśli dane użytkownika to null, w momencie, gdy wejdą na stronę ProfileFragment, zostaną przekierowani na stronę LoginFragment.

Możesz użyć parametru NavController.getPreviousBackStackEntry(), aby pobrać parametr NavBackStackEntry dla poprzedniego miejsca docelowego, który zawiera specyficzny dla tego miejsca stan NavController. LoginFragment używa SavedStateHandle poprzedniego elementu NavBackStackEntry do ustawiania wartości początkowej wskazującej, czy użytkownik się zalogował. To stan, który musielibyśmy przywrócić, gdy użytkownik natychmiast naciśnie systemowy przycisk Wstecz. Ustawienie tego stanu za pomocą funkcji SavedStateHandle sprawi, że stan będzie się utrzymywać aż do zakończenia procesu.

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

Gdy użytkownik wpisze nazwę użytkownika i hasło, zostaną one przekazane do UserViewModel w celu uwierzytelnienia. Jeśli uwierzytelnianie się powiedzie, UserViewModel zachowa dane użytkownika. Następnie LoginFragment aktualizuje wartość LOGIN_SUCCESSFUL w SavedStateHandle i wychodzi ze stosu wstecznego.

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

Cała logika uwierzytelniania odbywa się w obrębie UserViewModel. To ważne, ponieważ za określenie sposobu uwierzytelniania użytkowników nie odpowiada LoginFragment ani ProfileFragment. Uwzględnienie logiki w elemencie ViewModel ułatwia nie tylko udostępnianie, ale też testowanie. Jeśli logika nawigacji jest złożona, należy ją zweryfikować podczas testowania. Więcej informacji o strukturze architektury aplikacji na podstawie komponentów możliwych do przetestowania znajdziesz w przewodniku po architekturze aplikacji.

W obiekcie ProfileFragment wartość LOGIN_SUCCESSFUL zapisaną w SavedStateHandle można zaobserwować w metodzie onCreate(). Gdy użytkownik wróci do ProfileFragment, zostanie sprawdzona wartość LOGIN_SUCCESSFUL. Jeśli wartością jest false, użytkownik może zostać przekierowany z powrotem do interfejsu 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);
                    }
                });
    }

    ...
}

Jeśli użytkownik się zalogował, ProfileFragment wyświetli wiadomość powitalną.

Użyta tutaj metoda sprawdzania wyniku pozwala rozróżnić 2 różne przypadki:

  • Początkowy przypadek, w którym użytkownik nie jest zalogowany i powinno być wymagane zalogowanie się.
  • Użytkownik nie jest zalogowany, bo nie chce się logować (występuje to false).

Dzięki rozróżnieniu tych przypadków użycia można uniknąć ciągłego proszenia użytkownika o zalogowanie. Logika biznesowa obsługi przypadków niepowodzenia pozostawiasz Tobie i może obejmować wyświetlenie nakładki wyjaśniającej, dlaczego użytkownik musi się zalogować, dokończyć całą aktywność lub przekierować użytkownika do miejsca docelowego, które nie wymaga logowania, tak jak w poprzednim przykładzie kodu.