Navigazione condizionale

Quando progetti la navigazione per la tua app, potresti voler raggiungere una destinazione piuttosto che un'altra in base alla logica condizionale. Ad esempio, un utente potrebbe seguire un link diretto a una destinazione che richiede l'accesso oppure potrebbero essere diverse destinazioni di un gioco in cui il giocatore vince o perde.

Accesso utente

In questo esempio, un utente tenta di accedere a una schermata del profilo che richiede l'autenticazione. Poiché questa azione richiede l'autenticazione, l'utente dovrebbe essere reindirizzato a una schermata di accesso se non è già stato autenticato.

Il grafico di navigazione di questo esempio potrebbe avere il seguente aspetto:

un flusso di accesso viene gestito indipendentemente dal flusso di navigazione
            principale dell'app.
Figura 1. Un flusso di accesso viene gestito in modo indipendente dal flusso di navigazione principale dell'app.

Per l'autenticazione, l'app deve accedere a login_fragment, dove l'utente può inserire nome utente e password per l'autenticazione. Se viene accettato, l'utente viene reindirizzato alla schermata profile_fragment. Se non viene accettato, l'utente viene informato del fatto che le sue credenziali non sono valide utilizzando Snackbar. Se l'utente torna alla schermata del profilo senza eseguire l'accesso, viene indirizzato alla schermata main_fragment.

Ecco il grafico di navigazione per questa 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 pulsante su cui l'utente può fare clic per visualizzare il profilo. Se l'utente vuole vedere la schermata del profilo, deve prima eseguire l'autenticazione. Questa interazione è modellata utilizzando due frammenti separati, ma dipende dallo stato dell'utente condiviso. Queste informazioni sullo stato non sono di competenza di nessuno di questi due frammenti e sono più adeguatamente conservate in un UserViewModel condiviso. Questo ViewModel viene condiviso tra i frammenti definendolo come ambito l'attività, che implementa ViewModelStoreOwner. Nell'esempio seguente, requireActivity() restituisce MainActivity, perché MainActivity host 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);
        ...
    }
    ...
}

I dati utente in UserViewModel vengono esposti tramite LiveData, quindi per decidere dove navigare devi osservare questi dati. Al momento dell'accesso a ProfileFragment, l'app mostra un messaggio di benvenuto, se sono presenti dati utente. Se i dati utente sono null, accedi a LoginFragment, poiché l'utente deve eseguire l'autenticazione per poter visualizzare il profilo. Definisci la logica decisionale in ProfileFragment, come mostrato nell'esempio seguente:

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 i dati utente sono null quando raggiungono ProfileFragment, verranno reindirizzati a LoginFragment.

Puoi utilizzare NavController.getPreviousBackStackEntry() per recuperare il NavBackStackEntry per la destinazione precedente, che incapsula lo stato specifico di NavController per la destinazione. LoginFragment utilizza SavedStateHandle dei NavBackStackEntry precedenti per impostare un valore iniziale che indichi se l'utente ha eseguito l'accesso. Questo è lo stato che vorremmo restituire se l'utente dovesse premere immediatamente il pulsante Indietro del sistema. L'impostazione di questo stato utilizzando SavedStateHandle assicura che lo stato permanga fino al termine del 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);
    }
}

Dopo che l'utente inserisce un nome utente e una password, questi vengono trasmessi a UserViewModel per l'autenticazione. Se l'autenticazione viene completata correttamente, UserViewModel archivia i dati utente. LoginFragment quindi aggiorna il valore LOGIN_SUCCESSFUL su SavedStateHandle ed esce dallo stack back.

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
    }
}

Tieni presente che tutta la logica relativa all'autenticazione viene conservata all'interno di UserViewModel. Questo è importante, poiché non è responsabilità di LoginFragment o ProfileFragment determinare il modo in cui gli utenti vengono autenticati. L'incapsulamento della logica in un ViewModel semplifica non solo la condivisione, ma anche i test. Se la logica di navigazione è complessa, dovresti verificarla soprattutto tramite test. Consulta la Guida all'architettura dell'app per ulteriori informazioni su come strutturare l'architettura della tua app in base ai componenti testabili.

Tornando in ProfileFragment, il valore di LOGIN_SUCCESSFUL archiviato in SavedStateHandle può essere osservato nel metodo onCreate(). Quando l'utente torna a ProfileFragment, verrà controllato il valore LOGIN_SUCCESSFUL. Se il valore è false, l'utente può essere reindirizzato 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);
                    }
                });
    }

    ...
}

Se l'utente ha eseguito l'accesso, ProfileFragment mostra un messaggio di benvenuto.

La tecnica utilizzata qui per controllare i risultati ti consente di distinguere tra due diversi casi:

  • Il caso iniziale, in cui l'utente non ha eseguito l'accesso e dovrebbe essere richiesto di eseguire l'accesso.
  • L'utente non ha eseguito l'accesso perché ha scelto di non eseguire l'accesso (in seguito a false).

Individuando questi casi d'uso, puoi evitare di chiedere ripetutamente all'utente di eseguire l'accesso. La logica di business per la gestione dei casi di errore è lasciata a te e potrebbe includere la visualizzazione di un overlay che spiega perché l'utente deve eseguire l'accesso, completare l'intera attività o reindirizzare l'utente a una destinazione che non richiede l'accesso, come nel caso dell'esempio di codice precedente.