条件式导航

在为您的应用设计导航时,您可能希望根据条件逻辑导航到某个目标,而不是另一个目标。例如,用户可能会通过深层链接导航到需要登录的目标,或者在游戏中,根据玩家输赢可能有不同的目标。

用户登录

在此示例中,用户尝试导航到需要身份验证的个人资料屏幕。由于此操作需要身份验证,如果用户尚未通过身份验证,则应将其重定向到登录屏幕。

此示例的导航图可能如下所示

a login flow is handled independently from the app's main
            navigation flow.
图 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 包含一个按钮,用户可以点击该按钮查看其个人资料。如果用户想查看个人资料屏幕,他们必须首先进行身份验证。此交互使用两个独立的 Fragment 进行建模,但它依赖于共享的用户状态。此状态信息不是这两个 Fragment 中任何一个的职责,更适合保存在共享的 UserViewModel 中。此 ViewModel 通过将其作用域限定到实现 ViewModelStoreOwner 的 Activity 来在 Fragment 之间共享。在以下示例中,requireActivity() 解析为 MainActivity,因为 MainActivity 托管 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);
        ...
    }
    ...
}

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 中。这很重要,因为确定用户如何进行身份验证既不是 LoginFragment 也不是 ProfileFragment 的职责。将您的逻辑封装在 ViewModel 中不仅更容易共享,也更容易测试。如果您的导航逻辑复杂,您尤其应该通过测试来验证此逻辑。有关围绕可测试组件构建应用架构的更多信息,请参阅应用架构指南

回到 ProfileFragment,存储在 SavedStateHandle 中的 LOGIN_SUCCESSFUL 值可以在 onCreate() 方法中进行观察。当用户返回到 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)。

通过区分这些用例,您可以避免反复要求用户登录。处理失败情况的业务逻辑由您决定,可能包括显示一个解释用户为何需要登录的叠加层、完成整个 Activity,或者将用户重定向到不需要登录的目标,如之前的代码示例所示。