ViewPager y fragmentos: ¿cuál es la forma correcta de almacenar el estado del fragmento?

492

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 ViewPagersu 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 ViewPagercon 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 ViewPagerfragmentos. El código (que encontrará a continuación) dice que cada vez que se crea la actividad trato de crear un nuevo ViewPageradaptador 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();
    ...
}
Oleksii Malovanyi
fuente
1
Ahora me pregunto: ¿debería usar un enfoque muy diferente o tratar de guardar fragmentos de la actividad principal (Panel) a través de onSavedInstancestate para usarlos en onCreate (). ¿Hay una manera adecuada de guardar esos fragmentos y obtenerlos del paquete en onCreate? No parecen ser parcelable ...
Oleksii Malovanyi
2
El segundo enfoque funciona - ver "Sulution". Pero parece ser un código feo, ¿no es así?
Oleksii Malovanyi
1
Por el esfuerzo de limpiar la etiqueta de Android (detalles aquí: meta.stackexchange.com/questions/100529/… ), ¿le importaría publicar su solución como respuesta y marcarla como la seleccionada? De esa manera no aparecerá como una pregunta sin respuesta :)
Alexander Lucas
1
Sí, creo que está bien. Esperaba algo mejor que el mío ...
Oleksii Malovanyi
1
¿Funciona la solución tonta? Me da una excepción de puntero nulo ..
Zhen Liu

Respuestas:

451

Cuando FragmentPagerAdapteragrega 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 él FragmentManager.findFragmentByTag(), en lugar de crear uno nuevo. Todo esto es gratuito cuando se usa FragmentPagerAdaptery es por eso que es habitual tener su código de inicialización de fragmento dentro del getItem(int)método.

Incluso si no estuviéramos usando un FragmentPagerAdapter, no es una buena idea crear un nuevo fragmento cada vez Activity.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:

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(); 
    }

    ...

}

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é usando FragmentStatePagerAdapter). 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 putFragmentpoder 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 que offscreenPageLimitsea ​​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).

antonyt
fuente
77
Me encantó la última parte. Tenía un caché para los fragmentos y movió la lógica de caché dentro de 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 ...
Carlos Sobrino
2
Esto solucionó un problema que estaba teniendo sobre el cambio de rotación. Quizás es que no puedo ver por mirar, pero ¿está documentado? es decir, usar la etiqueta para recuperarse de un estado anterior? Tal vez sea una respuesta obvia, pero soy bastante nuevo en el desarrollo de Android.
1
@ Industrial-antidepressant Es la identificación del contenedor (por ejemplo, un FrameLayout) al que se debe agregar el Fragmento.
antonyt
1
por cierto, para mí fue FragmentPageAdapter.instantiateItem(ViewGroup, int)más que FragmentPageAdapter.instantiateItem(View, int).
Nombre para mostrar
1
Por cierto, ¿sabe qué (si alguno) método de ciclo de vida del fragmento se llama cuando el fragmento se quita de la pantalla? ¿Es onDetach()o algo más?
ygesher
36

Quiero ofrecer una solución que amplíe antonytla maravillosa respuesta y la mención de anulación FragmentPageAdapter.instantiateItem(View, int)para guardar referencias creadas Fragmentspara que pueda trabajar en ellas más adelante. Esto también debería funcionar con FragmentStatePagerAdapter; ver notas para más detalles.


Aquí hay un ejemplo simple de cómo obtener una referencia al Fragmentsdevuelto FragmentPagerAdapterque no se basa en el tagsconjunto interno del Fragments. La clave es anular instantiateItem()y guardar referencias allí en lugar de adentro 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 tagsvariables / referencias del miembro de la clase, Fragmentstambién puede tomar el tagsconjunto de FragmentPagerAdapterla misma manera: NOTA: esto no se aplica FragmentStatePagerAdapterya que no se establece tagsal crear su 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 el tagconjunto interno FragmentPagerAdaptery, en cambio, utiliza las API adecuadas para recuperarlos. De esta manera, incluso si los tagcambios en las versiones futuras de la SupportLibrarytodavía estará a salvo.


No olvides que dependiendo del diseño de tu Activity, el Fragmentsque estás tratando de trabajar puede o no existir aún, así que tienes que dar cuenta de eso haciendo nullcontroles antes de usar tus referencias.

Además, si en cambio está trabajando FragmentStatePagerAdapter, entonces no desea mantener referencias duras a su persona Fragmentsporque podría tener muchas de ellas y las referencias duras las mantendrían innecesariamente en la memoria. En su lugar, guarde las Fragmentreferencias en WeakReferencevariables 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...
}
Tony Chan
fuente
en la escasez de memoria, Android podría pedirle al FragmentManager que destruya fragmentos no utilizados, ¿verdad? Si estoy en lo cierto, su segunda versión funcionará, la primera podría fallar. (No sé si las versiones actuales de Android hacen esto, pero parece que tenemos que esperar eso.)
Moritz Ambos
1
¿Qué pasa con la creación de un FragmetPagerAdapteronCreate 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
Bahman
Anular 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; solo instantiateItem()se llama a este método . ¡La súper implementación para instantiateItem()volver a unir fragmentos después de la rotación (según sea necesario), en lugar de crear instancias nuevas!
HelloImKevo
Estoy obteniendo un puntero nulo Fragment createdFragment = (Fragment) super.instantiateItem..en la primera solución.
AlexS
Después de buscar en todas las iteraciones duplicadas / variantes de este problema, esta fue la mejor solución. (Referencia cruzada de otra Q con un título más relevante, ¡así que gracias por eso!)
MandisaW
18

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 FragmentPagerAdaptertienda en FragmentManagerla etiqueta generada mediante:

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

El viewIdes el container.getId(), el containeres su ViewPagerinstancia. El indexes la posición del fragmento. Por lo tanto, puede guardar la identificación del objeto en 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);

Si desea comunicarse con este fragmento, puede obtenerlo de FragmentManager, como:

getSupportFragmentManager().findFragmentByTag("android:switcher:" + viewpagerid + ":0")
user2110066
fuente
21
hmmm No creo que esta sea una buena manera de responder a una convención de nomenclatura interna de etiquetas que de ninguna manera se mantendrá igual para siempre.
Dori
1
Para aquellos que buscan una solución que no se base en lo interno tag, considere probar mi respuesta .
Tony Chan
16

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

public class MainActivity extends FragmentActivity

Entonces, dentro de esta Actividad, declaro un miembro privado para rastrear las páginas abiertas

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

Esto se actualiza cada vez que se llama a onSaveInstanceState y se restaura en onCreate

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

... así que una vez que se almacena, se puede recuperar ...

@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);
}

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

public class ViewPagerAdapter extends FragmentPagerAdapter

Una construcción idéntica (como se muestra arriba en MainActivity)

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

y esta sincronización (como se usa arriba en onSaveInstanceState) es compatible específicamente con los métodos

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

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

Y finalmente, en la clase de fragmentos.

public class CustomFragment extends Fragment

Para que todo esto funcione, hubo dos cambios, primero

public class CustomFragment extends Fragment implements Serializable

y luego agregar esto a onCreate para que los fragmentos no se destruyan

setRetainInstance(true);

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.

Frank Yin
fuente
2
+1 para setRetainInstance (verdadero): ¡la magia negra que estaba buscando!
declinación
Fue aún más fácil usando FragmentStatePagerAdapter (v13). Qué trato para usted con el estado restaurar y liberar.
The Butcher
Descripción clara y muy buena solución. ¡Gracias!
Bruno Bieri
8

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 PageAdapterantes de llamar super.onSaveInstanceState()y luego los vuelvo a crear en la creación de la actividad:

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

No puede eliminarlos onDestroy(), de lo contrario obtendrá esta excepción:

java.lang.IllegalStateException: No se puede realizar esta acción después de onSaveInstanceState

Aquí el código en el adaptador de página:

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

Solo guardo la página actual y la restauro una onCreate()vez que se han creado los fragmentos.

if (savedInstanceState != null)
    mViewPager.setCurrentItem( savedInstanceState.getInt("viewpagerpos", 0 ) );  
Sir John
fuente
No sé por qué, pero notifyDataSetChanged (); hace que la aplicación se bloquee
Malachiasz
4

¿Qué es eso BasePagerAdapter? Debe usar uno de los adaptadores de buscapersonas estándar, ya sea FragmentPagerAdaptero bien FragmentStatePagerAdapter, dependiendo de si desea que los Fragmentos que ya no sean necesarios ViewPagerse 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 ViewPagerse 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 FragmentManageren 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 para FragmentPagerAdaptero FragmentStatePagerAdapterpara ver cómo se hace esto.

hackbod
fuente
1
El código BasePagerAdapter está disponible en mi pregunta. Como se puede ver, simplemente extiende FragmentPagerAdapter para implementar TitleProvider . Entonces, todo ya ha estado funcionando con el enfoque sugerido por el desarrollador de Android.
Oleksii Malovanyi
No estaba al tanto de "FragmentStatePagerAdapter". Me salvaste literalmente horas. Gracias.
Knossos
2

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).

dell116
fuente
0

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 mFragmentses una lista de fragmentos mantenidos por la Actividad)

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();
    }
} 

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.

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

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.

Merlevede
fuente
0

Para obtener los fragmentos después del cambio de orientación, debe usar .getTag ().

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

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:

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;
    }
}

Así que solo crea una MyPageArrayList con los fragmentos:

    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()));

y agregarlos a viewPager:

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

después de esto, puede obtener después de que la orientación cambie el fragmento correcto utilizando su clase:

        WidgetDataFrag dataFragment = (WidgetDataFrag) getSupportFragmentManager()
            .findFragmentByTag(myFragPages.getTag(myViewPager.getId(), WidgetDataFrag.class));
Gorrón
fuente
-30

añadir:

   @SuppressLint("ValidFragment")

antes de tu clase.

no funciona hacer algo como esto:

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