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

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

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

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

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

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

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