پیمایش شرطی

هنگام طراحی پیمایش برای برنامه‌تان، ممکن است بخواهید براساس منطق شرطی به یک مقصد درمقابل مقصد دیگر پیمایش کنید. برای مثال، کاربر ممکن است پیوند عمیقی را به مقصدی دنبال کند که کاربر را ملزم به ورود به سیستم می‌کند، یا ممکن است مقصدهای متفاوتی در بازی برای زمانی که بازیکن برنده یا بازنده می‌شود داشته باشید.

ورود به سیستم کاربر

در این مثال، کاربر تلاش می‌کند به صفحه نمایه‌ای پیمایش کند که به اصالت‌سنجی نیاز دارد. چون این کنش به اصالت‌سنجی نیاز دارد، کاربر باید به صفحه ورود به سیستم هدایت شود اگر ازقبل اصالت‌سنجی نشده باشد.

نمودار پیمایش برای این مثال ممکن است چیزی شبیه به این باشد:

جریان ورود به سیستم مستقل از جریان پیمایش اصلی برنامه مدیریت می‌شود.
شکل ۱. جریان ورود به سیستم مستقل از جریان پیمایش اصلی برنامه مدیریت می‌شود.

برای اصالت‌سنجی، برنامه باید به 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 را پیاده‌سازی می‌کند، بین قطعه‌ها هم‌رسانی می‌شود. در مثال زیر، requireActivity() به MainActivity تبدیل می‌شود، زیرا MainActivity میزبان ProfileFragment است:

کاتلین

class ProfileFragment : Fragment() {
    private val userViewModel: UserViewModel by activityViewModels()
    ...
}

جاوا

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 خود تعریف کنید، همان‌طور که در مثال زیر نشان داده شده است:

کاتلین

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

جاوا

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 از SavedStateHandle NavBackStackEntry قبلی برای تنظیم مقدار اولیه استفاده می‌کند که نشان می‌دهد آیا کاربر با موفقیت وارد سیستم شده است یا نه. این وضعیتی است که می‌خواهیم درصورت فشار دادن فوری دکمه برگشت سیستم توسط کاربر، برگردانده شود. تنظیم این حالت بااستفاده از SavedStateHandle تضمین می‌کند که حالت درطول مرگ پردازش حفظ می‌شود.

کاتلین

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

جاوا

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 مقدار LOGIN_SUCCESSFUL را در SavedStateHandle به‌روز می‌کند و خودش را از پشته قبلی‌ها خارج می‌کند.

کاتلین

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

جاوا

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، مقدار LOGIN_SUCCESSFUL ذخیره‌شده در SavedStateHandle را می‌توان در روش onCreate() مشاهده کرد. وقتی کاربر به ProfileFragment برمی‌گردد، مقدار LOGIN_SUCCESSFUL بررسی می‌شود. اگر مقدار false باشد، کاربر می‌تواند به MainFragment هدایت شود.

کاتلین

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

    ...
}

جاوا

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

با تشخیص این موارد استفاده، می‌توانید از درخواست مکرر از کاربر برای ورود به سیستم جلوگیری کنید. منطق کسب‌وکار برای رسیدگی به موارد ناموفق به شما واگذار شده است و ممکن است شامل نمایش هم‌پوشانی‌ای باشد که توضیح می‌دهد چرا کاربر باید به سیستم وارد شود، کل فعالیت را تکمیل کند، یا کاربر را به مقصدی هدایت کند که نیازی به ورود به سیستم ندارد، همان‌طور که در مثال کد قبلی نشان داده شد.