android - viewpager - fragmentstatepageradapter
Cómo obtener fragmentos existentes al usar FragmentPagerAdapter (13)
Resumen del problema
Nota: En esta respuesta, voy a hacer referencia a FragmentPagerAdapter
y su código fuente. Pero la solución general también debería aplicarse a FragmentStatePagerAdapter
.
Si estás leyendo esto, probablemente ya sepas que FragmentPagerAdapter
/ FragmentStatePagerAdapter
está destinado a crear Fragments
para tu ViewPager
, pero después de la recreación de Actividad (ya sea desde una rotación de dispositivo o el sistema matando tu Aplicación para recuperar memoria) estos Fragments
no serán creados de nuevo, pero en su lugar se recuperaron sus instancias de FragmentManager
. Ahora di que tu Activity
necesita una referencia a estos Fragments
para hacer un trabajo sobre ellos. No tiene una id
o tag
para estos Fragments
creados porque FragmentPagerAdapter
configuró internamente . Entonces, el problema es cómo obtener una referencia sin esa información ...
Problema con las soluciones actuales: confiar en el código interno
Muchas de las soluciones que he visto en esta y otras preguntas similares se basan en obtener una referencia al Fragment
existente llamando a FragmentManager.findFragmentByTag()
e imitando la etiqueta creada internamente: "android:switcher:" + viewId + ":" + id
. El problema con esto es que estás confiando en el código fuente interno, que, como todos sabemos, no se garantiza que permanezca igual para siempre. Los ingenieros de Android en Google podrían decidir cambiar la estructura de las tag
, lo que rompería su código y no podría encontrar una referencia a los Fragments
existentes.
Solución alternativa sin depender de la tag
interna
Aquí hay un ejemplo simple de cómo obtener una referencia a los Fragments
devueltos por FragmentPagerAdapter
que no depende de las tags
internas establecidas en los Fragments
. La clave es anular instantiateItem()
y guardar referencias allí en lugar de en 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();
}
}
}
o si prefiere trabajar con tags
lugar de variables de miembro de clase / referencias a los Fragments
, también puede tomar las tags
establecidas por FragmentPagerAdapter
de la misma manera: NOTA: esto no se aplica a FragmentStatePagerAdapter
ya que no establece tags
al crear sus 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;
}
Tenga en cuenta que este método NO se basa en imitar la tag
interna establecida por FragmentPagerAdapter
y en su lugar utiliza las API adecuadas para recuperarlos. De esta forma, incluso si la tag
cambia en futuras versiones de SupportLibrary
, estará a salvo.
No olvide que, dependiendo del diseño de su Activity
, los Fragments
que está tratando de trabajar pueden o no existir aún, por lo que tiene que dar cuenta de ello haciendo comprobaciones null
antes de usar sus referencias.
Además, si en su lugar está trabajando con FragmentStatePagerAdapter
, entonces no desea mantener referencias difíciles a sus Fragments
porque podría tener muchos de ellos y las referencias difíciles los mantendrían innecesariamente en la memoria. En su lugar, guarde las referencias al Fragment
en WeakReference
variables WeakReference
en lugar de las estándar. Me gusta esto:
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...
}
Tengo problemas para comunicar mis fragmentos entre sí a través de la Activity
, que usa FragmentPagerAdapter
, como una clase auxiliar que implementa la administración de pestañas y todos los detalles de la conexión de un ViewPager
con el TabHost
asociado. Implementé FragmentPagerAdapter
misma manera que lo proporciona el proyecto de ejemplo de Android Support4Demos .
La pregunta principal es ¿cómo puedo obtener un fragmento particular de FragmentManager
cuando no tengo Id o Tag? FragmentPagerAdapter
está creando los fragmentos y auto generando el Id y las Etiquetas.
Creé este método que me funciona para obtener una referencia al fragmento actual.
public static Fragment getCurrentFragment(ViewPager pager, FragmentPagerAdapter adapter) {
try {
Method m = adapter.getClass().getSuperclass().getDeclaredMethod("makeFragmentName", int.class, long.class);
Field f = adapter.getClass().getSuperclass().getDeclaredField("mFragmentManager");
f.setAccessible(true);
FragmentManager fm = (FragmentManager) f.get(adapter);
m.setAccessible(true);
String tag = null;
tag = (String) m.invoke(null, pager.getId(), (long) pager.getCurrentItem());
return fm.findFragmentByTag(tag);
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalArgumentException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (NoSuchFieldException e) {
e.printStackTrace();
}
return null;
}
El principal obstáculo para manejar los fragmentos es que no puedes confiar en getItem (). Después de un cambio de orientación, las referencias a los fragmentos serán nulas y getItem () no se volverá a llamar.
Aquí hay un enfoque que no depende de la implementación de FragmentPagerAdapter para obtener la etiqueta. Reemplazar instantiateItem () que devolverá el fragmento creado a partir de getItem () o encontrado desde el administrador de fragmentos.
@Override
public Object instantiateItem(ViewGroup container, int position) {
Object value = super.instantiateItem(container, position);
if (position == 0) {
someFragment = (SomeFragment) value;
} else if (position == 1) {
anotherFragment = (AnotherFragment) value;
}
return value;
}
He encontrado una respuesta a mi pregunta basada en la siguiente publicación: reutilización de fragmentos en un fragmento de página
Pocas cosas que he aprendido:
-
getItem(int position)
enFragmentPagerAdapter
es un nombre bastante engañoso de lo que realmente hace este método. Crea nuevos fragmentos, no devuelve los existentes. En este sentido, el método debe renombrarse a algo comocreateItem(int position)
en el Android SDK. Entonces este método no nos ayuda a obtener fragmentos. - De acuerdo con la explicación en la referencia de FragmentPagerAdapterholds a los fragmentos anteriores , debe dejar la creación de los fragmentos en
FragmentPagerAdapter
y significa que no tiene referencia a los Fragmentos o sus etiquetas. Sin embargo, si tiene una etiqueta de fragmento, puede recuperar fácilmente la referencia desdeFragmentManager
llamando afindFragmentByTag()
. Necesitamos una forma de descubrir la etiqueta de un fragmento en la posición de página dada.
Solución
Agregue el siguiente método de ayuda en su clase para recuperar la etiqueta de fragmento y enviarla al método findFragmentByTag()
.
private String getFragmentTag(int viewPagerId, int fragmentPosition)
{
return "android:switcher:" + viewPagerId + ":" + fragmentPosition;
}
¡NOTA! Este es el método idéntico que utiliza FragmentPagerAdapter
al crear nuevos fragmentos. Ver este enlace http://code.google.com/p/openintents/source/browse/trunk/compatibility/AndroidSupportV2/src/android/support/v2/app/FragmentPagerAdapter.java#104
La forma en que lo hice es definir un Hashtable de WeakReferences de la siguiente manera:
protected Hashtable<Integer, WeakReference<Fragment>> fragmentReferences;
Luego escribí el método getItem () como este:
@Override
public Fragment getItem(int position) {
Fragment fragment;
switch(position) {
case 0:
fragment = new MyFirstFragmentClass();
break;
default:
fragment = new MyOtherFragmentClass();
break;
}
fragmentReferences.put(position, new WeakReference<Fragment>(fragment));
return fragment;
}
Entonces puedes escribir un método:
public Fragment getFragment(int fragmentId) {
WeakReference<Fragment> ref = fragmentReferences.get(fragmentId);
return ref == null ? null : ref.get();
}
Esto parece funcionar bien y me parece un poco menos hacky que el
"android:switcher:" + viewId + ":" + position
truco, ya que no depende de cómo se implementa FragmentPagerAdapter. Por supuesto, si el fragmento ha sido liberado por FragmentPagerAdapter o si aún no se ha creado, getFragment devolverá nulo.
Si alguien encuentra algo mal con este enfoque, los comentarios son más que bienvenidos.
Logré resolver este problema usando identificadores en lugar de etiquetas. (Estoy usando Definí FragmentStatePagerAdapter que usa mis Fragmentos personalizados en los que superé el método onAttach, donde guarda el id en alguna parte:
@Override
public void onAttach(Context context){
super.onAttach(context);
MainActivity.fragId = getId();
}
Y luego simplemente accedes al fragmento fácilmente dentro de la actividad:
Fragment f = getSupportFragmentManager.findFragmentById(fragId);
No estoy seguro si mi método fue la mejor o la mejor manera de hacerlo, ya que soy un principiante relativo con Java / Android, pero funcionó (estoy seguro de que viola los principios orientados a objetos, pero ninguna otra solución funcionó para mi caso de uso).
Tenía una actividad de alojamiento que usaba una ViewPager con un FragmentStatePagerAdapter. Para obtener referencias a los Fragmentos que fueron creados por FragmentStatePagerAdapter, creé una interfaz de devolución de llamada dentro de la clase de fragmento:
public interface Callbacks {
public void addFragment (Fragment fragment);
public void removeFragment (Fragment fragment);
}
En la actividad de alojamiento, implementé la interfaz y creé un LinkedHasSet para hacer un seguimiento de los fragmentos:
public class HostingActivity extends AppCompatActivity implements ViewPagerFragment.Callbacks {
private LinkedHashSet<Fragment> mFragments = new LinkedHashSet<>();
@Override
public void addFragment (Fragment fragment) {
mFragments.add(fragment);
}
@Override
public void removeFragment (Fragment fragment) {
mFragments.remove(fragment);
}
}
Dentro de la clase ViewPagerFragment agregué los fragmentos a la lista dentro de unir y los eliminé dentro deDetach:
public class ViewPagerFragment extends Fragment {
private Callbacks mCallbacks;
public interface Callbacks {
public void addFragment (Fragment fragment);
public void removeFragment (Fragment fragment);
}
@Override
public void onAttach (Context context) {
super.onAttach(context);
mCallbacks = (Callbacks) context;
// Add this fragment to the HashSet in the hosting activity
mCallbacks.addFragment(this);
}
@Override
public void onDetach() {
super.onDetach();
// Remove this fragment from the HashSet in the hosting activity
mCallbacks.removeFragment(this);
mCallbacks = null;
}
}
Dentro de la actividad de alojamiento, ahora podrá usar mFragmentos para recorrer los fragmentos que existen actualmente en FragmentStatePagerAdapter.
No sé si este es el mejor enfoque, pero nada más funcionó para mí. Todas las demás opciones, incluido getActiveFragment, devolvieron null o provocaron el bloqueo de la aplicación.
Noté que en la rotación de la pantalla el fragmento estaba siendo adjuntado, así que lo usé para enviar el fragmento a la actividad.
En el fragmento:
@Override
public void onAttach(Activity activity) {
super.onAttach(activity);
try {
mListener = (OnListInteractionListener) activity;
mListener.setListFrag(this);
} catch (ClassCastException e) {
throw new ClassCastException(activity.toString()
+ " must implement OnFragmentInteractionListener");
}
}
Luego en la actividad:
@Override
public void setListFrag(MyListFragment lf) {
if (mListFragment == null) {
mListFragment = lf;
}
}
Y finalmente en la actividad onCreate ():
if (savedInstanceState != null) {
if (mListFragment != null)
mListFragment.setListItems(items);
}
Este enfoque asocia el fragmento visible real a la actividad sin crear una nueva.
Siempre uso esta clase base cuando necesito acceder a fragmentos secundarios o fragmentos primarios (actualmente visibles). No se basa en ningún detalle de implementación y se ocupa de los cambios en el ciclo de vida, ya que los métodos sobreescritos se llaman en ambos casos: cuando se crea una nueva instancia de fragmento y cuando se recibe una instancia de FragmentManager.
public abstract class FragmentPagerAdapterExt extends FragmentPagerAdapter {
private final ArrayList<Fragment> mFragments;
private Fragment mPrimaryFragment;
public FragmentPagerAdapterExt(FragmentManager fm) {
super(fm);
mFragments = new ArrayList<>(getCount());
}
@Override public Object instantiateItem(ViewGroup container, int position) {
Object object = super.instantiateItem(container, position);
mFragments.add((Fragment) object);
return object;
}
@Override public void destroyItem(ViewGroup container, int position, Object object) {
mFragments.remove(object);
super.destroyItem(container, position, object);
}
@Override public void setPrimaryItem(ViewGroup container, int position, Object object) {
super.setPrimaryItem(container, position, object);
mPrimaryFragment = (Fragment) object;
}
/** Returns currently visible (primary) fragment */
public Fragment getPrimaryFragment() {
return mPrimaryFragment;
}
/** Returned list can contain null-values for not created fragments */
public List<Fragment> getFragments() {
return Collections.unmodifiableList(mFragments);
}
}
Solo sigue probando este código,
public class MYFragmentPAdp extends FragmentPagerAdapter {
public MYFragmentPAdp(FragmentManager fm) {
super(fm);
}
@Override
public int getCount() {
return 2;
}
@Override
public Fragment getItem(int position) {
if (position == 0)
Fragment fragment = new Fragment1();
else (position == 1)
Fragment fragment = new Fragment2();
return fragment;
}
}
Vea esta publicación sobre la devolución de fragmentos de FragmentPagerAdapter. Confía en que conozca el índice de su fragmento, pero esto se establecería en getItem () (solo en la creación de instancias)
la solución sugerida por @ personne3000 es agradable, pero tiene un problema: cuando la actividad pasa al segundo plano y el sistema la mata (para obtener algo de memoria libre) y luego se restaura, el fragmentReferences
getItem
estará vacío, porque getItem
'' t ser llamado.
La clase a continuación maneja tal situación:
public abstract class AbstractHolderFragmentPagerAdapter<F extends Fragment> extends FragmentPagerAdapter {
public static final String FRAGMENT_SAVE_PREFIX = "holder";
private final FragmentManager fragmentManager; // we need to store fragment manager ourselves, because parent''s field is private and has no getters.
public AbstractHolderFragmentPagerAdapter(FragmentManager fm) {
super(fm);
fragmentManager = fm;
}
private SparseArray<WeakReference<F>> holder = new SparseArray<WeakReference<F>>();
protected void holdFragment(F fragment) {
holdFragment(holder.size(), fragment);
}
protected void holdFragment(int position, F fragment) {
if (fragment != null)
holder.put(position, new WeakReference<F>(fragment));
}
public F getHoldedItem(int position) {
WeakReference<F> ref = holder.get(position);
return ref == null ? null : ref.get();
}
public int getHolderCount() {
return holder.size();
}
@Override
public void restoreState(Parcelable state, ClassLoader loader) { // code inspired by Google''s FragmentStatePagerAdapter implementation
super.restoreState(state, loader);
Bundle bundle = (Bundle) state;
for (String key : bundle.keySet()) {
if (key.startsWith(FRAGMENT_SAVE_PREFIX)) {
int index = Integer.parseInt(key.substring(FRAGMENT_SAVE_PREFIX.length()));
Fragment f = fragmentManager.getFragment(bundle, key);
holdFragment(index, (F) f);
}
}
}
@Override
public Parcelable saveState() {
Bundle state = (Bundle) super.saveState();
if (state == null)
state = new Bundle();
for (int i = 0; i < holder.size(); i++) {
int id = holder.keyAt(i);
final F f = getHoldedItem(i);
String key = FRAGMENT_SAVE_PREFIX + id;
fragmentManager.putFragment(state, key, f);
}
return state;
}
}
no es necesario que anule instantiateItem
ni confíe en la compatibilidad de crear etiquetas de fragmento con el método interno makeFragmentName
. instantiateItem
es un método público , por lo que puede (y en realidad debería ) onCreate
en el método de onCreate
de su actividad para obtener referencias a instancias de sus fragmentos y almacenarlos en vars locales si lo necesita. Solo recuerde rodear un conjunto de llamadas a instantiateItem
con los métodos startUpdate
y finishUpdate
como se describe en PagerAdapter
javadoc :
Una llamada al método PagerAdapter startUpdate (ViewGroup) indica que el contenido de ViewPager está a punto de cambiar. Seguirá una o más llamadas a instanciar el elemento (ViewGroup, int) y / o destroyItem (ViewGroup, int, Object), y el final de una actualización se señalará mediante una llamada a finishUpdate (ViewGroup).
Entonces, por ejemplo, esta es la manera de almacenar referencias a sus fragmentos de pestañas en el método onCreate
:
public class MyActivity extends AppCompatActivity {
Fragment0 tab0; Fragment1 tab1;
@Override protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.myLayout);
ViewPager viewPager = (ViewPager) findViewById(R.id.myViewPager);
MyPagerAdapter adapter = new MyPagerAdapter(getSupportFragmentManager());
viewPager.setAdapter(adapter);
TabLayout tabLayout = (TabLayout) findViewById(R.id.tabs);
tabLayout.setupWithViewPager(viewPager);
adapter.startUpdate(viewPager);
tab0 = (Fragment0) adapter.instantiateItem(viewPager, 0);
tab1 = (Fragment1) adapter.instantiateItem(viewPager, 1);
adapter.finishUpdate(viewPager);
}
class MyPagerAdapter extends FragmentPagerAdapter {
public MyPagerAdapter(FragmentManager manager) {super(manager);}
@Override public int getCount() {return 2;}
@Override public Fragment getItem(int position) {
if (position == 0) return new Fragment0();
if (position == 1) return new Fragment1();
return null; // or throw some exception
}
@Override public CharSequence getPageTitle(int position) {
if (position == 0) return getString(R.string.tab0);
if (position == 1) return getString(R.string.tab1);
return null; // or throw some exception
}
}
}
instantiateItem
primero intentará obtener referencias de instancias de fragmentos existentes desde FragmentManager
. Solo si aún no existen, creará nuevos usando el método getItem
de su adaptador y los "almacenará" en el FragmentManager
para cualquier uso futuro.
Alguna información adicional:
Si no llama a instantiateItem
rodeado por startUpdate
/ finishUpdate
en su método onCreate
, onCreate
riesgo de que sus instancias de fragmentos nunca se comprometan con FragmentManager
: cuando su actividad se convierte en primer plano, se llamará instantiateItem
automáticamente para obtener sus fragmentos, pero startUpdate
/ finishUpdate
puede que no (dependiendo de los detalles de implementación) y lo que básicamente hacen es comenzar / confirmar una FragmentTransaction
.
Esto puede hacer que las referencias a las instancias de fragmentos creados se pierdan muy rápidamente (por ejemplo, al rotar la pantalla) y se recreen con mucha más frecuencia de la necesaria. Dependiendo de qué tan "pesados" sean sus fragmentos, puede tener consecuencias de rendimiento no despreciables.
Más importante aún, en tal caso, las instancias de fragmentos almacenados en vars locales quedarán obsoletas: dado que la plataforma Android no pudo obtener las mismas instancias de FragmentManager
, puede crear y usar nuevas, mientras que sus vars seguirán haciendo referencia a los antiguos.