Cuando diseñas la navegación para tu app, es posible que desees navegar a un destino en lugar de otro según la lógica condicional. Por ejemplo, un usuario podría seguir un vínculo directo a un destino que requiere que este acceda, o bien podrías tener diferentes destinos en un juego para cuando el jugador gane o pierda.
Acceso del usuario
En este ejemplo, un usuario intenta navegar a una pantalla de perfil que requiere autenticación. Debido a que esta acción requiere autenticación, el usuario debe ser redireccionado a una pantalla de acceso si todavía no está autenticado.
El gráfico de navegación de este ejemplo podría verse de la siguiente manera:
Para realizar la autenticación, la app debe navegar a login_fragment
, donde el usuario puede ingresar un nombre de usuario y una contraseña a los efectos de autenticarse. Si se aceptan las credenciales, el usuario vuelve a la pantalla profile_fragment
. Si no se aceptan, se informa al usuario que sus credenciales no son válidas mediante un Snackbar
.
Si el usuario regresa a la pantalla de perfil sin acceder, se lo envía a la pantalla main_fragment
.
A continuación, se muestra el gráfico de navegación para esta app:
<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
contiene un botón en el que el usuario puede hacer clic para ver su perfil.
Si el usuario desea ver la pantalla de perfil, primero debe autenticarse. Esa interacción se modela con dos fragmentos separados, pero depende del estado del usuario compartido. Esa información de estado no es responsabilidad de ninguno de los dos fragmentos y se mantiene más apropiadamente en un UserViewModel
compartido.
Este ViewModel
se comparte entre los fragmentos cuando se determina su alcance en relación con la actividad, que implementa ViewModelStoreOwner
. En el siguiente ejemplo, requireActivity()
se resuelve en MainActivity
porque MainActivity
aloja 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); ... } ... }
Los datos del usuario en UserViewModel
se exponen a través de LiveData
, por lo que, para decidir a dónde navegar, debes observar esos datos. Cuando navegas a ProfileFragment
, la app muestra un mensaje de bienvenida si los datos del usuario están presentes. Si los datos del usuario son null
, navegas a LoginFragment
, ya que el usuario debe autenticarse antes de ver su perfil. Define la lógica de decisión en tu ProfileFragment
, como se muestra en el siguiente ejemplo:
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() { ... } }
Si los datos del usuario son null
cuando alcanzan el ProfileFragment
, se redireccionan al LoginFragment
.
Puedes usar NavController.getPreviousBackStackEntry()
para recuperar el NavBackStackEntry
para el destino anterior, que encapsula el estado específico de NavController
para el destino. LoginFragment
usa el SavedStateHandle
del NavBackStackEntry
anterior para establecer un valor inicial que indica si el usuario accedió con éxito. Este es el estado que queremos mostrar si el usuario presionara de inmediato el botón Atrás del sistema. Configurar este estado mediante SavedStateHandle
garantiza que el estado persista hasta el cierre del proceso.
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); } }
Una vez que el usuario ingresa un nombre de usuario y una contraseña, estos se pasan al UserViewModel
para su autenticación. Si el proceso es exitoso, el objeto UserViewModel
almacena los datos del usuario. Luego, LoginFragment
actualiza el valor LOGIN_SUCCESSFUL
en SavedStateHandle
y se retira de la pila de actividades.
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 } }
Ten en cuenta que toda la lógica relacionada con la autenticación se mantiene dentro de UserViewModel
. Esto es importante, ya que no es responsabilidad de LoginFragment
ni de ProfileFragment
determinar cómo se autentican los usuarios. Encapsular tu lógica en un ViewModel
hace que compartirla y probarla sea más fácil. Si tu lógica de navegación es compleja, debes verificar especialmente esta lógica mediante pruebas. Consulta la Guía de arquitectura de apps para obtener más información relacionada con cómo estructurar la arquitectura de tu app en torno a los componentes que se pueden probar.
En ProfileFragment
, se puede observar el valor LOGIN_SUCCESSFUL
almacenado en SavedStateHandle
en el método onCreate()
. Cuando el usuario vuelva a ProfileFragment
, se verificará el valor LOGIN_SUCCESSFUL
. Si el valor es false
, se podrá redireccionar al usuario de vuelta a 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); } }); } ... }
Si el usuario accedió correctamente, ProfileFragment
mostrará un mensaje de bienvenida.
La técnica que se usa aquí para verificar el resultado te permite distinguir entre dos casos diferentes:
- El caso inicial, en el que el usuario no accedió, y se le debería pedir que lo haga.
- El caso en el que el usuario no accedió porque optó por no hacerlo (un resultado
false
)
Si identificas esos casos de uso, puedes evitar que se le solicite al usuario que acceda en forma reiterada. Tú decides la lógica empresarial para gestionar los casos de falla, la cual podría incluir mostrar una superposición que explique por qué el usuario necesita acceder, finalizar toda la actividad o redireccionar al usuario a un destino para el que no se requiera acceder, como fue el caso del ejemplo de código anterior.