Khi thiết kế việc điều hướng trong ứng dụng, bạn nên điều hướng tới một đích đến so với đích đến khác dựa trên logic có điều kiện. Ví dụ: người dùng có thể đi theo liên kết sâu tới một đích đến yêu cầu người dùng phải đăng nhập, hoặc bạn có thể có nhiều đích đến trong một trò chơi khi người chơi giành chiến thắng hoặc thua.
Đăng nhập người dùng
Trong ví dụ này, người dùng cố gắng chuyển đến màn hình hồ sơ nơi yêu cầu xác thực. Do hành động này đòi hỏi xác thực nên người dùng phải được chuyển hướng đến màn hình đăng nhập nếu họ chưa được xác thực.
Biểu đồ điều hướng cho ví dụ này có thể giống như sau:
![quy trình đăng nhập được xử lý độc lập với quy trình điều hướng chính của ứng dụng.](https://developer.android.google.cn/static/images/guide/navigation/navigation-conditional-login.png?authuser=9&hl=vi)
Để xác thực, ứng dụng phải chuyển đến login_fragment
, tại đó người dùng có thể nhập tên người dùng và mật khẩu để xác thực. Nếu được chấp nhận, người dùng sẽ được chuyển về màn hình profile_fragment
. Nếu không được chấp nhận, người dùng sẽ nhận được thông báo thông tin đăng nhập của họ không hợp lệ bằng cách sử dụng Snackbar
.
Nếu người dùng quay lại màn hình hồ sơ mà không đăng nhập, họ sẽ được chuyển đến màn hình main_fragment
.
Dưới đây là biểu đồ điều hướng cho ứng dụng này:
<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
chứa nút người dùng có thể nhấp để xem hồ sơ của họ.
Nếu người dùng muốn xem màn hình hồ sơ, trước tiên họ phải xác thực. Hành động tương tác này được mô hình hóa bằng cách sử dụng hai phân mảnh riêng biệt nhưng phụ thuộc vào trạng thái người dùng được chia sẻ. Thông tin về trạng thái không thuộc trách nhiệm của một trong hai phân mảnh này và được giữ ở vị trí phù hợp hơn trong một UserViewModel
chung.
ViewModel
này được chia sẻ giữa các phân mảnh bằng cách đối chiếu nó với hoạt động triển khai ViewModelStoreOwner
. Trong ví dụ sau, requireActivity()
phân giải thành MainActivity
do MainActivity
lưu trữ 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); ... } ... }
Dữ liệu người dùng trong UserViewModel
sẽ bị tiết lộ qua LiveData
. Vì vậy, để quyết định vị trí điều hướng, bạn nên quan sát dữ liệu này. Khi chuyển đến ProfileFragment
, ứng dụng sẽ hiển thị thông điệp chào mừng nếu có dữ liệu người dùng. Nếu dữ liệu người dùng là null
, bạn phải chuyển đến LoginFragment
do người dùng cần phải xác thực trước khi được xem hồ sơ của mình. Hãy xác định logic có ý nghĩa quyết định trong ProfileFragment
của bạn, như minh họa trong ví dụ sau:
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() { ... } }
Nếu dữ liệu người dùng là null
khi họ truy cập vào ProfileFragment
, họ sẽ được chuyển hướng đến LoginFragment
.
Bạn có thể sử dụng NavController.getPreviousBackStackEntry()
để truy xuất NavBackStackEntry
cho đích đến trước đó nơi đóng gói trạng thái NavController
cụ thể cho đích đến. LoginFragment
sử dụng SavedStateHandle
của NavBackStackEntry
trước đó để đặt giá trị ban đầu cho biết liệu người dùng đã đăng nhập thành công hay chưa. Đây là trạng thái mà chúng ta muốn trả về nếu người dùng nhấn ngay vào nút quay lại hệ thống. Việc đặt trạng thái này bằng cách sử dụng SavedStateHandle
sẽ đảm bảo trạng thái vẫn tồn tại trong quá trình bị buộc tắt.
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); } }
Sau khi người dùng nhập tên người dùng và mật khẩu, họ sẽ được chuyển tới UserViewModel
để xác thực. Nếu xác thực thành công, UserViewModel
sẽ lưu trữ dữ liệu người dùng. Sau đó, LoginFragment
sẽ cập nhật giá trị LOGIN_SUCCESSFUL
trên SavedStateHandle
và tự tắt khỏi ngăn xếp lui.
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 } }
Lưu ý tất cả các logic liên quan đến việc xác thực đều được lưu giữ trong UserViewModel
. Điều này rất quan trọng bởi LoginFragment
hoặc ProfileFragment
không có trách nhiệm xác định cách người dùng xác thực. Việc đóng gói logic trong ViewModel
không chỉ giúp bạn dễ dàng chia sẻ hơn mà còn dễ dàng kiểm thử hơn. Nếu logic điều hướng phức tạp, bạn nên đặc biệt xác minh logic này thông qua kiểm thử. Xem Hướng dẫn về cấu trúc ứng dụng để biết thêm thông tin về cách xây dựng kiến trúc của ứng dụng xung quanh thành phần có thể kiểm thử.
Quay lại ProfileFragment
, bạn có thể quan sát giá trị LOGIN_SUCCESSFUL
lưu trữ trong SavedStateHandle
trong phương thức onCreate()
. Khi người dùng quay lại ProfileFragment
, hệ thống sẽ kiểm tra giá trị LOGIN_SUCCESSFUL
. Nếu giá trị là false
, người dùng có thể được chuyển hướng trở lại 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); } }); } ... }
Nếu người dùng đăng nhập thành công, ProfileFragment
sẽ hiển thị thông điệp chào mừng.
Kỹ thuật được sử dụng tại đây để kiểm tra kết quả này cho phép bạn phân biệt giữa hai trường hợp:
- Trường hợp ban đầu khi người dùng chưa đăng nhập và nên được yêu cầu đăng nhập.
- Người dùng chưa đăng nhập vì họ đã chọn không đăng nhập (do
false
).
Bằng cách phân biệt các trường hợp sử dụng này, bạn có thể tránh liên tục yêu cầu người dùng đăng nhập. Logic kinh doanh để xử lý các trường hợp không thành công là tùy vào bạn và có thể bao gồm việc hiển thị một lớp phủ giải thích lý do tại sao người dùng cần đăng nhập, hoàn tất toàn bộ hoạt động hoặc chuyển hướng người dùng đến một đích đến không yêu cầu đăng nhập, như trong ví dụ về mã trước đó.