Navegación condicional

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:

un flujo de acceso se controla de manera independiente del flujo de navegación principal de la app.
Figura 1. Un flujo de acceso se controla de manera independiente del flujo de navegación principal de la app.

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.