Skip to content

I miei frammenti continuano a ricrearsi ogni volta che faccio clic o passo al frammento successivo

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

Architettura consigliata Android

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

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

  1. Creare un navigatore personalizzato
  2. Utilizzare il tag personalizzato
  3. Aggiungere azioni globali
  4. Usa azioni globali
  5. 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 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

Immettere la descrizione dell'immagine quiImmettere la descrizione dell'immagine qui

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.

Valutazioni e commenti



Utilizzate il nostro motore di ricerca

Ricerca
Generic filters

Lascia un commento

Il tuo indirizzo email non sarà pubblicato.