ניווט מותנה

כשמעצבים את הניווט באפליקציה, כדאי לנווט אל יעד לעומת אחר על סמך לוגיקה מותנית. לדוגמה, משתמש עשויים ללחוץ על קישור עומק כדי להגיע ליעד שדורש את רישום המשתמש או שיש לכם יעדים שונים במשחק, שבהם השחקן מנצח או מפסיד.

התחברות של משתמשים

בדוגמה הזו, משתמש מנסה לנווט למסך פרופיל שדורש אימות. בגלל שהפעולה הזו דורשת אימות, המשתמש יופנו למסך התחברות אם הם עדיין לא אומתו.

גרף הניווט לדוגמה הזו עשוי להיראות כך:

תהליך ההתחברות מטופל בנפרד
            .
איור 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).

אם תבדילו בין מקרי השימוש האלה, תוכלו להימנע מבקשה חוזרת המשתמש כדי להתחבר. הלוגיקה העסקית לטיפול במקרים של כשלים כבר משאירה לכם היא עשויה לכלול הצגת שכבת-על שמסבירה למה התחברות, סיום כל הפעילות או הפניה אוטומטית של המשתמש ליעד שאינה מחייבת התחברות, כמו שהיה בדוגמת הקוד הקודמת.