При разработке навигации для вашего приложения вы можете захотеть перейти к одному пункту назначения по сравнению с другим на основе условной логики. Например, пользователь может перейти по ссылке на контент, для просмотра которого требуется войти в аккаунт, или в игре могут быть разные целевые страницы для победы и поражения.
Вход пользователя
В этом примере пользователь пытается перейти на экран профиля, для которого требуется аутентификация. Поскольку это действие требует аутентификации, пользователь должен быть перенаправлен на экран входа, если он ещё не вошел в систему.
Граф навигации для этого примера может выглядеть примерно так:
Для аутентификации приложение должно перейти на страницу 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.
Чтобы получить NavBackStackEntry для предыдущего пункта назначения, можно использовать NavController.getPreviousBackStackEntry(). Это позволит инкапсулировать состояние пункта назначения, относящееся к 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).
Различая эти варианты использования, вы можете избежать повторных запросов на вход пользователя. Бизнес-логика обработки ошибок остается на ваше усмотрение. Например, вы можете показать оверлей с объяснением, почему пользователю нужно войти в аккаунт, завершить все действия или перенаправить пользователя на страницу, где вход не требуется, как в предыдущем примере кода.