Skip to content

Mes fragments continuent à se recréer chaque fois que je reclique ou que je navigue vers le fragment suivant.

Après une longue sélection d'informations, nous avons résolu ce problème rencontré par certains de nos utilisateurs. Nous partageons la réponse et notre désir est d'être très utile.

Solution :

TL;DR : passez à la MONTREZ-MOI DÉJÀ LES ÉTAPES ! !! section

C'est le comportement normal des fragments. Ils sont censés être recréés chaque fois qu'ils sont retirés ou remplacés et vous êtes censé restaurer leurs états en utilisant... onSaveInstanceState.

Voici un bel article qui décrit comment le faire : Saving Fragment States (Sauvegarde des états des fragments).

En dehors de cela, vous pouvez utiliser le modèle de vue qui fait partie de l'architecture android recommandée suivante. Ils sont un excellent moyen de conserver et de restaurer les données de l'interface utilisateur.

Vous pouvez apprendre comment mettre en œuvre cette architecture en suivant ce laboratoire de code étape par étape.

Architecture recommandée Android

EDIT : Solution :

Cela a pris un certain temps mais voilà. La solution n'utilise pas ViewModels pour le moment.

Lisez attentivement car chaque étape est importante. Cette solution couvre les deux parties suivantes

  • Mise en œuvre d'une navigation appropriée sur l'appui de la touche retour.
  • Garder le fragment vivant pendant la navigation

Fond d'écran :

Le composant de navigation Android fournit un NavController classe que vous utilisez pour naviguer entre différentes destinations. En interne, NavController utilise une classe Navigator qui effectue réellement la navigation. Navigator est une classe abstraite et chacun peut étendre/hériter cette classe pour créer son propre navigateur personnalisé afin de fournir une navigation personnalisée en fonction du type de destination. Lors de l'utilisation de fragments comme destinations, la classe NavHostFragment utilise une classe FragmentNavigator dont l'implémentation par défaut remplace les fragments à chaque fois que l'on navigue en utilisant la classe FragmentTransaction.replace() qui détruit complètement le fragment précédent et ajoute un nouveau fragment. Donc, nous devons créer notre propre navigateur et au lieu d'utiliser FragmentTransaction.replace() nous utiliserons une combinaison de FragmentTransaction.hide() et FragmentTransaction.show() pour éviter que le fragment ne soit détruit.

Comportement par défaut de l'interface utilisateur de navigation :

Par défaut chaque fois que vous naviguez vers un autre fragment autre que le fragment d'origine/départ, ils ne seront pas ajoutés au backstack donc disons que si vous sélectionnez des fragments dans l'ordre suivant.

A -> B -> C -> D -> E

votre pile arrière aura seulement

[A, E]

comme vous pouvez le voir les fragments B, C, D n'ont pas été ajoutés à la pile arrière, donc en appuyant sur la touche retour, vous arriverez toujours au fragment A qui est le fragment de départ.

Le comportement que nous voulons pour le moment :

Nous voulons un comportement simple mais efficace. Nous ne voulons pas que tous les fragments soient ajoutés au backstack mais si le fragment est déjà dans le backstack nous voulons pop tous les fragments jusqu'au fragment sélectionné.

Disons que je sélectionne les fragments dans l'ordre suivant

A -> B -> C -> D -> E

le backstack devrait aussi être

[A, B, C, D, E]

En appuyant sur le bouton "back", seul le dernier fragment devrait être retiré et la pile arrière devrait être comme ceci

[A, B, C, D]

mais si nous naviguons vers, disons, le fragment B, puisque B est déjà dans la pile, tous les fragments au-dessus de B devraient être retirés et notre pile arrière devrait ressembler à ceci

 [A, B]

J'espère que ce comportement a du sens. Ce comportement est facile à mettre en œuvre en utilisant des actions globales comme vous le verrez ci-dessous et est meilleur que celui par défaut.

OK Hotshot ! maintenant quoi ? :

Maintenant nous avons deux options

  1. étendre FragmentNavigator
  2. copier/coller FragmentNavigator

Eh bien, personnellement, je voulais juste étendre FragmentNavigator et remplacer navigate() mais puisque toutes ses variables membres sont privées, je ne pouvais pas mettre en œuvre une navigation appropriée.

J'ai donc décidé de copier-coller l'ensemble de la méthode FragmentNavigator et juste changer le nom dans le code entier de "FragmentNavigator" à ce que je veux l'appeler.

MONTREZ-MOI DÉJÀ LES ÉTAPES ! !! :

  1. Créer un navigateur personnalisé
  2. Utiliser la balise personnalisée
  3. Ajouter des actions globales
  4. Utiliser les actions globales
  5. Ajouter le navigateur personnalisé au NavController.

ÉTAPE 1 : Créer un navigateur personnalisé

Voici mon navigateur personnalisé appelé StickyCustomNavigator. Tout le code est le même que FragmentNavigator sauf le navigate() de la méthode. Comme vous pouvez le voir, il utilise hide() , show() et add() au lieu de la méthode replace(). La logique est simple. Cacher le fragment précédent et montrer le fragment de destination. Si c'est la première fois que nous allons vers un fragment de destination spécifique, alors ajoutez le fragment au lieu de le montrer.

@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 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);
            }
        }
    }
}

ÉTAPE 2 : Utilisez la balise personnalisée

Maintenant, ouvrez votre navigation.xml et renommez le fichier fragment liés à votre navigation inférieure avec le nom que vous avez donné dans le fichier @Navigator.Name() plus tôt.




    


ÉTAPE 3 : Ajoutez une action globale

Les actions globales sont un moyen de naviguer vers une destination depuis n'importe où dans votre application. Vous pouvez utiliser l'éditeur visuel ou directement utiliser xml pour ajouter des actions globales. Définissez l'action globale sur chaque fragment avec les paramètres suivants.

  • destination : self
  • popUpTo : self
  • singleTop : true/checked

Entrez la description de l'image iciEntrez la description de l'image ici

Voici comment votre navigation.xml devrait ressembler après avoir ajouté des actions globales




    

    

    
    
    
    
    
    
    

ÉTAPE 4 : Utilisez les actions globales

Lorsque vous avez écrit

 NavigationUI.setupWithNavController (bottomNavigationView, navHostFragment.getNavController ());

puis à l'intérieur setupWithNavController() NavigationUI utilise bottomNavigationView.setOnNavigationItemSelectedListener() pour naviguer vers les fragments appropriés en fonction de l'identifiant de l'élément de menu qui a été cliqué. Son comportement par défaut est celui que j'ai mentionné précédemment. Nous y ajouterons notre propre mise en œuvre et utiliserons des actions globales pour obtenir le comportement de retour en arrière souhaité.

Voici comment le faire simplement dans 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;

            }
        });

ETAPE FINALE 5 : Ajoutez votre navigateur personnalisé au NavController.

Ajoutez votre navigateur comme suit dans votre MainActivity. Assurez-vous que vous passez childFragmentManager de la NavHostFragment.

navController.getNavigatorProvider().addNavigator(new StickyFragmentNavigator(this, navHostFragment.getChildFragmentManager(),R.id.nav_host_fragment));

Ajoutez également le graphique de navigation à NavController ici aussi en utilisant setGraph() comme indiqué ci-dessous.

C'est ainsi que mon MainActivity ressemble après étape 4 et étape 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;

            }
        });

    }

}

J'espère que cela vous aidera.

Si vous faites défiler, vous pouvez trouver les avis d'autres programmeurs, vous avez également la liberté d'insérer le vôtre si vous en avez envie.



Utilisez notre moteur de recherche

Ricerca
Generic filters

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée.