Fragmente scheinen sehr schön für die Aufteilung der UI-Logik in einige Module zu sein. Aber zusammen mit ViewPager
seinem 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 ViewPager
mit 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 ViewPager
Fragmente der Aktivität . Der Code (den Sie unten finden) besagt, dass ich jedes Mal, wenn die Aktivität erstellt wird, versuche, einen neuen ViewPager
Fragmentadapter 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();
...
}
quelle
Antworten:
Wenn der
FragmentPagerAdapter
FragmentManager 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 verbindenFragmentManager.findFragmentByTag()
, anstatt ein neues zu erstellen. All dies ist bei Verwendung von kostenlosFragmentPagerAdapter
und daher ist es üblich, dass Ihr Fragmentinitialisierungscode in dergetItem(int)
Methode enthalten ist.Selbst wenn wir a nicht verwenden
FragmentPagerAdapter
, ist es keine gute Idee, jedes Mal ein neues Fragment zu erstellenActivity.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:
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 verwendenFragmentStatePagerAdapter
). 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 dasoffscreenPageLimit
hoch 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.
quelle
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 ...FragmentPageAdapter.instantiateItem(ViewGroup, int)
eher alsFragmentPageAdapter.instantiateItem(View, int)
.onDetach()
oder etwas anderes?Ich möchte eine Lösung anbieten, die
antonyt
die wunderbare Antwort und die Erwähnung des Überschreibens erweitertFragmentPageAdapter.instantiateItem(View, int)
, um Verweise auf erstellte zu speichern,Fragments
damit Sie später daran arbeiten können. Dies sollte auch funktionieren mitFragmentStatePagerAdapter
; Einzelheiten finden Sie in den Anmerkungen.Hier ist ein einfaches Beispiel dafür, wie Sie einen Verweis auf das
Fragments
zurückgegebene erhaltenFragmentPagerAdapter
, der nicht von der internentags
Menge auf dem abhängtFragments
. Der Schlüssel besteht darininstantiateItem()
, Referenzen dort zu überschreiben und zu speichern, anstatt ingetItem()
.oder wenn Sie arbeiten lieber mit
tags
statt Klasse Membervariablen / Verweis auf dieFragments
Sie kann auch den greifentags
Satz vonFragmentPagerAdapter
auf die gleiche Weise: HINWEIS: dies gilt nicht ,FragmentStatePagerAdapter
da es nicht gesetzt ist ,tags
wenn seine ErstellungFragments
.Beachten Sie, dass diese Methode NICHT auf der Nachahmung des internen
tag
Satzes durch beruhtFragmentPagerAdapter
und stattdessen geeignete APIs zum Abrufen verwendet. Auf diese Weise sind Sie auch dann sicher, wenn Sietag
Änderungen in zukünftigen Versionen von vornehmenSupportLibrary
.Vergessen Sie nicht, dass das Design
Activity
, an demFragments
Sie arbeiten möchten , je nach Design möglicherweise noch vorhanden ist oder nicht. Sie müssen dies berücksichtigen, indem Sienull
Überprüfungen durchführen, bevor Sie Ihre Referenzen verwenden.Wenn Sie stattdessen mit arbeiten
FragmentStatePagerAdapter
, möchten Sie keine harten Verweise auf Ihre behalten,Fragments
da Sie möglicherweise viele davon haben und harte Verweise sie unnötigerweise im Speicher behalten würden. Speichern Sie stattdessen dieFragment
Referenzen inWeakReference
Variablen anstelle von Standardreferenzen. So was:quelle
FragmetPagerAdapter
in onCreate of Activity bei jeder Bildschirmdrehung? Ist das falsch, weil es die Wiederverwendung bereits hinzugefügter Fragmente inFragmentPagerAdapter
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 RotationgetItem()
wird NICHT aufgerufen; Nur diese MethodeinstantiateItem()
wird aufgerufen. Die Super-Implementierung zuminstantiateItem()
tatsächlichen Anhängen von Fragmenten nach dem Drehen (nach Bedarf), anstatt neue Instanzen zu instanziieren!Fragment createdFragment = (Fragment) super.instantiateItem..
in der ersten Lösung einen Nullzeiger .Ich habe eine andere relativ einfache Lösung für Ihre Frage gefunden.
Wie Sie dem FragmentPagerAdapter-Quellcode entnehmen können , werden die vom
FragmentPagerAdapter
Speicher verwalteten FragmenteFragmentManager
unter dem Tag generiert mit:Das
viewId
ist dascontainer.getId()
, dascontainer
ist deineViewPager
Instanz. Dasindex
ist die Position des Fragments. Daher können Sie die Objekt-ID unter folgender Adresse speichernoutState
:Wenn Sie mit diesem Fragment kommunizieren möchten, können Sie Folgendes abrufen
FragmentManager
, z.quelle
tag
stützt, sollten Sie meine Antwort ausprobieren .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
Innerhalb dieser Aktivität erkläre ich ein privates Mitglied, um die geöffneten Seiten zu verfolgen
Dies wird jedes Mal aktualisiert, wenn onSaveInstanceState in onCreate aufgerufen und wiederhergestellt wird
... sobald es gespeichert ist, kann es abgerufen werden ...
Dies waren die notwendigen Änderungen an der Hauptaktivität, und daher benötigte ich die Mitglieder und Methoden in meinem FragmentPagerAdapter, damit dies funktioniert
ein identisches Konstrukt (wie oben in MainActivity gezeigt)
und diese Synchronisierung (wie oben in onSaveInstanceState verwendet) wird speziell von den Methoden unterstützt
Und schließlich in der Fragmentklasse
Damit dies alles funktioniert, gab es zunächst zwei Änderungen
und fügen Sie dies dann zu onCreate hinzu, damit Fragmente nicht zerstört werden
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.
quelle
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
PageAdapter
vor dem Aufrufsuper.onSaveInstanceState()
und erstelle sie bei der Aktivitätserstellung neu:Sie können sie nicht entfernen
onDestroy()
, sonst erhalten Sie diese Ausnahme:java.lang.IllegalStateException:
Kann diese Aktion danach nicht mehr ausführenonSaveInstanceState
Hier der Code im Seitenadapter:
Ich speichere die aktuelle Seite erst und stelle sie wieder her
onCreate()
, nachdem die Fragmente erstellt wurden.quelle
Was ist das
BasePagerAdapter
? Sie sollten einen der Standard-Pager-Adapter verwenden - entwederFragmentPagerAdapter
oderFragmentStatePagerAdapter
, je nachdem, ob Fragmente, die von den nicht mehr benötigt werdenViewPager
, entweder beibehalten (erstere) oder deren Status gespeichert (letztere) und neu erstellt werden sollen, wenn wieder gebraucht.Beispielcode für die Verwendung
ViewPager
finden Sie hierEs ist richtig, dass die Verwaltung von Fragmenten in einem Ansichtspager über Aktivitätsinstanzen hinweg etwas kompliziert ist, da
FragmentManager
das 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 ansehenFragmentPagerAdapter
oderFragmentStatePagerAdapter
sehen, wie dies gemacht wird.quelle
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).quelle
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
mFragments
es sich um eine Liste von Fragmenten handelt, die von der Aktivität verwaltet werden).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.
Natürlich können Sie diesen Code bei Bedarf weiter optimieren, indem Sie beispielsweise sicherstellen, dass die Fragmente Instanzen einer bestimmten Klasse sind.
quelle
Um die Fragmente nach einer Orientierungsänderung zu erhalten, müssen Sie .getTag () verwenden.
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:
Erstellen Sie einfach eine MyPageArrayList mit den Fragmenten:
und fügen Sie sie dem viewPager hinzu:
Danach können Sie nach der Orientierung das richtige Fragment mithilfe seiner Klasse ändern:
quelle
hinzufügen:
vor deiner Klasse.
es funktioniert nicht so etwas:
quelle