Dopo aver esaminato vari repository e siti Internet, alla fine abbiamo trovato la soluzione che ti mostreremo ora.
Soluzione:
TL;DR: salta al MOSTRAMI GIÀ I PASSI!!! sezione
Questo è il comportamento normale dei frammenti. Dovrebbero essere ricreati ogni volta che vengono rimossi o sostituiti e si dovrebbe ripristinare il loro stato usando onSaveInstanceState
.
Ecco un bell'articolo che descrive come farlo: Salvare gli stati dei frammenti
Oltre a questo, si possono usare i View Model, che fanno parte della seguente architettura android raccomandata. Sono un ottimo modo per conservare e ripristinare i dati dell'interfaccia utente.
Si può imparare a implementare questa architettura seguendo questo laboratorio di codice passo per passo
EDIT : Soluzione
Ci è voluto un po', ma eccola qui. La soluzione non utilizza ViewModels
al momento.
Leggete attentamente perché ogni passo è importante. Questa soluzione copre le due parti seguenti
- Implementare una navigazione corretta alla pressione del tasto indietro
- Mantenere vivo il frammento durante la navigazione
Sfondo :
Il componente di navigazione di Android fornisce un NavController
che si usa per navigare tra diverse destinazioni. Internamente NavController
utilizza una classe Navigator
che esegue effettivamente la navigazione. Navigator
è una classe astratta e chiunque può estendere/ereditare questa classe per creare il proprio navigatore personalizzato, in modo da fornire una navigazione personalizzata a seconda del tipo di destinazione. Quando si usano i frammenti come destinazioni, la classe NavHostFragment
utilizza una classe FragmentNavigator
la cui implementazione predefinita sostituisce i frammenti ogni volta che si naviga usando il metodo FragmentTransaction.replace()
che distrugge completamente il frammento precedente e ne aggiunge uno nuovo. Quindi dobbiamo creare un nostro navigatore e invece di usare FragmentTransaction.replace()
useremo una combinazione di FragmentTransaction.hide()
e FragmentTransaction.show()
per evitare che il frammento venga distrutto.
Comportamento predefinito dell'interfaccia di navigazione :
Per impostazione predefinita, ogni volta che si naviga verso qualsiasi altro frammento diverso da quello di partenza non viene aggiunto al backstack, quindi se si selezionano frammenti nel seguente ordine
A -> B -> C -> D -> E
il backstack avrà solo
[A, E]
Come si può vedere, i frammenti B, C e D non sono stati aggiunti al backstack, quindi premendo back si arriva sempre al frammento A, che è il frammento iniziale.
Il comportamento che vogliamo per ora:
Vogliamo un comportamento semplice ma efficace. Non vogliamo che tutti i frammenti vengano aggiunti al backstack, ma se il frammento è già nel backstack vogliamo che tutti i frammenti vengano aggiunti fino al frammento selezionato.
Supponiamo di selezionare i frammenti nel seguente ordine
A -> B -> C -> D -> E
anche il backstack dovrebbe essere
[A, B, C, D, E]
premendo back solo l'ultimo frammento dovrebbe essere estratto e il backstack dovrebbe essere come questo
[A, B, C, D]
ma se navighiamo verso, per esempio, il frammento B, dato che B è già nello stack, tutti i frammenti sopra B dovrebbero essere estratti e il nostro backstack dovrebbe apparire come questo
[A, B]
Spero che questo comportamento abbia senso. Questo comportamento è facile da implementare usando le azioni globali, come si vedrà di seguito, ed è migliore di quello predefinito.
OK Hotshot! E ora?
Ora abbiamo due opzioni
- estendere
FragmentNavigator
- copia/incolla
FragmentNavigator
Personalmente volevo solo estendere FragmentNavigator
e sovrascrivere navigate()
ma dato che tutte le sue variabili membro sono private, non ho potuto implementare una navigazione adeguata.
Quindi ho deciso di copiare e incollare l'intero metodo FragmentNavigator
e cambiare il nome dell'intero codice da "FragmentNavigator" a qualsiasi nome io voglia dare.
MOSTRAMI GIÀ I PASSI!!! :
- Creare un navigatore personalizzato
- Utilizzare il tag personalizzato
- Aggiungere azioni globali
- Usa azioni globali
- Aggiungere il navigatore personalizzato al NavController
FASE 1: Creare un navigatore personalizzato
Ecco il mio navigatore personalizzato chiamato StickyCustomNavigator
. Tutto il codice è uguale a quello di FragmentNavigator
tranne che per l'opzione navigate()
. Come si può vedere, utilizza hide()
, show()
e add()
invece del metodo replace()
. La logica è semplice. Nascondere il frammento precedente e mostrare il frammento di destinazione. Se è la prima volta che si va a uno specifico frammento di destinazione, allora si aggiunge il frammento invece di mostrarlo.
@Navigator.Name("sticky_fragment")
public class StickyFragmentNavigator extends Navigator {
private static final String TAG = "StickyFragmentNavigator";
private static final String KEY_BACK_STACK_IDS = "androidx-nav-fragment:navigator:backStackIds";
private final Context mContext;
@SuppressWarnings("WeakerAccess") /* synthetic access */
final FragmentManager mFragmentManager;
private final int mContainerId;
@SuppressWarnings("WeakerAccess") /* synthetic access */
ArrayDeque mBackStack = new ArrayDeque<>();
@SuppressWarnings("WeakerAccess") /* synthetic access */
boolean mIsPendingBackStackOperation = false;
private final FragmentManager.OnBackStackChangedListener mOnBackStackChangedListener =
new FragmentManager.OnBackStackChangedListener() {
@SuppressLint("RestrictedApi")
@Override
public void onBackStackChanged() {
// If we have pending operations made by us then consume this change, otherwise
// detect a pop in the back stack to dispatch callback.
if (mIsPendingBackStackOperation) {
mIsPendingBackStackOperation = !isBackStackEqual();
return;
}
// The initial Fragment won't be on the back stack, so the
// real count of destinations is the back stack entry count + 1
int newCount = mFragmentManager.getBackStackEntryCount() + 1;
if (newCount < mBackStack.size()) {
// Handle cases where the user hit the system back button
while (mBackStack.size() > newCount) {
mBackStack.removeLast();
}
dispatchOnNavigatorBackPress();
}
}
};
public StickyFragmentNavigator(@NonNull Context context, @NonNull FragmentManager manager,
int containerId) {
mContext = context;
mFragmentManager = manager;
mContainerId = containerId;
}
@Override
protected void onBackPressAdded() {
mFragmentManager.addOnBackStackChangedListener(mOnBackStackChangedListener);
}
@Override
protected void onBackPressRemoved() {
mFragmentManager.removeOnBackStackChangedListener(mOnBackStackChangedListener);
}
@Override
public boolean popBackStack() {
if (mBackStack.isEmpty()) {
return false;
}
if (mFragmentManager.isStateSaved()) {
Log.i(TAG, "Ignoring popBackStack() call: FragmentManager has already"
+ " saved its state");
return false;
}
if (mFragmentManager.getBackStackEntryCount() > 0) {
mFragmentManager.popBackStack(
generateBackStackName(mBackStack.size(), mBackStack.peekLast()),
FragmentManager.POP_BACK_STACK_INCLUSIVE);
mIsPendingBackStackOperation = true;
} // else, we're on the first Fragment, so there's nothing to pop from FragmentManager
mBackStack.removeLast();
return true;
}
@NonNull
@Override
public StickyFragmentNavigator.Destination createDestination() {
return new StickyFragmentNavigator.Destination(this);
}
@NonNull
public Fragment instantiateFragment(@NonNull Context context,
@SuppressWarnings("unused") @NonNull FragmentManager fragmentManager,
@NonNull String className, @Nullable Bundle args) {
return Fragment.instantiate(context, className, args);
}
@Nullable
@Override
public NavDestination navigate(@NonNull StickyFragmentNavigator.Destination destination, @Nullable Bundle args,
@Nullable NavOptions navOptions, @Nullable Navigator.Extras navigatorExtras) {
if (mFragmentManager.isStateSaved()) {
Log.i(TAG, "Ignoring navigate() call: FragmentManager has already"
+ " saved its state");
return null;
}
String className = destination.getClassName();
if (className.charAt(0) == '.') {
className = mContext.getPackageName() + className;
}
final FragmentTransaction ft = mFragmentManager.beginTransaction();
int enterAnim = navOptions != null ? navOptions.getEnterAnim() : -1;
int exitAnim = navOptions != null ? navOptions.getExitAnim() : -1;
int popEnterAnim = navOptions != null ? navOptions.getPopEnterAnim() : -1;
int popExitAnim = navOptions != null ? navOptions.getPopExitAnim() : -1;
if (enterAnim != -1 || exitAnim != -1 || popEnterAnim != -1 || popExitAnim != -1) {
enterAnim = enterAnim != -1 ? enterAnim : 0;
exitAnim = exitAnim != -1 ? exitAnim : 0;
popEnterAnim = popEnterAnim != -1 ? popEnterAnim : 0;
popExitAnim = popExitAnim != -1 ? popExitAnim : 0;
ft.setCustomAnimations(enterAnim, exitAnim, popEnterAnim, popExitAnim);
}
String tag = Integer.toString(destination.getId());
Fragment primaryNavigationFragment = mFragmentManager.getPrimaryNavigationFragment();
if(primaryNavigationFragment != null)
ft.hide(primaryNavigationFragment);
Fragment destinationFragment = mFragmentManager.findFragmentByTag(tag);
if(destinationFragment == null) {
destinationFragment = instantiateFragment(mContext, mFragmentManager, className, args);
destinationFragment.setArguments(args);
ft.add(mContainerId, destinationFragment , tag);
}
else
ft.show(destinationFragment);
ft.setPrimaryNavigationFragment(destinationFragment);
final @IdRes int destId = destination.getId();
final boolean initialNavigation = mBackStack.isEmpty();
// TODO Build first class singleTop behavior for fragments
final boolean isSingleTopReplacement = navOptions != null && !initialNavigation
&& navOptions.shouldLaunchSingleTop()
&& mBackStack.peekLast() == destId;
boolean isAdded;
if (initialNavigation) {
isAdded = true;
} else if (isSingleTopReplacement) {
// Single Top means we only want one instance on the back stack
if (mBackStack.size() > 1) {
// If the Fragment to be replaced is on the FragmentManager's
// back stack, a simple replace() isn't enough so we
// remove it from the back stack and put our replacement
// on the back stack in its place
mFragmentManager.popBackStackImmediate(
generateBackStackName(mBackStack.size(), mBackStack.peekLast()), 0);
mIsPendingBackStackOperation = false;
}
isAdded = false;
} else {
ft.addToBackStack(generateBackStackName(mBackStack.size() + 1, destId));
mIsPendingBackStackOperation = true;
isAdded = true;
}
if (navigatorExtras instanceof FragmentNavigator.Extras) {
FragmentNavigator.Extras extras = (FragmentNavigator.Extras) navigatorExtras;
for (Map.Entry sharedElement : extras.getSharedElements().entrySet()) {
ft.addSharedElement(sharedElement.getKey(), sharedElement.getValue());
}
}
ft.setReorderingAllowed(true);
ft.commit();
// The commit succeeded, update our view of the world
if (isAdded) {
mBackStack.add(destId);
return destination;
} else {
return null;
}
}
@Override
@Nullable
public Bundle onSaveState() {
Bundle b = new Bundle();
int[] backStack = new int[mBackStack.size()];
int index = 0;
for (Integer id : mBackStack) {
backStack[index++] = id;
}
b.putIntArray(KEY_BACK_STACK_IDS, backStack);
return b;
}
@Override
public void onRestoreState(@Nullable Bundle savedState) {
if (savedState != null) {
int[] backStack = savedState.getIntArray(KEY_BACK_STACK_IDS);
if (backStack != null) {
mBackStack.clear();
for (int destId : backStack) {
mBackStack.add(destId);
}
}
}
}
@NonNull
private String generateBackStackName(int backStackIndex, int destId) {
return backStackIndex + "-" + destId;
}
private int getDestId(@Nullable String backStackName) {
String[] split = backStackName != null ? backStackName.split("-") : new String[0];
if (split.length != 2) {
throw new IllegalStateException("Invalid back stack entry on the "
+ "NavHostFragment's back stack - use getChildFragmentManager() "
+ "if you need to do custom FragmentTransactions from within "
+ "Fragments created via your navigation graph.");
}
try {
// Just make sure the backStackIndex is correctly formatted
Integer.parseInt(split[0]);
return Integer.parseInt(split[1]);
} catch (NumberFormatException e) {
throw new IllegalStateException("Invalid back stack entry on the "
+ "NavHostFragment's back stack - use getChildFragmentManager() "
+ "if you need to do custom FragmentTransactions from within "
+ "Fragments created via your navigation graph.");
}
}
@SuppressWarnings("WeakerAccess") /* synthetic access */
boolean isBackStackEqual() {
int fragmentBackStackCount = mFragmentManager.getBackStackEntryCount();
// Initial fragment won't be on the FragmentManager's back stack so +1 its count.
if (mBackStack.size() != fragmentBackStackCount + 1) {
return false;
}
// From top to bottom verify destination ids match in both back stacks/
Iterator backStackIterator = mBackStack.descendingIterator();
int fragmentBackStackIndex = fragmentBackStackCount - 1;
while (backStackIterator.hasNext() && fragmentBackStackIndex >= 0) {
int destId = backStackIterator.next();
try {
int fragmentDestId = getDestId(mFragmentManager
.getBackStackEntryAt(fragmentBackStackIndex--)
.getName());
if (destId != fragmentDestId) {
return false;
}
} catch (NumberFormatException e) {
throw new IllegalStateException("Invalid back stack entry on the "
+ "NavHostFragment's back stack - use getChildFragmentManager() "
+ "if you need to do custom FragmentTransactions from within "
+ "Fragments created via your navigation graph.");
}
}
return true;
}
@NavDestination.ClassType(Fragment.class)
public static class Destination extends NavDestination {
private String mClassName;
public Destination(@NonNull NavigatorProvider navigatorProvider) {
this(navigatorProvider.getNavigator(StickyFragmentNavigator.class));
}
public Destination(@NonNull Navigator extends StickyFragmentNavigator.Destination> fragmentNavigator) {
super(fragmentNavigator);
}
@CallSuper
@Override
public void onInflate(@NonNull Context context, @NonNull AttributeSet attrs) {
super.onInflate(context, attrs);
TypedArray a = context.getResources().obtainAttributes(attrs,
R.styleable.FragmentNavigator);
String className = a.getString(R.styleable.FragmentNavigator_android_name);
if (className != null) {
setClassName(className);
}
a.recycle();
}
@NonNull
public final StickyFragmentNavigator.Destination setClassName(@NonNull String className) {
mClassName = className;
return this;
}
@NonNull
public final String getClassName() {
if (mClassName == null) {
throw new IllegalStateException("Fragment class was not set");
}
return mClassName;
}
}
public static final class Extras implements Navigator.Extras {
private final LinkedHashMap mSharedElements = new LinkedHashMap<>();
Extras(Map sharedElements) {
mSharedElements.putAll(sharedElements);
}
@NonNull
public Map getSharedElements() {
return Collections.unmodifiableMap(mSharedElements);
}
public static final class Builder {
private final LinkedHashMap mSharedElements = new LinkedHashMap<>();
@NonNull
public StickyFragmentNavigator.Extras.Builder addSharedElements(@NonNull Map sharedElements) {
for (Map.Entry sharedElement : sharedElements.entrySet()) {
View view = sharedElement.getKey();
String name = sharedElement.getValue();
if (view != null && name != null) {
addSharedElement(view, name);
}
}
return this;
}
@NonNull
public StickyFragmentNavigator.Extras.Builder addSharedElement(@NonNull View sharedElement, @NonNull String name) {
mSharedElements.put(sharedElement, name);
return this;
}
@NonNull
public StickyFragmentNavigator.Extras build() {
return new StickyFragmentNavigator.Extras(mSharedElements);
}
}
}
}
FASE 2: Utilizzare il tag personalizzato
Ora apriamo il tag navigation.xml
e rinominare il tag frammento relativi alla navigazione in basso, con il nome che è stato dato in @Navigator.Name()
prima.
FASE 3: Aggiungere un'azione globale
Le azioni globali sono un modo per navigare verso una destinazione da qualsiasi punto dell'applicazione. Si può usare l'editor visuale o direttamente l'xml per aggiungere azioni globali. Impostare l'azione globale su ogni frammento con le seguenti impostazioni
- destinazione : self
- popUpTo : self
- singleTop : true/checked
Questo è il modo in cui il tuo navigation.xml
dovrebbe apparire dopo aver aggiunto le azioni globali
PASSO 4: Utilizzare le azioni globali
Quando si è scritto
NavigationUI.setupWithNavController (bottomNavigationView, navHostFragment.getNavController ());
allora all'interno di setupWithNavController()
NavigationUI utilizza bottomNavigationView.setOnNavigationItemSelectedListener()
per navigare verso i frammenti appropriati, a seconda dell'id della voce di menu che è stata cliccata. Il suo comportamento predefinito è quello descritto in precedenza. Aggiungeremo la nostra implementazione e useremo le azioni globali per ottenere il comportamento desiderato.
Ecco come farlo semplicemente in MainActivity
bottomNavigationView.setOnNavigationItemSelectedListener(new BottomNavigationView.OnNavigationItemSelectedListener() {
@Override
public boolean onNavigationItemSelected(@NonNull MenuItem menuItem) {
int id = menuItem.getItemId();
if (menuItem.isChecked()) return false;
switch (id)
{
case R.id.navigation_home :
navController.navigate(R.id.action_global_navigation_home);
break;
case R.id.navigation_images :
navController.navigate(R.id.action_global_navigation_images);
break;
case R.id.navigation_videos :
navController.navigate(R.id.action_global_navigation_videos);
break;
case R.id.navigation_songs :
navController.navigate(R.id.action_global_navigation_songs);
break;
case R.id.navigation_notifications :
navController.navigate(R.id.action_global_navigation_notifications);
break;
}
return true;
}
});
FASE FINALE 5: Aggiungere il navigatore personalizzato a NavController
Aggiungere il navigatore come segue nella MainActivity. Assicurarsi di passare childFragmentManager
dell'elemento NavHostFragment
.
navController.getNavigatorProvider().addNavigator(new StickyFragmentNavigator(this, navHostFragment.getChildFragmentManager(),R.id.nav_host_fragment));
Aggiungere anche il grafico di navigazione a NavController
anche qui, utilizzando setGraph()
come mostrato di seguito.
Ecco come il mio MainActivity
appare dopo che passo 4 e passo 5
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
BottomNavigationView navView = findViewById(R.id.nav_view);
AppBarConfiguration appBarConfiguration = new AppBarConfiguration.Builder(
R.id.navigation_home, R.id.navigation_images, R.id.navigation_videos,R.id.navigation_songs,R.id.navigation_notifications)
.build();
NavHostFragment navHostFragment = (NavHostFragment)getSupportFragmentManager().findFragmentById(R.id.nav_host_fragment);
final NavController navController = Navigation.findNavController(this, R.id.nav_host_fragment);
navController.getNavigatorProvider().addNavigator(new StickyFragmentNavigator(this, navHostFragment.getChildFragmentManager(),R.id.nav_host_fragment));
navController.setGraph(R.navigation.mobile_navigation);
NavigationUI.setupActionBarWithNavController(this, navController, appBarConfiguration);
NavigationUI.setupWithNavController(navView,navController);
navView.setOnNavigationItemSelectedListener(new BottomNavigationView.OnNavigationItemSelectedListener() {
@Override
public boolean onNavigationItemSelected(@NonNull MenuItem menuItem) {
int id = menuItem.getItemId();
if (menuItem.isChecked()) return false;
switch (id)
{
case R.id.navigation_home :
navController.navigate(R.id.action_global_navigation_home);
break;
case R.id.navigation_images :
navController.navigate(R.id.action_global_navigation_images);
break;
case R.id.navigation_videos :
navController.navigate(R.id.action_global_navigation_videos);
break;
case R.id.navigation_songs :
navController.navigate(R.id.action_global_navigation_songs);
break;
case R.id.navigation_notifications :
navController.navigate(R.id.action_global_navigation_notifications);
break;
}
return true;
}
});
}
}
Spero che questo sia utile.