Los fragmentos parecen ser muy buenos para la separación de la lógica de la interfaz de usuario en algunos módulos. Pero junto con ViewPager
su ciclo de vida todavía está nublado para mí. ¡Entonces los pensamientos de Guru son muy necesarios!
Editar
Vea la solución tonta a continuación ;-)
Alcance
La actividad principal tiene una ViewPager
con fragmentos. Esos fragmentos podrían implementar una lógica un poco diferente para otras actividades (principales), por lo que los datos de los fragmentos se llenan a través de una interfaz de devolución de llamada dentro de la actividad. Y todo funciona bien en el primer lanzamiento, pero ...
Problema
Cuando la actividad se recrea (por ejemplo, en el cambio de orientación), también lo hacen los ViewPager
fragmentos. El código (que encontrará a continuación) dice que cada vez que se crea la actividad trato de crear un nuevo ViewPager
adaptador de fragmentos igual que los fragmentos (tal vez este es el problema) pero FragmentManager ya tiene todos estos fragmentos almacenados en algún lugar (¿dónde?) Y comienza el mecanismo de recreación para aquellos. Entonces, el mecanismo de recreación llama a onAttach, onCreateView, etc. del fragmento "antiguo" con mi llamada de interfaz de devolución de llamada para iniciar datos a través del método implementado de la Actividad. Pero este método apunta al fragmento recién creado que se crea a través del método onCreate de la actividad.
Problema
Tal vez estoy usando patrones incorrectos, pero incluso el libro Android 3 Pro no tiene mucho al respecto. Entonces, por favor , dame un golpe dos y señala cómo hacerlo de la manera correcta. ¡Muchas gracias!
Código
Actividad principal
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 helper
public class BasePagerActivity extends FragmentActivity {
BasePagerAdapter mPagerAdapter;
ViewPager mPager;
}
Adaptador
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
}
}
Fragmento
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... */
}
Solución
La solución tonta es guardar los fragmentos dentro de onSaveInstanceState (de la actividad del host) con putFragment y obtenerlos dentro de onCreate a través de getFragment. Pero todavía tengo la extraña sensación de que las cosas no deberían funcionar así ... Ver el código a continuación:
@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();
...
}
fuente
Respuestas:
Cuando
FragmentPagerAdapter
agrega un fragmento al FragmentManager, utiliza una etiqueta especial basada en la posición particular en la que se colocará el fragmento.FragmentPagerAdapter.getItem(int position)
solo se llama cuando no existe un fragmento para esa posición. Después de rotar, Android notará que ya creó / guardó un fragmento para esta posición en particular y, por lo tanto, simplemente intenta volver a conectarse con élFragmentManager.findFragmentByTag()
, en lugar de crear uno nuevo. Todo esto es gratuito cuando se usaFragmentPagerAdapter
y es por eso que es habitual tener su código de inicialización de fragmento dentro delgetItem(int)
método.Incluso si no estuviéramos usando un
FragmentPagerAdapter
, no es una buena idea crear un nuevo fragmento cada vezActivity.onCreate(Bundle)
. Como habrá notado, cuando se agrega un fragmento al FragmentManager, se volverá a crear después de la rotación y no es necesario volver a agregarlo. Hacerlo es una causa común de errores al trabajar con fragmentos.Un enfoque habitual cuando se trabaja con fragmentos es este:
Cuando se utiliza a
FragmentPagerAdapter
, cedemos la gestión de fragmentos al adaptador y no tenemos que realizar los pasos anteriores. De forma predeterminada, solo precargará un Fragmento delante y detrás de la posición actual (aunque no los destruye a menos que lo esté usandoFragmentStatePagerAdapter
). Esto está controlado por ViewPager.setOffscreenPageLimit (int) . Debido a esto, no se garantiza que los métodos de llamada directa en los fragmentos fuera del adaptador sean válidos, ya que es posible que ni siquiera estén vivos.Para abreviar una larga historia, su solución para
putFragment
poder obtener una referencia después no es tan loca, y no muy diferente a la forma normal de usar fragmentos de todos modos (arriba). De lo contrario, es difícil obtener una referencia porque el adaptador agrega el fragmento y no usted personalmente. Solo asegúrate de queoffscreenPageLimit
sea lo suficientemente alto como para cargar tus fragmentos deseados en todo momento, ya que confías en que esté presente. Esto evita las capacidades de carga lenta de ViewPager, pero parece ser lo que desea para su aplicación.Otro enfoque es anular
FragmentPageAdapter.instantiateItem(View, int)
y guardar una referencia al fragmento devuelto por la súper llamada antes de devolverlo (tiene la lógica para encontrar el fragmento, si ya está presente).Para una imagen más completa, eche un vistazo a algunas de las fuentes de FragmentPagerAdapter (corto) y ViewPager (largo).
fuente
FragmentPageAdapter.instantiateItem(View, int)
. Por último corregido un error larga duración que sólo aparece en la rotación / cambio de config y me estaba volviendo loca ...FragmentPageAdapter.instantiateItem(ViewGroup, int)
más queFragmentPageAdapter.instantiateItem(View, int)
.onDetach()
o algo más?Quiero ofrecer una solución que amplíe
antonyt
la maravillosa respuesta y la mención de anulaciónFragmentPageAdapter.instantiateItem(View, int)
para guardar referencias creadasFragments
para que pueda trabajar en ellas más adelante. Esto también debería funcionar conFragmentStatePagerAdapter
; ver notas para más detalles.Aquí hay un ejemplo simple de cómo obtener una referencia al
Fragments
devueltoFragmentPagerAdapter
que no se basa en eltags
conjunto interno delFragments
. La clave es anularinstantiateItem()
y guardar referencias allí en lugar de adentrogetItem()
.o si prefiere trabajar con
tags
variables / referencias del miembro de la clase,Fragments
también puede tomar eltags
conjunto deFragmentPagerAdapter
la misma manera: NOTA: esto no se aplicaFragmentStatePagerAdapter
ya que no se establecetags
al crear suFragments
.Tenga en cuenta que este método NO se basa en imitar el
tag
conjunto internoFragmentPagerAdapter
y, en cambio, utiliza las API adecuadas para recuperarlos. De esta manera, incluso si lostag
cambios en las versiones futuras de laSupportLibrary
todavía estará a salvo.No olvides que dependiendo del diseño de tu
Activity
, elFragments
que estás tratando de trabajar puede o no existir aún, así que tienes que dar cuenta de eso haciendonull
controles antes de usar tus referencias.Además, si en cambio está trabajando
FragmentStatePagerAdapter
, entonces no desea mantener referencias duras a su personaFragments
porque podría tener muchas de ellas y las referencias duras las mantendrían innecesariamente en la memoria. En su lugar, guarde lasFragment
referencias enWeakReference
variables en lugar de las estándar. Me gusta esto:fuente
FragmetPagerAdapter
onCreate of Activity en cada rotación de pantalla? Es este mal, ya que podrá eludir la reutilización de fragmentos ya se han agregado enFragmentPagerAdapter
instantiateItem()
es el camino a seguir; esto me ayudó a manejar las rotaciones de pantalla y recuperar mis instancias de Fragment existentes una vez que se reanudaron la Actividad y el Adaptador; Me dejé comentarios en el código como recordatorio: después de la rotación,getItem()
NO se invoca; soloinstantiateItem()
se llama a este método . ¡La súper implementación parainstantiateItem()
volver a unir fragmentos después de la rotación (según sea necesario), en lugar de crear instancias nuevas!Fragment createdFragment = (Fragment) super.instantiateItem..
en la primera solución.Encontré otra solución relativamente fácil para su pregunta.
Como puede ver en el código fuente de FragmentPagerAdapter , los fragmentos gestionados por la
FragmentPagerAdapter
tienda enFragmentManager
la etiqueta generada mediante:El
viewId
es elcontainer.getId()
, elcontainer
es suViewPager
instancia. Elindex
es la posición del fragmento. Por lo tanto, puede guardar la identificación del objeto enoutState
:Si desea comunicarse con este fragmento, puede obtenerlo de
FragmentManager
, como:fuente
tag
, considere probar mi respuesta .Quiero ofrecer una solución alternativa para quizás un caso ligeramente diferente, ya que muchas de mis búsquedas de respuestas me llevaron a este hilo.
Mi caso : estoy creando / agregando páginas dinámicamente y deslizándolas en un ViewPager, pero cuando se gira (onConfigurationChange) termino con una nueva página porque, por supuesto, se vuelve a llamar a OnCreate. Pero quiero mantener una referencia a todas las páginas que se crearon antes de la rotación.
Problema : no tengo identificadores únicos para cada fragmento que creo, por lo que la única forma de referencia era almacenar referencias de alguna manera en una matriz para restaurarlas después del cambio de rotación / configuración.
Solución alternativa : el concepto clave era que la Actividad (que muestra los Fragmentos) también administre la matriz de referencias a los Fragmentos existentes, ya que esta actividad puede utilizar Bundles en onSaveInstanceState
Entonces, dentro de esta Actividad, declaro un miembro privado para rastrear las páginas abiertas
Esto se actualiza cada vez que se llama a onSaveInstanceState y se restaura en onCreate
... así que una vez que se almacena, se puede recuperar ...
Estos fueron los cambios necesarios en la actividad principal, por lo que necesitaba los miembros y los métodos dentro de mi FragmentPagerAdapter para que esto funcione, así que dentro de
Una construcción idéntica (como se muestra arriba en MainActivity)
y esta sincronización (como se usa arriba en onSaveInstanceState) es compatible específicamente con los métodos
Y finalmente, en la clase de fragmentos.
Para que todo esto funcione, hubo dos cambios, primero
y luego agregar esto a onCreate para que los fragmentos no se destruyan
Todavía estoy en el proceso de comprender el ciclo de vida de Fragmentos y Android, por lo que tengo la advertencia de que puede haber redundancias / ineficiencias en este método. Pero funciona para mí y espero que sea útil para otros con casos similares al mío.
fuente
Mi solución es muy grosera pero funciona: al ser mis fragmentos creados dinámicamente a partir de los datos retenidos, simplemente elimino todos los fragmentos de
PageAdapter
antes de llamarsuper.onSaveInstanceState()
y luego los vuelvo a crear en la creación de la actividad:No puede eliminarlos
onDestroy()
, de lo contrario obtendrá esta excepción:java.lang.IllegalStateException:
No se puede realizar esta acción después deonSaveInstanceState
Aquí el código en el adaptador de página:
Solo guardo la página actual y la restauro una
onCreate()
vez que se han creado los fragmentos.fuente
¿Qué es eso
BasePagerAdapter
? Debe usar uno de los adaptadores de buscapersonas estándar, ya seaFragmentPagerAdapter
o bienFragmentStatePagerAdapter
, dependiendo de si desea que los Fragmentos que ya no sean necesariosViewPager
se mantengan (los primeros) o se guarden su estado (los últimos) y se vuelvan a crear si Necesitado de nuevo.El código de muestra para usar
ViewPager
se puede encontrar aquíEs cierto que la gestión de fragmentos en un localizador de vistas a través de instancias de actividad es un poco complicada, porque
FragmentManager
en el marco se encarga de salvar el estado y restaurar cualquier fragmento activo que haya realizado el localizador. Todo esto realmente significa que el adaptador cuando se inicializa debe asegurarse de que se vuelva a conectar con los fragmentos restaurados que haya. Usted puede mirar en el código paraFragmentPagerAdapter
oFragmentStatePagerAdapter
para ver cómo se hace esto.fuente
Si alguien tiene problemas con su FragmentStatePagerAdapter que no restaura adecuadamente el estado de sus fragmentos ... es decir ... el FragmentStatePagerAdapter crea nuevos Fragmentos en lugar de restaurarlos desde el estado ...
Asegúrese de llamar
ViewPager.setOffscreenPageLimit()
ANTES de llamarViewPager.setAdapter(fragmentStatePagerAdapter)
Al llamar
ViewPager.setOffscreenPageLimit()
... el ViewPager buscará inmediatamente su adaptador e intentará obtener sus fragmentos. Esto podría suceder antes de que ViewPager tenga la oportunidad de restaurar los Fragmentos de savedInstanceState (creando así nuevos Fragmentos que no pueden reinicializarse desde SavedInstanceState porque son nuevos).fuente
Se me ocurrió esta solución simple y elegante. Asume que la actividad es responsable de crear los Fragmentos, y el Adaptador solo los sirve.
Este es el código del adaptador (nada raro aquí, excepto por el hecho de que
mFragments
es una lista de fragmentos mantenidos por la Actividad)Todo el problema de este hilo es obtener una referencia de los fragmentos "viejos", así que uso este código en onCreate de la actividad.
Por supuesto, puede ajustar aún más este código si es necesario, por ejemplo, asegurándose de que los fragmentos sean instancias de una clase en particular.
fuente
Para obtener los fragmentos después del cambio de orientación, debe usar .getTag ().
Para un poco más de manejo, escribí mi propia ArrayList para mi PageAdapter para obtener el fragmento de viewPagerId y FragmentClass en cualquier posición:
Así que solo crea una MyPageArrayList con los fragmentos:
y agregarlos a viewPager:
después de esto, puede obtener después de que la orientación cambie el fragmento correcto utilizando su clase:
fuente
añadir:
antes de tu clase.
no funciona hacer algo como esto:
fuente