Ao projetar a navegação para o app, você pode precisar navegar de um destino para outro seguindo a lógica condicional. Por exemplo, um usuário pode seguir um link direto para um destino que requer que o usuário faça login ou você pode ter destinos diferentes em um jogo para quando o jogador ganha ou perde.
Login do usuário
Neste exemplo, um usuário tenta navegar para uma tela de perfil que requer autenticação. Como essa ação requer autenticação, o usuário precisa ser redirecionado para uma tela de login se ainda não estiver autenticado.
O gráfico de navegação para este exemplo pode ser parecido com este:
Para fazer a autenticação, o app precisa navegar para login_fragment
, onde o usuário
pode inserir um nome de usuário e uma senha. Se aceito, o usuário será
enviado de volta para a tela profile_fragment
. Se não for aceito, o usuário será
informado de que as credenciais são inválidas usando
Snackbar
.
Se o usuário navegar de volta para a tela do perfil sem fazer login, ele será
enviado para a tela main_fragment
.
Veja o gráfico de navegação deste 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
contém um botão no qual o usuário pode clicar para visualizar seu perfil.
Se o usuário quiser ver a tela do perfil, primeiro ele precisa ser autenticado. Essa
interação é modelada usando dois fragmentos separados, mas isso depende do estado compartilhado
do usuário. Essas informações de estado não são de responsabilidade de um
desses dois fragmentos e são mantidas mais apropriadamente em um UserViewModel
compartilhado.
Esse ViewModel
é compartilhado entre os fragmentos, juntando-o à atividade,
que implementa ViewModelStoreOwner
. No exemplo a seguir,
requireActivity()
resolve para MainActivity
porque MainActivity
hospeda
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); ... } ... }
Os dados do usuário no UserViewModel
são expostos por LiveData
. Assim, para decidir onde
navegar, você precisa observar esses dados. Ao navegar para
ProfileFragment
, o app exibirá uma mensagem de boas-vindas se os dados do usuário estiverem
presentes. Se os dados do usuário forem null
, você navegará para LoginFragment
,
já que o usuário precisa se autenticar antes de ver o perfil deles. Defina a
lógica de declínio no ProfileFragment
, como mostrado no exemplo a seguir:
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() { ... } }
Se os dados do usuário forem null
quando chegarem ao ProfileFragment
, eles serão
redirecionados para LoginFragment
.
Você pode usar
NavController.getPreviousBackStackEntry()
para recuperar a NavBackStackEntry
do destino anterior, que encapsula o estado específico do NavController
para o destino. LoginFragment
usa o
SavedStateHandle
do
NavBackStackEntry
anterior para definir um valor inicial que indica se o
usuário fez login. Esse é o estado que queremos retornar se
o usuário pressionar o botão "Voltar" do sistema imediatamente. Definir esse estado
usando SavedStateHandle
garante que o estado persista ao encerramento do processo.
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); } }
Depois que o usuário insere um nome de usuário e uma senha, eles são transmitidos para
UserViewModel
para autenticação. Se a autenticação for bem-sucedida, o
UserViewModel
armazenará os dados do usuário. Em seguida, o LoginFragment
atualiza o valor
LOGIN_SUCCESSFUL
no SavedStateHandle
e sai
da pilha de retorno.
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 } }
Observe que toda a lógica relacionada à autenticação será salva no
UserViewModel
. Isso é importante, já que não é responsabilidade de
LoginFragment
ou ProfileFragment
determinar como os usuários são
autenticados. O encapsulamento da lógica em um ViewModel
facilita não só
o compartilhamento, mas também os testes. Se a lógica de navegação for complexa,
verifique-a especialmente por meio de testes. Consulte o
Guia para a arquitetura do app para ver mais informações sobre como
estruturar a arquitetura do app com componentes testáveis.
De volta ao ProfileFragment
, o valor LOGIN_SUCCESSFUL
armazenado em
SavedStateHandle
pode ser observado no método
onCreate()
.
Quando o usuário retornar para ProfileFragment
, o valor LOGIN_SUCCESSFUL
será verificado. Se o valor for false
, o usuário poderá ser redirecionado de volta
para o 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); } }); } ... }
Se o usuário fizer login, ProfileFragment
exibirá uma
mensagem de boas-vindas.
A técnica usada aqui para verificar o resultado permite distinguir entre dois casos diferentes:
- O caso inicial, em que o usuário não está conectado e precisa ser solicitado a fazer login.
- O usuário não está conectado porque optou por não fazer login (resultado de
false
).
Ao diferenciar esses casos de uso, é possível evitar que o usuário faça login repetidamente. Você decide a lógica de negócios para lidar com casos de falha. Isso pode incluir, por exemplo, a exibição de uma sobreposição que explica por que o usuário precisa fazer login, o encerramento de toda a atividade ou o redirecionamento do usuário a um destino que não exija login, como foi o caso no exemplo de código anterior.