Navegação condicional

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:

um fluxo de login é tratado independentemente do fluxo de
            navegação principal do app.
Figura 1. Um fluxo de login é processado de modo independente do fluxo de navegação principal do app.

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.