عند تصميم التنقل في تطبيقك، قد ترغب في الانتقال إلى وجهة مقابل أخرى بناءً على المنطق الشرطي. على سبيل المثال، قد يتبع المستخدم رابطًا لصفحة معيّنة في وجهة تتطلب أن يسجّل المستخدم دخوله، أو قد تكون لديك وجهات مختلفة في اللعبة تتعلق بالفوز أو الخسارة.
تسجيل دخول المستخدم
في هذا المثال، يحاول المستخدم الانتقال إلى شاشة الملف الشخصي التي تتطلب مصادقة. بما أنّ هذا الإجراء يتطلب المصادقة، يجب إعادة توجيه المستخدم إلى شاشة تسجيل الدخول إذا لم تتم مصادقته من قبل.
قد يبدو الرسم البياني للتنقل في هذا المثال كما يلي:
للمصادقة، يجب أن ينتقل التطبيق إلى 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
).
ومن خلال التمييز بين حالات الاستخدام هذه، يمكنك تجنُّب طلب تسجيل الدخول بشكل متكرّر من المستخدم. يعود إليك منطق العمل في التعامل مع حالات الفشل، وقد يتضمن ذلك عرض تراكب يشرح سبب احتياج المستخدم إلى تسجيل الدخول، أو إنهاء النشاط بالكامل، أو إعادة توجيه المستخدم إلى وجهة لا تتطلب تسجيل الدخول، كما كان الحال في مثال الرمز السابق.