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:
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.