Solución:
TL; DR: saltar al SOLO MUESTRAME LOS PASOS YA !!! sección
Ese es el comportamiento normal de los fragmentos. Se supone que deben volver a crearse cada vez que se eliminan o reemplazan y se supone que debe restaurar sus estados usando onSaveInstanceState
.
Aquí hay un buen artículo que describe cómo hacerlo: Guardar estados de fragmentos
Aparte de eso, puede usar View Model, que forma parte de la siguiente arquitectura de Android recomendada. Son una excelente manera de retener y restaurar los datos de la interfaz de usuario.
Puede aprender cómo implementar esta arquitectura siguiendo este laboratorio de código paso a paso
EDITAR: Solución
Tomó un tiempo pero aquí está. La solución no usa ViewModels
en este momento.
Lea atentamente porque cada paso es importante. Esta solución cubre las siguientes dos partes
- Implementar la navegación adecuada al presionar la tecla Atrás
- Mantener vivo el fragmento durante la navegación
Fondo :
El componente de navegación de Android proporciona una NavController
clase que usa para navegar entre diferentes destinos. Internamente NavController
usa un Navigator
que realmente hace la navegación. Navigator
es una clase abstracta y cualquiera puede extender / heredar esta clase para crear su propio navegador personalizado para proporcionar navegación personalizada según el tipo de destino. Cuando se utilizan fragmentos como destinos, NavHostFragment
usa un FragmentNavigator
cuya implementación predeterminada reemplaza los fragmentos cada vez que navegamos usando FragmentTransaction.replace()
que destruye completamente el fragmento anterior y agrega un nuevo fragmento. Entonces tenemos que crear nuestro propio navegador y en lugar de usar FragmentTransaction.replace()
usaremos una combinación de FragmentTransaction.hide()
y FragmentTransaction.show()
para evitar que el fragmento sea destruido.
Comportamiento predeterminado de la interfaz de usuario de navegación:
De forma predeterminada, cada vez que navega a cualquier otro fragmento que no sea el fragmento de inicio / inicio, no se agregarán al backstack, así que digamos si selecciona fragmentos en el siguiente orden
A -> B -> C -> D -> E
tu back stack solo tendrá
[A, E]
como puede ver, los fragmentos B, C, D no se agregaron al backstack, por lo que presionar hacia atrás siempre lo llevará al fragmento A, que es el fragmento de inicio
El comportamiento que queremos por ahora:
Queremos un comportamiento sencillo pero eficaz. No queremos que todos los fragmentos se agreguen al backstack, pero si el fragmento ya está en el backstack, queremos hacer estallar todos los fragmentos hasta el fragmento seleccionado.
Digamos que selecciono el fragmento en el siguiente orden
A -> B -> C -> D -> E
el backstack también debe ser
[A, B, C, D, E]
al presionar hacia atrás, solo se debe sacar el último fragmento y el backstack debe ser así
[A, B, C, D]
pero si navegamos hasta, digamos, el fragmento B, dado que B ya está en la pila, entonces todos los fragmentos de arriba de B deberían aparecer y nuestro backstack debería verse así
[A, B]
Espero que este comportamiento tenga sentido. Este comportamiento es fácil de implementar usando acciones globales como verá a continuación y es mejor que el predeterminado.
¡OK Hotshot! ahora que ? :
Ahora tenemos dos opciones
- ampliar
FragmentNavigator
- copiar pegar
FragmentNavigator
Bueno, yo personalmente quería extenderme FragmentNavigator
y anular navigate()
pero como todas sus variables miembro son privadas, no pude implementar la navegación adecuada.
Así que decidí copiar y pegar todo FragmentNavigator
class y simplemente cambie el nombre en todo el código de “FragmentNavigator” a como quiera llamarlo.
SOLO MUESTRAME LOS PASOS YA !!! :
- Crear navegador personalizado
- Usar etiqueta personalizada
- Agregar acciones globales
- Utilice acciones globales
- Agregue el navegador personalizado al NavController
PASO 1: Cree un navegador personalizado
Aquí está mi navegador personalizado llamado StickyCustomNavigator
. Todo el código es el mismo que FragmentNavigator
excepto el navigate()
método. Como puede ver, usa hide()
, show()
y add()
método en lugar de replace()
. La lógica es simple. Oculte el fragmento anterior y muestre el fragmento de destino. Si es la primera vez que vamos a un fragmento de destino específico, agregue el fragmento en lugar de mostrarlo.
@Navigator.Name("sticky_fragment")
public class StickyFragmentNavigator extends Navigator<StickyFragmentNavigator.Destination> {
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<Integer> 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<View, String> 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<Integer> 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<View, String> mSharedElements = new LinkedHashMap<>();
Extras(Map<View, String> sharedElements) {
mSharedElements.putAll(sharedElements);
}
@NonNull
public Map<View, String> getSharedElements() {
return Collections.unmodifiableMap(mSharedElements);
}
public static final class Builder {
private final LinkedHashMap<View, String> mSharedElements = new LinkedHashMap<>();
@NonNull
public StickyFragmentNavigator.Extras.Builder addSharedElements(@NonNull Map<View, String> sharedElements) {
for (Map.Entry<View, String> 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);
}
}
}
}
PASO 2: Utilice una etiqueta personalizada
Ahora abre tu navigation.xml
archivo y cambie el nombre del fragmento etiquetas relacionadas con la navegación inferior con el nombre que hayas dado @Navigator.Name()
más temprano.
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:andro
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:
app:startDestination="@+id/navigation_home">
<sticky_fragment
android:
android:name="com.example.bottomnavigationlogic.ui.home.HomeFragment"
android:label="@string/title_home"
tools:layout="@layout/fragment_home" />
</navigation>
PASO 3: Agregar acción global
Las acciones globales son una forma de navegar hacia el destino desde cualquier lugar de su aplicación. Puede usar el editor visual o usar directamente xml para agregar acciones globales. Establezca una acción global en cada fragmento con la siguiente configuración
- destino: uno mismo
- popUpTo: self
- singleTop: verdadero / comprobado
Este es tu como tu navigation.xml
debería verse después de agregar acciones globales
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:andro
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:
app:startDestination="@+id/navigation_home">
<sticky_fragment
android:
android:name="com.example.bottomnavigationlogic.ui.home.HomeFragment"
android:label="@string/title_home"
tools:layout="@layout/fragment_home" />
<sticky_fragment
android:
android:name="com.example.bottomnavigationlogic.ui.images.ImagesFragment"
android:label="@string/title_images"
tools:layout="@layout/fragment_images" />
<sticky_fragment
android:
android:name="com.example.bottomnavigationlogic.ui.videos.VideosFragment"
android:label="@string/title_videos"
tools:layout="@layout/fragment_videos" />
<sticky_fragment
android:
android:name="com.example.bottomnavigationlogic.ui.songs.SongsFragment"
android:label="@string/title_songs"
tools:layout="@layout/fragment_songs" />
<sticky_fragment
android:
android:name="com.example.bottomnavigationlogic.ui.notifications.NotificationsFragment"
android:label="@string/title_notifications"
tools:layout="@layout/fragment_notifications" />
<action
android:
app:destination="@id/navigation_home"
app:launchSingleTop="true"
app:popUpTo="@id/navigation_home" />
<action
android:
app:destination="@id/navigation_notifications"
app:launchSingleTop="true"
app:popUpTo="@id/navigation_notifications" />
<action
android:
app:destination="@id/navigation_songs"
app:launchSingleTop="true"
app:popUpTo="@id/navigation_songs" />
<action
android:
app:destination="@id/navigation_videos"
app:launchSingleTop="true"
app:popUpTo="@id/navigation_videos" />
</navigation>
PASO 4: Utilice acciones globales
Cuando escribiste
NavigationUI.setupWithNavController (bottomNavigationView, navHostFragment.getNavController ());
luego adentro setupWithNavController()
Usos de NavigationUI bottomNavigationView.setOnNavigationItemSelectedListener()
para navegar a los fragmentos adecuados según la identificación del elemento del menú en el que se hizo clic. Su comportamiento predeterminado es el que mencioné antes. Agregaremos nuestra propia implementación y usaremos acciones globales para lograr nuestro comportamiento deseado de back press.
Así es como se hace simplemente en 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;
}
});
PASO FINAL 5: agregue su navegador personalizado a NavController
Agregue su navegador de la siguiente manera en su MainActivity. Asegúrate de pasar childFragmentManager
de El NavHostFragment
.
navController.getNavigatorProvider().addNavigator(new StickyFragmentNavigator(this, navHostFragment.getChildFragmentManager(),R.id.nav_host_fragment));
También agregue el gráfico de navegación a NavController
aquí también usando setGraph()
método como se muestra a continuación.
Asi es como mi MainActivity
parece después paso 4 y paso 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;
}
});
}
}
Espero que esto ayude.