Navigazione condizionale

Quando progetti la navigazione per la tua app, potrebbe essere utile aprire una di destinazione rispetto a un'altra basata sulla logica condizionale. Ad esempio, un utente potrebbe seguire un link diretto a una destinazione che richiede l'accesso dell'utente oppure potresti avere destinazioni diverse in un gioco per quando vittorie o sconfitte.

Accesso utente

In questo esempio, un utente tenta di accedere alla schermata di un profilo che richiede autenticazione. Poiché questa azione richiede l'autenticazione, l'utente deve verranno reindirizzati a una schermata di accesso, se non sono già autenticati.

Il grafico di navigazione per questo esempio potrebbe avere un aspetto simile al seguente:

un flusso di accesso sia gestito in modo indipendente rispetto alla
            durante la navigazione.
Figura 1. Un flusso di accesso viene gestito in modo indipendente flusso di navigazione principale dell'app.

Per autenticarsi, l'app deve accedere al login_fragment, dove l'utente inserire un nome utente e una password per l'autenticazione. Se accettato, l'utente viene rimandato alla schermata profile_fragment. Se non viene accettato, l'utente viene informato che le sue credenziali non sono valide Snackbar Se l'utente torna alla schermata del profilo senza effettuare l'accesso, inviati 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 proprio profilo. Se l'utente vuole vedere la schermata del profilo, deve prima eseguire l'autenticazione. Questo l'interazione viene modellata utilizzando due frammenti separati, ma dipende lo stato dell'utente. Queste informazioni sullo stato non sono di responsabilità di nessuno questi due frammenti ed è più appropriatamente conservato in un elemento UserViewModel condiviso. Questo ViewModel viene condiviso tra i frammenti scegliendo come ambito l'attività, che implementa ViewModelStoreOwner. Nell'esempio seguente, requireActivity() si risolve in MainActivity, perché MainActivity ospita 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 per navigare, devi osservare questi dati. Durante la navigazione verso ProfileFragment, l'app mostra un messaggio di benvenuto se i dati utente sono presenti. Se i dati utente sono null, accedi a LoginFragment, perché l'utente deve autenticarsi prima di vedere il proprio profilo. Definisci i logica di decisione 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, reindirizzato al sito LoginFragment.

Puoi utilizzare NavController.getPreviousBackStackEntry() per recuperare NavBackStackEntry per la destinazione precedente, che incapsula lo stato specifico di NavController per la destinazione. LoginFragment utilizza SavedStateHandle del NavBackStackEntry precedente per impostare un valore iniziale che indica se utente ha eseguito l'accesso. Questo è lo stato che vorremmo restituire se l'utente doveva premere immediatamente il pulsante Indietro del sistema. Impostazione di questo stato l'utilizzo di SavedStateHandle garantisce che lo stato permanga anche con la morte 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);
    }
}

Una volta che l'utente inserisce un nome utente e una password, questi vengono passati alla UserViewModel per l'autenticazione. Se l'autenticazione ha esito positivo, UserViewModel memorizza i dati utente. LoginFragment aggiorna quindi LOGIN_SUCCESSFUL su SavedStateHandle e si distingue da lo stack posteriore.

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 si trova all'interno UserViewModel. Questo è importante, in quanto non è responsabilità LoginFragment o ProfileFragment per determinare in che modo gli utenti autenticati. L'incapsulamento della logica in un ViewModel lo rende non solo più facile da condividere, ma anche più facile da testare. Se la logica di navigazione è complessa, dovresti verificare questa logica tramite test. Consulta le Guida all'architettura delle app per ulteriori informazioni su per strutturare l'architettura dell'app in base a componenti testabili.

Tornando a ProfileFragment, il valore LOGIN_SUCCESSFUL memorizzato nella SavedStateHandle può essere osservato onCreate() . Quando l'utente torna alla ProfileFragment, LOGIN_SUCCESSFUL verrà controllato. Se il valore è false, l'utente può essere reindirizzato in 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 verificare il risultato consente di distinguere tra due casi diversi:

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

Distribuendo questi casi d'uso, puoi evitare di chiedere ripetutamente l'utente deve effettuare l'accesso. La logica di business per la gestione dei casi di errore è a tua disposizione e potrebbe includere la visualizzazione di un overlay che spiega perché l'utente deve accedere, terminare l'attività o reindirizzare l'utente a una destinazione che non richiedono l'accesso, come nell'esempio di codice precedente.