條件式導覽

設計應用程式導覽時,您可能會想根據條件邏輯前往其中一個到達網頁而非另一個到達網頁。例如,使用者可能會點選導向某個到達網頁的深層連結,而該到達網頁會要求使用者登入。或者,您也可能會在遊戲中針對玩家的輸贏提供不同的到達網頁。

使用者登入

在此範例中,使用者嘗試前往需要身分驗證的設定檔畫面。由於這個動作需要進行身分驗證,使用者如果尚未通過身分驗證,系統會將其重新導向至登入畫面。

這個範例的導覽圖大致如下:

登入流程的處理獨立於應用程式的主要導覽流程之外。
圖 1. 登入流程的處理獨立於應用程式的主要導覽流程之外。

如要進行身分驗證,應用程式必須前往 login_fragment,使用者可在此輸入使用者名稱和密碼進行身分驗證。如果通過,系統會將使用者送回 profile_fragment 畫面。如果未通過,系統會透過 Snackbar 通知使用者身分驗證資訊無效。如果使用者未登入就返回個人資料畫面,系統會將他們導向 main_fragment 畫面。

以下是這個應用程式的導覽圖:

<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 包含一個按鈕,使用者可以點按,以查看個人資料。如果使用者想看到個人資料畫面,必須先進行身分驗證。此互動是使用兩個單獨片段進行模擬,但取決於共用的使用者狀態。這兩個片段皆不負責保留這個狀態資訊,而是將其妥善保管在共用 UserViewModel 中。在片段之間共用這個 ViewModel 的方法是將其範圍限定為導入 ViewModelStoreOwner 的活動。在以下範例中,由於 MainActivity 代管 ProfileFragment,因此 requireActivity() 解析為 MainActivity

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

UserViewModel 中的使用者資料透過 LiveData 公開,因此,如要判斷要前往的位置,請觀察這項資料。前往 ProfileFragment 後,如果使用者資料存在,應用程式會顯示歡迎訊息。如果使用者資料為 null,就必須前往 LoginFragment,因為使用者需要先進行身分驗證,才能查看個人資料。您可以在 ProfileFragment 中定義決定邏輯,如以下範例所示:

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

當使用者觸及 ProfileFragment 時,若使用者資料為 null,系統會將其重新導向至 LoginFragment

您可以使用 NavController.getPreviousBackStackEntry() 擷取上一個到達網頁的 NavBackStackEntry,系統會封裝到達網頁的 NavController 特定狀態。LoginFragment 會使用上一個 NavBackStackEntrySavedStateHandle 設定初始值,指示使用者是否已成功登入。這是我們希望使用者立即按下系統返回按鈕後所傳回的狀態。使用 SavedStateHandle 設定這個狀態,可確保狀態在程序終止期間持續存在。

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

使用者輸入使用者名稱和密碼後,系統會將資料傳遞至 UserViewModel 進行身分驗證。如果身分驗證成功,UserViewModel 就會儲存使用者資料。LoginFragment 隨後會更新 SavedStateHandle 上的 LOGIN_SUCCESSFUL 值,並自動從返回堆疊中彈出。

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

請注意,與身分驗證相關的所有邏輯都會保留在 UserViewModel 中。這一點非常重要,因為 LoginFragmentProfileFragment 皆不負責判斷使用者的身分驗證方式。將邏輯封裝在 ViewModel 中,您不但可更輕鬆地共用檔案,還能更輕鬆地進行測試。如果您的導覽邏輯錯綜複雜,建議您以測試方式來驗證這個邏輯。請參閱 應用程式架構指南,進一步瞭解如何根據可測試元件建構應用程式的架構。

返回 ProfileFragment中,您可以在 onCreate() 方法中觀察儲存在 SavedStateHandle 中的 LOGIN_SUCCESSFUL 值。當使用者返回 ProfileFragment 時,系統會檢查 LOGIN_SUCCESSFUL 值。如果值為 false,系統會將使用者重新導向回 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);
                    }
                });
    }

    ...
}

如果使用者成功登入,ProfileFragment 會顯示歡迎訊息。

這裡使用的查看結果技巧,讓您能夠區分兩個不同的情況:

  • 初始情況,使用者未登入,系統應要求使用者登入。
  • 使用者未登入,因為「使用者選擇不登入」 (結果為 false)。

藉由區分這些用途,可避免重複要求使用者登入。處理失敗情況的商業邏輯由您決定,其中可能包含重疊顯示,說明為什麼使用者需要登入、完成整個活動,或將使用者重新導向至不需要登入的到達網頁原因,正如前一個程式碼範例所示。