التنقّل المشروط

عند تصميم التنقل في تطبيقك، قد ترغب في الانتقال إلى وجهة مقابل أخرى بناءً على المنطق الشرطي. على سبيل المثال، قد يتبع المستخدم رابطًا لصفحة معيّنة في وجهة تتطلب أن يسجّل المستخدم دخوله، أو قد تكون لديك وجهات مختلفة في اللعبة تتعلق بالفوز أو الخسارة.

تسجيل دخول المستخدم

في هذا المثال، يحاول المستخدم الانتقال إلى شاشة الملف الشخصي التي تتطلب مصادقة. بما أنّ هذا الإجراء يتطلب المصادقة، يجب إعادة توجيه المستخدم إلى شاشة تسجيل الدخول إذا لم تتم مصادقته من قبل.

قد يبدو الرسم البياني للتنقل في هذا المثال كما يلي:

يتم التعامل مع تدفق تسجيل الدخول بشكل مستقل عن مسار التنقل الرئيسي
            في التطبيق.
الشكل 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. في المثال التالي، تتم مطابقة 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() {
        ...
    }
}

وإذا كانت بيانات المستخدم هي null عند وصولها إلى ProfileFragment، سيُعاد توجيهها إلى LoginFragment.

يمكنك استخدام NavController.getPreviousBackStackEntry() لاسترداد NavBackStackEntry للوجهة السابقة التي تضم حالة NavController الخاصة للوجهة. يستخدم LoginFragment السمة SavedStateHandle الخاصة بالسمة NavBackStackEntry السابقة لضبط قيمة أولية تشير إلى ما إذا كان المستخدم قد سجّل الدخول بنجاح. هذه هي الحالة التي نريد إعادتها إذا ضغط المستخدم فورًا على زر الرجوع في النظام. يؤدي ضبط هذه الحالة باستخدام 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 القيمة LOGIN_SUCCESSFUL على 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)

        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، يمكن ملاحظة القيمة LOGIN_SUCCESSFUL المخزّنة في SavedStateHandle من خلال طريقة 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).

ومن خلال التمييز بين حالات الاستخدام هذه، يمكنك تجنُّب طلب تسجيل الدخول بشكل متكرّر من المستخدم. يعود إليك منطق العمل في التعامل مع حالات الفشل، وقد يتضمن ذلك عرض تراكب يشرح سبب احتياج المستخدم إلى تسجيل الدخول، أو إنهاء النشاط بالكامل، أو إعادة توجيه المستخدم إلى وجهة لا تتطلب تسجيل الدخول، كما كان الحال في مثال الرمز السابق.