ViewPager und Fragmente - Wie kann der Status des Fragments richtig gespeichert werden?

492

Fragmente scheinen sehr schön für die Aufteilung der UI-Logik in einige Module zu sein. Aber zusammen mit ViewPagerseinem Lebenszyklus ist für mich immer noch neblig. Guru-Gedanken werden also dringend benötigt!

Bearbeiten

Siehe dumme Lösung unten ;-)

Umfang

Hauptaktivität hat eine ViewPagermit Fragmenten. Diese Fragmente könnten eine etwas andere Logik für andere (Haupt-) Aktivitäten implementieren, sodass die Daten der Fragmente über eine Rückrufschnittstelle innerhalb der Aktivität gefüllt werden. Und beim ersten Start funktioniert alles einwandfrei, aber! ...

Problem

Wenn die Aktivität neu erstellt wird (z. B. bei einer Änderung der Ausrichtung), tun dies auch die ViewPagerFragmente der Aktivität . Der Code (den Sie unten finden) besagt, dass ich jedes Mal, wenn die Aktivität erstellt wird, versuche, einen neuen ViewPagerFragmentadapter zu erstellen, der mit Fragmenten identisch ist (möglicherweise ist dies das Problem), aber FragmentManager hat bereits alle diese Fragmente irgendwo gespeichert (wo?) Und startet den Erholungsmechanismus für diese. Der Wiederherstellungsmechanismus ruft also onAttach, onCreateView usw. des "alten" Fragments mit meinem Aufruf der Rückrufschnittstelle auf, um Daten über die implementierte Methode der Aktivität zu initiieren. Diese Methode verweist jedoch auf das neu erstellte Fragment, das über die onCreate-Methode der Aktivität erstellt wird.

Problem

Vielleicht verwende ich falsche Muster, aber selbst das Android 3 Pro-Buch hat nicht viel damit zu tun. Also, bitte , gib mir einen Doppelsieg und zeige, wie es richtig geht. Danke vielmals!

Code

Hauptaktivität

public class DashboardActivity extends BasePagerActivity implements OnMessageListActionListener {

private MessagesFragment mMessagesFragment;

@Override
protected void onCreate(Bundle savedInstanceState) {
    Logger.d("Dash onCreate");
    super.onCreate(savedInstanceState);

    setContentView(R.layout.viewpager_container);
    new DefaultToolbar(this);

    // create fragments to use
    mMessagesFragment = new MessagesFragment();
    mStreamsFragment = new StreamsFragment();

    // set titles and fragments for view pager
    Map<String, Fragment> screens = new LinkedHashMap<String, Fragment>();
    screens.put(getApplicationContext().getString(R.string.dashboard_title_dumb), new DumbFragment());
    screens.put(getApplicationContext().getString(R.string.dashboard_title_messages), mMessagesFragment);

    // instantiate view pager via adapter
    mPager = (ViewPager) findViewById(R.id.viewpager_pager);
    mPagerAdapter = new BasePagerAdapter(screens, getSupportFragmentManager());
    mPager.setAdapter(mPagerAdapter);

    // set title indicator
    TitlePageIndicator indicator = (TitlePageIndicator) findViewById(R.id.viewpager_titles);
    indicator.setViewPager(mPager, 1);

}

/* set of fragments callback interface implementations */

@Override
public void onMessageInitialisation() {

    Logger.d("Dash onMessageInitialisation");
    if (mMessagesFragment != null)
        mMessagesFragment.loadLastMessages();
}

@Override
public void onMessageSelected(Message selectedMessage) {

    Intent intent = new Intent(this, StreamActivity.class);
    intent.putExtra(Message.class.getName(), selectedMessage);
    startActivity(intent);
}

BasePagerActivity aka Helfer

public class BasePagerActivity extends FragmentActivity {

BasePagerAdapter mPagerAdapter;
ViewPager mPager;
}

Adapter

public class BasePagerAdapter extends FragmentPagerAdapter implements TitleProvider {

private Map<String, Fragment> mScreens;

public BasePagerAdapter(Map<String, Fragment> screenMap, FragmentManager fm) {

    super(fm);
    this.mScreens = screenMap;
}

@Override
public Fragment getItem(int position) {

    return mScreens.values().toArray(new Fragment[mScreens.size()])[position];
}

@Override
public int getCount() {

    return mScreens.size();
}

@Override
public String getTitle(int position) {

    return mScreens.keySet().toArray(new String[mScreens.size()])[position];
}

// hack. we don't want to destroy our fragments and re-initiate them after
@Override
public void destroyItem(View container, int position, Object object) {

    // TODO Auto-generated method stub
}

}

Fragment

public class MessagesFragment extends ListFragment {

private boolean mIsLastMessages;

private List<Message> mMessagesList;
private MessageArrayAdapter mAdapter;

private LoadMessagesTask mLoadMessagesTask;
private OnMessageListActionListener mListener;

// define callback interface
public interface OnMessageListActionListener {
    public void onMessageInitialisation();
    public void onMessageSelected(Message selectedMessage);
}

@Override
public void onAttach(Activity activity) {
    super.onAttach(activity);
    // setting callback
    mListener = (OnMessageListActionListener) activity;
    mIsLastMessages = activity instanceof DashboardActivity;

}

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
    inflater.inflate(R.layout.fragment_listview, container);
    mProgressView = inflater.inflate(R.layout.listrow_progress, null);
    mEmptyView = inflater.inflate(R.layout.fragment_nodata, null);
    return super.onCreateView(inflater, container, savedInstanceState);
}

@Override
public void onActivityCreated(Bundle savedInstanceState) {
    super.onActivityCreated(savedInstanceState);

    // instantiate loading task
    mLoadMessagesTask = new LoadMessagesTask();

    // instantiate list of messages
    mMessagesList = new ArrayList<Message>();
    mAdapter = new MessageArrayAdapter(getActivity(), mMessagesList);
    setListAdapter(mAdapter);
}

@Override
public void onResume() {
    mListener.onMessageInitialisation();
    super.onResume();
}

public void onListItemClick(ListView l, View v, int position, long id) {
    Message selectedMessage = (Message) getListAdapter().getItem(position);
    mListener.onMessageSelected(selectedMessage);
    super.onListItemClick(l, v, position, id);
}

/* public methods to load messages from host acitivity, etc... */
}

Lösung

Die blöde Lösung besteht darin, die Fragmente in onSaveInstanceState (der Host-Aktivität) mit putFragment zu speichern und sie über getFragment in onCreate abzurufen. Aber ich habe immer noch das seltsame Gefühl, dass die Dinge nicht so funktionieren sollten ... Siehe Code unten:

    @Override
protected void onSaveInstanceState(Bundle outState) {

    super.onSaveInstanceState(outState);
    getSupportFragmentManager()
            .putFragment(outState, MessagesFragment.class.getName(), mMessagesFragment);
}

protected void onCreate(Bundle savedInstanceState) {
    Logger.d("Dash onCreate");
    super.onCreate(savedInstanceState);

    ...
    // create fragments to use
    if (savedInstanceState != null) {
        mMessagesFragment = (MessagesFragment) getSupportFragmentManager().getFragment(
                savedInstanceState, MessagesFragment.class.getName());
                StreamsFragment.class.getName());
    }
    if (mMessagesFragment == null)
        mMessagesFragment = new MessagesFragment();
    ...
}
Oleksii Malovanyi
quelle
1
Ich frage mich jetzt: Soll ich einen ganz anderen Ansatz verwenden oder versuchen, Fragmente der Hauptaktivität (Dashboard) über onSavedInstancestate zu speichern, um sie in onCreate () zu verwenden? Gibt es eine geeignete Möglichkeit, diese Fragmente zu speichern und sie aus dem Bundle in onCreate zu erhalten? Sie scheinen nicht paketierbar zu sein ...
Oleksii Malovanyi
2
2. Ansatz funktioniert - siehe "Sulution". Aber es scheint ein hässlicher Code zu sein, nicht wahr?
Oleksii Malovanyi
1
Würde es Ihnen etwas ausmachen , Ihre Lösung als Antwort zu veröffentlichen und als ausgewählte zu markieren, um das Android-Tag zu bereinigen (Details hier: meta.stackexchange.com/questions/100529/… )? Auf diese Weise wird es nicht als unbeantwortete Frage
Alexander Lucas
1
Ja, denke es ist OK. Ich hoffte auf etwas Besseres als meins ...
Oleksii Malovanyi
1
Funktioniert die blöde Lösung überhaupt? Es gibt mir eine Nullzeiger-Ausnahme.
Zhen Liu

Antworten:

451

Wenn der FragmentPagerAdapterFragmentManager ein Fragment hinzufügt, wird ein spezielles Tag verwendet, das auf der bestimmten Position basiert, an der das Fragment platziert wird. FragmentPagerAdapter.getItem(int position)wird nur aufgerufen, wenn kein Fragment für diese Position vorhanden ist. Nach dem Drehen merkt Android, dass es bereits ein Fragment für diese bestimmte Position erstellt / gespeichert hat, und versucht daher einfach, sich wieder mit ihm zu verbinden FragmentManager.findFragmentByTag(), anstatt ein neues zu erstellen. All dies ist bei Verwendung von kostenlos FragmentPagerAdapterund daher ist es üblich, dass Ihr Fragmentinitialisierungscode in der getItem(int)Methode enthalten ist.

Selbst wenn wir a nicht verwenden FragmentPagerAdapter, ist es keine gute Idee, jedes Mal ein neues Fragment zu erstellen Activity.onCreate(Bundle). Wie Sie bemerkt haben, wird ein Fragment, das dem FragmentManager hinzugefügt wird, nach dem Drehen für Sie neu erstellt, und es muss nicht erneut hinzugefügt werden. Dies ist eine häufige Fehlerursache bei der Arbeit mit Fragmenten.

Ein üblicher Ansatz bei der Arbeit mit Fragmenten ist folgender:

protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    ...

    CustomFragment fragment;
    if (savedInstanceState != null) {
        fragment = (CustomFragment) getSupportFragmentManager().findFragmentByTag("customtag");
    } else {
        fragment = new CustomFragment();
        getSupportFragmentManager().beginTransaction().add(R.id.container, fragment, "customtag").commit(); 
    }

    ...

}

Bei Verwendung eines FragmentPagerAdapter wir die Fragmentverwaltung an den Adapter ab und müssen die obigen Schritte nicht ausführen. Standardmäßig wird nur ein Fragment vor und hinter der aktuellen Position vorgeladen (obwohl es sie nur zerstört, wenn Sie es verwenden FragmentStatePagerAdapter). Dies wird von ViewPager.setOffscreenPageLimit (int) gesteuert . Aus diesem Grund ist das direkte Aufrufen von Methoden für die Fragmente außerhalb des Adapters nicht garantiert, da sie möglicherweise nicht einmal aktiv sind.

Um es kurz zu machen, Ihre Lösung putFragment um später eine Referenz zu erhalten, ist nicht so verrückt und auch nicht so anders als die normale Art, Fragmente zu verwenden (siehe oben). Andernfalls ist es schwierig, eine Referenz zu erhalten, da das Fragment vom Adapter hinzugefügt wird und nicht Sie persönlich. Stellen Sie einfach sicher, dass das offscreenPageLimithoch genug ist, um Ihre gewünschten Fragmente jederzeit zu laden, da Sie sich darauf verlassen können, dass es vorhanden ist. Dies umgeht die Funktionen zum verzögerten Laden des ViewPager, scheint jedoch genau das zu sein, was Sie für Ihre Anwendung wünschen.

Ein anderer Ansatz ist das Überschreiben FragmentPageAdapter.instantiateItem(View, int) , einen Verweis auf das vom Superaufruf zurückgegebene Fragment und zu speichern, bevor es zurückgegeben wird (es hat die Logik, das Fragment zu finden, falls es bereits vorhanden ist).

Schauen Sie sich für ein vollständigeres Bild einige der Quellen von FragmentPagerAdapter (kurz) und ViewPager (lang) an.

antonyt
quelle
7
Liebte den letzten Teil. Hatte einen Cache für die Fragmente und verschob den Put in die Cache-Logik innerhalb der FragmentPageAdapter.instantiateItem(View, int). Schließlich wurde ein lang anhaltender Fehler behoben, der nur in der Rotations- / Konfigurationsänderung auftritt und mich verrückt machte ...
Carlos Sobrinho
2
Dies behebt ein Problem, das ich beim Rotationswechsel hatte. Es ist vielleicht so, dass ich es einfach nicht sehen kann, aber ist das dokumentiert? dh das Tag verwenden, um von einem vorherigen Zustand wiederherzustellen? Es ist vielleicht eine offensichtliche Antwort, aber ich bin ziemlich neu in der Android-Entwicklung.
1
@ Industrial-Antidepressivum Dies ist die ID des Containers (z. B. eines FrameLayout), dem das Fragment hinzugefügt werden soll.
Antonyt
1
Übrigens war es für mich FragmentPageAdapter.instantiateItem(ViewGroup, int)eher als FragmentPageAdapter.instantiateItem(View, int).
Anzeigename
1
Übrigens wissen Sie, welche (wenn überhaupt) Fragmentlebenszyklusmethode aufgerufen wird, wenn das Fragment vom Bildschirm gewischt wird? Ist es onDetach()oder etwas anderes?
Ygesher
36

Ich möchte eine Lösung anbieten, die antonytdie wunderbare Antwort und die Erwähnung des Überschreibens erweitert FragmentPageAdapter.instantiateItem(View, int), um Verweise auf erstellte zu speichern, Fragmentsdamit Sie später daran arbeiten können. Dies sollte auch funktionieren mit FragmentStatePagerAdapter; Einzelheiten finden Sie in den Anmerkungen.


Hier ist ein einfaches Beispiel dafür, wie Sie einen Verweis auf das Fragmentszurückgegebene erhalten FragmentPagerAdapter, der nicht von der internen tagsMenge auf dem abhängt Fragments. Der Schlüssel besteht darin instantiateItem(), Referenzen dort zu überschreiben und zu speichern, anstatt in getItem().

public class SomeActivity extends Activity {
    private FragmentA m1stFragment;
    private FragmentB m2ndFragment;

    // other code in your Activity...

    private class CustomPagerAdapter extends FragmentPagerAdapter {
        // other code in your custom FragmentPagerAdapter...

        public CustomPagerAdapter(FragmentManager fm) {
            super(fm);
        }

        @Override
        public Fragment getItem(int position) {
            // Do NOT try to save references to the Fragments in getItem(),
            // because getItem() is not always called. If the Fragment
            // was already created then it will be retrieved from the FragmentManger
            // and not here (i.e. getItem() won't be called again).
            switch (position) {
                case 0:
                    return new FragmentA();
                case 1:
                    return new FragmentB();
                default:
                    // This should never happen. Always account for each position above
                    return null;
            }
        }

        // Here we can finally safely save a reference to the created
        // Fragment, no matter where it came from (either getItem() or
        // FragmentManger). Simply save the returned Fragment from
        // super.instantiateItem() into an appropriate reference depending
        // on the ViewPager position.
        @Override
        public Object instantiateItem(ViewGroup container, int position) {
            Fragment createdFragment = (Fragment) super.instantiateItem(container, position);
            // save the appropriate reference depending on position
            switch (position) {
                case 0:
                    m1stFragment = (FragmentA) createdFragment;
                    break;
                case 1:
                    m2ndFragment = (FragmentB) createdFragment;
                    break;
            }
            return createdFragment;
        }
    }

    public void someMethod() {
        // do work on the referenced Fragments, but first check if they
        // even exist yet, otherwise you'll get an NPE.

        if (m1stFragment != null) {
            // m1stFragment.doWork();
        }

        if (m2ndFragment != null) {
            // m2ndFragment.doSomeWorkToo();
        }
    }
}

oder wenn Sie arbeiten lieber mit tagsstatt Klasse Membervariablen / Verweis auf die FragmentsSie kann auch den greifen tagsSatz von FragmentPagerAdapterauf die gleiche Weise: HINWEIS: dies gilt nicht , FragmentStatePagerAdapterda es nicht gesetzt ist , tagswenn seine Erstellung Fragments.

@Override
public Object instantiateItem(ViewGroup container, int position) {
    Fragment createdFragment = (Fragment) super.instantiateItem(container, position);
    // get the tags set by FragmentPagerAdapter
    switch (position) {
        case 0:
            String firstTag = createdFragment.getTag();
            break;
        case 1:
            String secondTag = createdFragment.getTag();
            break;
    }
    // ... save the tags somewhere so you can reference them later
    return createdFragment;
}

Beachten Sie, dass diese Methode NICHT auf der Nachahmung des internen tagSatzes durch beruht FragmentPagerAdapterund stattdessen geeignete APIs zum Abrufen verwendet. Auf diese Weise sind Sie auch dann sicher, wenn Sie tagÄnderungen in zukünftigen Versionen von vornehmen SupportLibrary.


Vergessen Sie nicht, dass das Design Activity, an dem FragmentsSie arbeiten möchten , je nach Design möglicherweise noch vorhanden ist oder nicht. Sie müssen dies berücksichtigen, indem Sie nullÜberprüfungen durchführen, bevor Sie Ihre Referenzen verwenden.

Wenn Sie stattdessen mit arbeiten FragmentStatePagerAdapter, möchten Sie keine harten Verweise auf Ihre behalten, Fragmentsda Sie möglicherweise viele davon haben und harte Verweise sie unnötigerweise im Speicher behalten würden. Speichern Sie stattdessen die FragmentReferenzen in WeakReferenceVariablen anstelle von Standardreferenzen. So was:

WeakReference<Fragment> m1stFragment = new WeakReference<Fragment>(createdFragment);
// ...and access them like so
Fragment firstFragment = m1stFragment.get();
if (firstFragment != null) {
    // reference hasn't been cleared yet; do work...
}
Tony Chan
quelle
Bei Speichermangel könnte Android den FragmentManager bitten, nicht verwendete Fragmente zu zerstören, oder? Wenn ich recht habe, wird Ihre zweite Version funktionieren, die erste könnte fehlschlagen. (Ich weiß nicht, ob aktuelle Android-Versionen dies tun, aber es scheint, dass wir das erwarten müssen.)
Moritz Beide
1
Was ist mit dem Erstellen einer FragmetPagerAdapterin onCreate of Activity bei jeder Bildschirmdrehung? Ist das falsch, weil es die Wiederverwendung bereits hinzugefügter Fragmente inFragmentPagerAdapter
Bahman
Überschreiben instantiateItem()ist der richtige Weg; Dies half mir, Bildschirmrotationen zu handhaben und meine vorhandenen Fragmentinstanzen abzurufen, sobald die Aktivität und der Adapter wieder aufgenommen wurden. Ich habe mir Kommentare im Code hinterlassen, um daran zu erinnern: Nach der Rotation getItem()wird NICHT aufgerufen; Nur diese Methode instantiateItem()wird aufgerufen. Die Super-Implementierung zum instantiateItem()tatsächlichen Anhängen von Fragmenten nach dem Drehen (nach Bedarf), anstatt neue Instanzen zu instanziieren!
HelloImKevo
Ich erhalte Fragment createdFragment = (Fragment) super.instantiateItem..in der ersten Lösung einen Nullzeiger .
AlexS
Nach dem Durchsuchen aller verschiedenen Duplikate / Varianten-Iterationen dieses Problems war dies die beste Lösung. (Querverweis von einem anderen Q mit einem relevanteren Titel, also danke dafür!)
MandisaW
18

Ich habe eine andere relativ einfache Lösung für Ihre Frage gefunden.

Wie Sie dem FragmentPagerAdapter-Quellcode entnehmen können , werden die vom FragmentPagerAdapterSpeicher verwalteten Fragmente FragmentManagerunter dem Tag generiert mit:

String tag="android:switcher:" + viewId + ":" + index;

Das viewIdist das container.getId(), das containerist deine ViewPagerInstanz. Das indexist die Position des Fragments. Daher können Sie die Objekt-ID unter folgender Adresse speichern outState:

@Override
protected void onSaveInstanceState(Bundle outState) {
    super.onSaveInstanceState(outState);
    outState.putInt("viewpagerid" , mViewPager.getId() );
}

@Override
    protected void onCreate(Bundle savedInstanceState) {
    setContentView(R.layout.activity_main);
    if (savedInstanceState != null)
        viewpagerid=savedInstanceState.getInt("viewpagerid", -1 );  

    MyFragmentPagerAdapter titleAdapter = new MyFragmentPagerAdapter (getSupportFragmentManager() , this);        
    mViewPager = (ViewPager) findViewById(R.id.pager);
    if (viewpagerid != -1 ){
        mViewPager.setId(viewpagerid);
    }else{
        viewpagerid=mViewPager.getId();
    }
    mViewPager.setAdapter(titleAdapter);

Wenn Sie mit diesem Fragment kommunizieren möchten, können Sie Folgendes abrufen FragmentManager, z.

getSupportFragmentManager().findFragmentByTag("android:switcher:" + viewpagerid + ":0")
user2110066
quelle
21
hmmm Ich denke nicht, dass dies ein guter Weg ist, da Sie auf eine interne Tag-Namenskonvention antworten, die keineswegs für immer gleich bleiben wird.
Dori
1
Wenn Sie nach einer Lösung suchen, die sich nicht auf das Interne tagstützt, sollten Sie meine Antwort ausprobieren .
Tony Chan
16

Ich möchte eine alternative Lösung für einen vielleicht etwas anderen Fall anbieten, da mich viele meiner Suchanfragen nach Antworten immer wieder zu diesem Thread geführt haben.

Mein Fall : Ich erstelle / füge Seiten dynamisch hinzu und schiebe sie in einen ViewPager, aber wenn ich sie drehe (onConfigurationChange), erhalte ich eine neue Seite, da OnCreate natürlich erneut aufgerufen wird. Ich möchte jedoch auf alle Seiten verweisen, die vor der Rotation erstellt wurden.

Problem - Ich habe keine eindeutigen Bezeichner für jedes von mir erstellte Fragment. Die einzige Möglichkeit zum Verweisen bestand darin, Verweise in einem Array zu speichern, das nach der Rotations- / Konfigurationsänderung wiederhergestellt werden soll.

Problemumgehung - Das Schlüsselkonzept bestand darin, dass die Aktivität (die die Fragmente anzeigt) auch das Array von Verweisen auf vorhandene Fragmente verwaltet, da diese Aktivität Bundles in onSaveInstanceState verwenden kann

public class MainActivity extends FragmentActivity

Innerhalb dieser Aktivität erkläre ich ein privates Mitglied, um die geöffneten Seiten zu verfolgen

private List<Fragment> retainedPages = new ArrayList<Fragment>();

Dies wird jedes Mal aktualisiert, wenn onSaveInstanceState in onCreate aufgerufen und wiederhergestellt wird

@Override
protected void onSaveInstanceState(Bundle outState) {
    retainedPages = _adapter.exportList();
    outState.putSerializable("retainedPages", (Serializable) retainedPages);
    super.onSaveInstanceState(outState);
}

... sobald es gespeichert ist, kann es abgerufen werden ...

@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    if (savedInstanceState != null) {
        retainedPages = (List<Fragment>) savedInstanceState.getSerializable("retainedPages");
    }
    _mViewPager = (CustomViewPager) findViewById(R.id.viewPager);
    _adapter = new ViewPagerAdapter(getApplicationContext(), getSupportFragmentManager());
    if (retainedPages.size() > 0) {
        _adapter.importList(retainedPages);
    }
    _mViewPager.setAdapter(_adapter);
    _mViewPager.setCurrentItem(_adapter.getCount()-1);
}

Dies waren die notwendigen Änderungen an der Hauptaktivität, und daher benötigte ich die Mitglieder und Methoden in meinem FragmentPagerAdapter, damit dies funktioniert

public class ViewPagerAdapter extends FragmentPagerAdapter

ein identisches Konstrukt (wie oben in MainActivity gezeigt)

private List<Fragment> _pages = new ArrayList<Fragment>();

und diese Synchronisierung (wie oben in onSaveInstanceState verwendet) wird speziell von den Methoden unterstützt

public List<Fragment> exportList() {
    return _pages;
}

public void importList(List<Fragment> savedPages) {
    _pages = savedPages;
}

Und schließlich in der Fragmentklasse

public class CustomFragment extends Fragment

Damit dies alles funktioniert, gab es zunächst zwei Änderungen

public class CustomFragment extends Fragment implements Serializable

und fügen Sie dies dann zu onCreate hinzu, damit Fragmente nicht zerstört werden

setRetainInstance(true);

Ich bin immer noch dabei, mich mit Fragmenten und dem Lebenszyklus von Android zu beschäftigen. Daher kann es bei dieser Methode zu Redundanzen / Ineffizienzen kommen. Aber es funktioniert für mich und ich hoffe, dass es für andere hilfreich sein kann, wenn meine Fälle ähnlich sind.

Frank Yin
quelle
2
+1 für setRetainInstance (true) - die schwarze Magie, nach der ich gesucht habe!
Deklination
Mit FragmentStatePagerAdapter (v13) war es noch einfacher. Welches Geschäft für Sie mit der Wiederherstellung und Freigabe des Zustands.
Der Metzger
Klare Beschreibung und sehr schöne Lösung. Vielen Dank!
Bruno Bieri
8

Meine Lösung ist sehr unhöflich, funktioniert aber: Da meine Fragmente dynamisch aus gespeicherten Daten erstellt werden, entferne ich einfach alle Fragmente aus dem PageAdaptervor dem Aufruf super.onSaveInstanceState()und erstelle sie bei der Aktivitätserstellung neu:

@Override
protected void onSaveInstanceState(Bundle outState) {
    outState.putInt("viewpagerpos", mViewPager.getCurrentItem() );
    mSectionsPagerAdapter.removeAllfragments();
    super.onSaveInstanceState(outState);
}

Sie können sie nicht entfernen onDestroy(), sonst erhalten Sie diese Ausnahme:

java.lang.IllegalStateException: Kann diese Aktion danach nicht mehr ausführen onSaveInstanceState

Hier der Code im Seitenadapter:

public void removeAllfragments()
{
    if ( mFragmentList != null ) {
        for ( Fragment fragment : mFragmentList ) {
            mFm.beginTransaction().remove(fragment).commit();
        }
        mFragmentList.clear();
        notifyDataSetChanged();
    }
}

Ich speichere die aktuelle Seite erst und stelle sie wieder her onCreate(), nachdem die Fragmente erstellt wurden.

if (savedInstanceState != null)
    mViewPager.setCurrentItem( savedInstanceState.getInt("viewpagerpos", 0 ) );  
Sir John
quelle
Ich weiß nicht warum, aber notifyDataSetChanged (); verursacht App zum Absturz
Malachiasz
4

Was ist das BasePagerAdapter? Sie sollten einen der Standard-Pager-Adapter verwenden - entweder FragmentPagerAdapteroder FragmentStatePagerAdapter, je nachdem, ob Fragmente, die von den nicht mehr benötigt werden ViewPager, entweder beibehalten (erstere) oder deren Status gespeichert (letztere) und neu erstellt werden sollen, wenn wieder gebraucht.

Beispielcode für die Verwendung ViewPagerfinden Sie hier

Es ist richtig, dass die Verwaltung von Fragmenten in einem Ansichtspager über Aktivitätsinstanzen hinweg etwas kompliziert ist, da FragmentManagerdas Framework dafür sorgt, dass der Status gespeichert und alle aktiven Fragmente wiederhergestellt werden, die der Pager erstellt hat. Das alles bedeutet wirklich, dass der Adapter beim Initialisieren sicherstellen muss, dass er sich wieder mit den wiederhergestellten Fragmenten verbindet. Sie können sich den Code ansehen FragmentPagerAdapteroder FragmentStatePagerAdaptersehen, wie dies gemacht wird.

Hackbod
quelle
1
Der BasePagerAdapter-Code ist in meiner Frage verfügbar. Wie man sehen kann, wird FragmentPagerAdapter einfach erweitert, um TitleProvider zu implementieren . Also hat schon alles mit Android Dev vorgeschlagen.
Oleksii Malovanyi
"FragmentStatePagerAdapter" war mir nicht bekannt. Du hast mir buchstäblich Stunden gespart. Vielen Dank.
Knossos
2

Wenn jemand Probleme mit seinem FragmentStatePagerAdapter hat, der den Status seiner Fragmente nicht ordnungsgemäß wiederherstellt ... dh ... werden vom FragmentStatePagerAdapter neue Fragmente erstellt, anstatt sie aus dem Status wiederherzustellen ...

ViewPager.setOffscreenPageLimit()Stellen Sie sicher, dass Sie anrufen, bevor Sie anrufenViewPager.setAdapter(fragmentStatePagerAdapter)

Beim Aufrufen ViewPager.setOffscreenPageLimit()... sucht der ViewPager sofort nach seinem Adapter und versucht, seine Fragmente abzurufen . Dies kann passieren, bevor der ViewPager die Möglichkeit hat, die Fragmente aus savedInstanceState wiederherzustellen (wodurch neue Fragmente erstellt werden, die nicht aus SavedInstanceState neu initialisiert werden können, weil sie neu sind).

dell116
quelle
0

Ich habe mir diese einfache und elegante Lösung ausgedacht. Es wird davon ausgegangen, dass die Aktivität für die Erstellung der Fragmente verantwortlich ist und der Adapter sie nur bedient.

Dies ist der Code des Adapters (nichts Seltsames hier, außer der Tatsache, dass mFragmentses sich um eine Liste von Fragmenten handelt, die von der Aktivität verwaltet werden).

class MyFragmentPagerAdapter extends FragmentStatePagerAdapter {

    public MyFragmentPagerAdapter(FragmentManager fm) {
        super(fm);
    }

    @Override
    public Fragment getItem(int position) {
        return mFragments.get(position);
    }

    @Override
    public int getCount() {
        return mFragments.size();
    }

    @Override
    public int getItemPosition(Object object) {
        return POSITION_NONE;
    }

    @Override
    public CharSequence getPageTitle(int position) {
        TabFragment fragment = (TabFragment)mFragments.get(position);
        return fragment.getTitle();
    }
} 

Das ganze Problem dieses Threads besteht darin, einen Verweis auf die "alten" Fragmente zu erhalten, daher verwende ich diesen Code in onCreate der Aktivität.

    if (savedInstanceState!=null) {
        if (getSupportFragmentManager().getFragments()!=null) {
            for (Fragment fragment : getSupportFragmentManager().getFragments()) {
                mFragments.add(fragment);
            }
        }
    }

Natürlich können Sie diesen Code bei Bedarf weiter optimieren, indem Sie beispielsweise sicherstellen, dass die Fragmente Instanzen einer bestimmten Klasse sind.

Merlevede
quelle
0

Um die Fragmente nach einer Orientierungsänderung zu erhalten, müssen Sie .getTag () verwenden.

    getSupportFragmentManager().findFragmentByTag("android:switcher:" + viewPagerId + ":" + positionOfItemInViewPager)

Für ein bisschen mehr Handhabung habe ich meine eigene ArrayList für meinen PageAdapter geschrieben, um das Fragment von viewPagerId und der FragmentClass an einer beliebigen Position abzurufen:

public class MyPageAdapter extends FragmentPagerAdapter implements Serializable {
private final String logTAG = MyPageAdapter.class.getName() + ".";

private ArrayList<MyPageBuilder> fragmentPages;

public MyPageAdapter(FragmentManager fm, ArrayList<MyPageBuilder> fragments) {
    super(fm);
    fragmentPages = fragments;
}

@Override
public Fragment getItem(int position) {
    return this.fragmentPages.get(position).getFragment();
}

@Override
public CharSequence getPageTitle(int position) {
    return this.fragmentPages.get(position).getPageTitle();
}

@Override
public int getCount() {
    return this.fragmentPages.size();
}


public int getItemPosition(Object object) {
    //benötigt, damit bei notifyDataSetChanged alle Fragemnts refrehsed werden

    Log.d(logTAG, object.getClass().getName());
    return POSITION_NONE;
}

public Fragment getFragment(int position) {
    return getItem(position);
}

public String getTag(int position, int viewPagerId) {
    //getSupportFragmentManager().findFragmentByTag("android:switcher:" + R.id.shares_detail_activity_viewpager + ":" + myViewPager.getCurrentItem())

    return "android:switcher:" + viewPagerId + ":" + position;
}

public MyPageBuilder getPageBuilder(String pageTitle, int icon, int selectedIcon, Fragment frag) {
    return new MyPageBuilder(pageTitle, icon, selectedIcon, frag);
}


public static class MyPageBuilder {

    private Fragment fragment;

    public Fragment getFragment() {
        return fragment;
    }

    public void setFragment(Fragment fragment) {
        this.fragment = fragment;
    }

    private String pageTitle;

    public String getPageTitle() {
        return pageTitle;
    }

    public void setPageTitle(String pageTitle) {
        this.pageTitle = pageTitle;
    }

    private int icon;

    public int getIconUnselected() {
        return icon;
    }

    public void setIconUnselected(int iconUnselected) {
        this.icon = iconUnselected;
    }

    private int iconSelected;

    public int getIconSelected() {
        return iconSelected;
    }

    public void setIconSelected(int iconSelected) {
        this.iconSelected = iconSelected;
    }

    public MyPageBuilder(String pageTitle, int icon, int selectedIcon, Fragment frag) {
        this.pageTitle = pageTitle;
        this.icon = icon;
        this.iconSelected = selectedIcon;
        this.fragment = frag;
    }
}

public static class MyPageArrayList extends ArrayList<MyPageBuilder> {
    private final String logTAG = MyPageArrayList.class.getName() + ".";

    public MyPageBuilder get(Class cls) {
        // Fragment über FragmentClass holen
        for (MyPageBuilder item : this) {
            if (item.fragment.getClass().getName().equalsIgnoreCase(cls.getName())) {
                return super.get(indexOf(item));
            }
        }
        return null;
    }

    public String getTag(int viewPagerId, Class cls) {
        // Tag des Fragment unabhängig vom State z.B. nach bei Orientation change
        for (MyPageBuilder item : this) {
            if (item.fragment.getClass().getName().equalsIgnoreCase(cls.getName())) {
                return "android:switcher:" + viewPagerId + ":" + indexOf(item);
            }
        }
        return null;
    }
}

Erstellen Sie einfach eine MyPageArrayList mit den Fragmenten:

    myFragPages = new MyPageAdapter.MyPageArrayList();

    myFragPages.add(new MyPageAdapter.MyPageBuilder(
            getString(R.string.widget_config_data_frag),
            R.drawable.ic_sd_storage_24dp,
            R.drawable.ic_sd_storage_selected_24dp,
            new WidgetDataFrag()));

    myFragPages.add(new MyPageAdapter.MyPageBuilder(
            getString(R.string.widget_config_color_frag),
            R.drawable.ic_color_24dp,
            R.drawable.ic_color_selected_24dp,
            new WidgetColorFrag()));

    myFragPages.add(new MyPageAdapter.MyPageBuilder(
            getString(R.string.widget_config_textsize_frag),
            R.drawable.ic_settings_widget_24dp,
            R.drawable.ic_settings_selected_24dp,
            new WidgetTextSizeFrag()));

und fügen Sie sie dem viewPager hinzu:

    mAdapter = new MyPageAdapter(getSupportFragmentManager(), myFragPages);
    myViewPager.setAdapter(mAdapter);

Danach können Sie nach der Orientierung das richtige Fragment mithilfe seiner Klasse ändern:

        WidgetDataFrag dataFragment = (WidgetDataFrag) getSupportFragmentManager()
            .findFragmentByTag(myFragPages.getTag(myViewPager.getId(), WidgetDataFrag.class));
Schnorrer
quelle
-30

hinzufügen:

   @SuppressLint("ValidFragment")

vor deiner Klasse.

es funktioniert nicht so etwas:

@SuppressLint({ "ValidFragment", "HandlerLeak" })
andro_stackoverflow
quelle