Saltar al contenido

Mis fragmentos se siguen recreando cada vez que vuelvo a hacer clic o navego al siguiente fragmento

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

Arquitectura recomendada por Android

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

  1. ampliar FragmentNavigator
  2. 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 !!! :

  1. Crear navegador personalizado
  2. Usar etiqueta personalizada
  3. Agregar acciones globales
  4. Utilice acciones globales
  5. 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

ingrese la descripción de la imagen aquí
ingrese la descripción de la imagen aquí

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.

¡Haz clic para puntuar esta entrada!
(Votos: 0 Promedio: 0)



Utiliza Nuestro Buscador

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *