כשמעצבים את הניווט באפליקציה, כדאי לנווט אל יעד לעומת אחר על סמך לוגיקה מותנית. לדוגמה, משתמש עשויים ללחוץ על קישור עומק כדי להגיע ליעד שדורש את רישום המשתמש או שיש לכם יעדים שונים במשחק, שבהם השחקן מנצח או מפסיד.
התחברות של משתמשים
בדוגמה הזו, משתמש מנסה לנווט למסך פרופיל שדורש אימות. בגלל שהפעולה הזו דורשת אימות, המשתמש יופנו למסך התחברות אם הם עדיין לא אומתו.
גרף הניווט לדוגמה הזו עשוי להיראות כך:
כדי לבצע אימות, האפליקציה צריכה לעבור אל 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
).
אם תבדילו בין מקרי השימוש האלה, תוכלו להימנע מבקשה חוזרת המשתמש כדי להתחבר. הלוגיקה העסקית לטיפול במקרים של כשלים כבר משאירה לכם היא עשויה לכלול הצגת שכבת-על שמסבירה למה התחברות, סיום כל הפעילות או הפניה אוטומטית של המשתמש ליעד שאינה מחייבת התחברות, כמו שהיה בדוגמת הקוד הקודמת.