Điều hướng có điều kiện

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.
Hình 1. 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.

Để 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 NavControllercụ 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 đó.