android example - Diferencia entre FragmentPagerAdapter y FragmentStatePagerAdapter




vs notifydatasetchanged (6)

¿Cuál es la diferencia entre FragmentPagerAdapter y FragmentStatePagerAdapter ?

Acerca de FragmentPagerAdapter La guía de Google dice:

Esta versión del buscapersonas es mejor para usar cuando hay un puñado de fragmentos típicamente estáticos, como un conjunto de pestañas. El fragmento de cada página que visiten los usuarios se guardará en la memoria, aunque su jerarquía de vistas puede destruirse cuando no esté visible. Esto puede resultar en el uso de una cantidad significativa de memoria, ya que las instancias de fragmentos pueden conservar una cantidad arbitraria de estado. Para conjuntos de páginas más grandes, considere FragmentStatePagerAdapter.

Y sobre FragmentStatePagerAdapter :

Esta versión del buscapersonas es más útil cuando hay una gran cantidad de páginas, que funcionan más como una vista de lista. Cuando las páginas no son visibles para el usuario, su fragmento completo puede ser destruido, solo manteniendo el estado guardado de ese fragmento. Esto permite que el buscapersonas retenga mucha menos memoria asociada con cada página visitada en comparación con FragmentPagerAdapter, lo que podría generar una sobrecarga mayor al cambiar de página.

Así que solo tengo 3 fragmentos. Pero todos ellos son módulos separados con una gran cantidad de datos.

Fragment1 maneja algunos datos (que ingresan los usuarios) y los pasa a través de la actividad a Fragment2 , que es solo un simple ListFragment . Fragment3 también es un ListFragment .

Así que mis preguntas son : ¿Qué adaptador debo usar? FragmentPagerAdapter o FragmentStatePagerAdapter ?


Answers

  • FragmentPagerAdapter almacena todo el fragmento en la memoria, y podría aumentar la sobrecarga de memoria si se utiliza una gran cantidad de fragmentos en ViewPager .

  • Al contrario que su hermano, FragmentStatePagerAdapter solo almacena el estado de la instancia guardada de los fragmentos, y destruye todos los fragmentos cuando pierden el enfoque.

  • Por lo tanto, FragmentStatePagerAdapter debe usarse cuando tenemos que usar fragmentos dinámicos, como fragmentos con widgets, ya que sus datos podrían almacenarse en el estado de savedInstanceState guardado. savedInstanceState afectará el rendimiento incluso si hay una gran cantidad de fragmentos.

  • Por el contrario, su hermano FragmentPagerAdapter debe usarse cuando necesitamos almacenar todo el fragmento en la memoria.

  • Cuando digo que todo el fragmento se guarda en la memoria, significa que sus instancias no serán destruidas y crearían una sobrecarga de memoria. Por lo tanto, se recomienda usar FragmentPagerAdapter solo cuando hay un número bajo de fragmentos para ViewPager .

  • Sería aún mejor si los fragmentos son estáticos, ya que no tendrían una gran cantidad de objetos cuyas instancias se almacenarían.

Para ser mas detallista,

FragmentStatePagerAdapter:

  • con FragmentStatePagerAdapter , su fragmento innecesario se destruye. Se compromete una transacción para eliminar completamente el fragmento del FragmentManager de su actividad.

  • El estado en FragmentStatePagerAdapter proviene del hecho de que guardará el Bundle de su fragmento de savedInstanceState cuando se savedInstanceState Cuando el usuario navega hacia atrás, el nuevo fragmento se restaurará usando el estado del fragmento.

FragmentPagerAdapter:

  • Por comparación, FragmentPagerAdapter no hace nada de ese tipo. Cuando el fragmento ya no es necesario. FragmentPagerAdapter llama a detach(Fragment) en la transacción en lugar de remove(Fragment) .

  • Esta destrucción es la vista del fragmento, pero deja la instancia del fragmento viva en el FragmentManager los fragmentos creados en el FragmentPagerAdapter nunca se destruyen.


Como dicen los doctores, piénsalo de esta manera. Si tuviera que hacer una aplicación como un lector de libros, no querrá cargar todos los fragmentos en la memoria a la vez. Le gustaría cargar y destruir Fragments medida que el usuario lee. En este caso utilizará FragmentStatePagerAdapter . Si solo está mostrando 3 "pestañas" que no contienen una gran cantidad de datos pesados ​​(como Bitmaps ), FragmentPagerAdapter podría serle de utilidad. Además, tenga en cuenta que ViewPager por defecto cargará 3 fragmentos en la memoria. El primer Adapter que mencione podría destruir la jerarquía de View y volver a cargarlo cuando sea necesario, el segundo Adapter solo guarda el estado del Fragment y lo destruye completamente, si el usuario vuelve a esa página, se recupera el estado.


Algo que no se dice explícitamente en la documentación o en las respuestas en esta página (aunque implícito en @Naruto), es que FragmentPagerAdapter no actualizará los Fragmentos si los datos en el Fragmento cambian porque mantiene el Fragmento en la memoria.

Entonces, incluso si tiene un número limitado de Fragmentos para mostrar, si desea poder actualizar sus fragmentos (por ejemplo, vuelva a ejecutar la consulta para actualizar el listView en el Fragmento), necesita usar FragmentStatePagerAdapter.

Mi punto aquí es que el número de Fragmentos y si son o no similares no siempre es el aspecto clave a considerar. Si tus fragmentos son dinámicos también es clave.


FragmentStatePagerAdapter = Para acomodar una gran cantidad de fragmentos en ViewPager. Como este adaptador destruye el fragmento cuando no es visible para el usuario y solo se guarda el estado de fragmento guardado del fragmento para su uso posterior. De esta manera, se utiliza una cantidad baja de memoria y se entrega un mejor rendimiento en caso de fragmentos dinámicos.


Aquí hay un ciclo de vida de registro de cada fragmento en ViewPager que tiene 4 fragmentos y offscreenPageLimit = 1 (default value)

FragmentStatePagerAdapter

Ir a Fragment1 (actividad de lanzamiento)

Fragment1: onCreateView
Fragment1: onStart
Fragment2: onCreateView
Fragment2: onStart

Ir a Fragment2

Fragment3: onCreateView
Fragment3: onStart

Ir a Fragment3

Fragment1: onStop
Fragment1: onDestroyView
Fragment1: onDestroy
Fragment1: onDetach
Fragment4: onCreateView
Fragment4: onStart

Ir a Fragment4

Fragment2: onStop
Fragment2: onDestroyView
Fragment2: onDestroy

FragmentPagerAdapter

Ir a Fragment1 (actividad de lanzamiento)

Fragment1: onCreateView
Fragment1: onStart
Fragment2: onCreateView
Fragment2: onStart

Ir a Fragment2

Fragment3: onCreateView
Fragment3: onStart

Ir a Fragment3

Fragment1: onStop
Fragment1: onDestroyView
Fragment4: onCreateView
Fragment4: onStart

Ir a Fragment4

Fragment2: onStop
Fragment2: onDestroyView

Conclusión : FragmentStatePagerAdapter llama onDestroy cuando se supera el Fragmento offscreenPageLimit onDestroy mientras que FragmentPagerAdapter no.

Nota : creo que deberíamos usar FragmentStatePagerAdapter para un ViewPager que tiene mucha página porque será bueno para el rendimiento.

Ejemplo de offscreenPageLimit :

Si vamos a Fragment3, destruirá Fragmento 1 (o Fragmento 5 si tiene) porque offscreenPageLimit = 1 . Si configuramos offscreenPageLimit > 1 no se destruirá.
Si en este ejemplo, configuramos offscreenPageLimit=4 , no hay ninguna diferencia entre usar FragmentStatePagerAdapter o FragmentPagerAdapter porque Fragment nunca llama onDestroyView y onDestroy cuando cambiamos la pestaña

Demo de Github aquí


Ante todo:

  • Esta solución supone que los elementos que aún están visibles, después de que se cambió el conjunto de datos, también se deslizan hacia la derecha y luego vuelven a deslizarse desde la parte inferior nuevamente (al menos eso es lo que entendí que está pidiendo)
  • Debido a este requisito, no pude encontrar una solución fácil y agradable para este problema (al menos durante la primera iteración). La única forma que encontré fue engañar al adaptador y luchar contra el marco para hacer algo para lo que no estaba destinado. Esta es la razón por la que la primera parte (Cómo funciona normalmente) describe cómo lograr animaciones agradables con RecyclerView manera predeterminada . La segunda parte describe la solución sobre cómo imponer la salida / diapositiva en animación para todos los elementos después de que el conjunto de datos haya cambiado.
  • Más adelante, encontré una mejor solución que no requiere engañar al adaptador con identificadores aleatorios (saltar a la parte inferior de la versión actualizada).

Como funciona normalmente

Para habilitar animaciones, debe decirle al RecyclerView cómo cambió el conjunto de datos (para que sepa qué tipo de animaciones deben ejecutarse). Esto se puede hacer de dos formas:

1) Versión simple: Necesitamos configurar adapter.setHasStableIds(true); y proporcionar los identificadores de sus artículos a través de public long getItemId(int position) en su Adapter al RecyclerView . RecyclerView utiliza estos identificadores para averiguar qué elementos se eliminaron / agregaron / movieron durante la llamada a adapter.notifyDataSetChanged();

2) Versión avanzada: en lugar de llamar a adapter.notifyDataSetChanged(); También puede indicar explícitamente cómo cambió el conjunto de datos. El Adapter proporciona varios métodos, como adapter.notifyItemChanged(int position) , adapter.notifyItemInserted(int position) , ... para describir los cambios en el conjunto de datos

Las animaciones que se activan para reflejar los cambios en el conjunto de datos son administradas por el ItemAnimator . RecyclerView ya está equipado con un buen valor predeterminado DefaultItemAnimator . Además, es posible definir un comportamiento de animación personalizado con un ItemAnimator personalizado.

Estrategia para implementar el deslizar hacia fuera (derecha), deslizar hacia adentro (abajo)

La diapositiva a la derecha es la animación que se debe reproducir si los elementos se eliminan del conjunto de datos. La diapositiva desde la animación inferior debe reproducirse para los elementos que se agregaron al conjunto de datos. Como mencioné al principio, asumo que es deseable que todos los elementos se deslicen hacia la derecha y se deslicen desde la parte inferior. Incluso si son visibles antes y después del cambio de conjunto de datos. Normalmente, RecyclerView jugaría para cambiar / mover animaciones para los elementos que permanecen visibles. Sin embargo, debido a que queremos utilizar la animación de quitar / agregar para todos los elementos, necesitamos engañar al adaptador para que piense que solo hay elementos nuevos después del cambio y que se eliminaron todos los elementos previamente disponibles. Esto se puede lograr proporcionando una identificación aleatoria para cada elemento en el adaptador:

@Override
public long getItemId(int position) {
    return Math.round(Math.random() * Long.MAX_VALUE);
}

Ahora debemos proporcionar un ItemAnimator personalizado que administre las animaciones para los elementos agregados / eliminados. La estructura del SlidingAnimator presentado es muy similar al android.support.v7.widget.DefaultItemAnimator que se proporciona con RecyclerView . También tenga en cuenta que esto es una prueba de concepto y debe ajustarse antes de utilizarlo en cualquier aplicación:

public class SlidingAnimator extends SimpleItemAnimator {
    List<RecyclerView.ViewHolder> pendingAdditions = new ArrayList<>();
    List<RecyclerView.ViewHolder> pendingRemovals = new ArrayList<>();

    @Override
    public void runPendingAnimations() {
        final List<RecyclerView.ViewHolder> additionsTmp = pendingAdditions;
        List<RecyclerView.ViewHolder> removalsTmp = pendingRemovals;
        pendingAdditions = new ArrayList<>();
        pendingRemovals = new ArrayList<>();

        for (RecyclerView.ViewHolder removal : removalsTmp) {
            // run the pending remove animation
            animateRemoveImpl(removal);
        }
        removalsTmp.clear();

        if (!additionsTmp.isEmpty()) {
            Runnable adder = new Runnable() {
                public void run() {
                    for (RecyclerView.ViewHolder addition : additionsTmp) {
                        // run the pending add animation
                        animateAddImpl(addition);
                    }
                    additionsTmp.clear();
                }
            };
            // play the add animation after the remove animation finished
            ViewCompat.postOnAnimationDelayed(additionsTmp.get(0).itemView, adder, getRemoveDuration());
        }
    }

    @Override
    public boolean animateAdd(RecyclerView.ViewHolder holder) {
        pendingAdditions.add(holder);
        // translate the new items vertically so that they later slide in from the bottom
        holder.itemView.setTranslationY(300);
        // also make them invisible
        holder.itemView.setAlpha(0);
        // this requests the execution of runPendingAnimations()
        return true;
    }

    @Override
    public boolean animateRemove(final RecyclerView.ViewHolder holder) {
        pendingRemovals.add(holder);
        // this requests the execution of runPendingAnimations()
        return true;
    }

    private void animateAddImpl(final RecyclerView.ViewHolder holder) {
        View view = holder.itemView;
        final ViewPropertyAnimatorCompat anim = ViewCompat.animate(view);
        anim
                // undo the translation we applied in animateAdd
                .translationY(0)
                // undo the alpha we applied in animateAdd
                .alpha(1)
                .setDuration(getAddDuration())
                .setInterpolator(new DecelerateInterpolator())
                .setListener(new ViewPropertyAnimatorListener() {
                    @Override
                    public void onAnimationStart(View view) {
                        dispatchAddStarting(holder);
                    }

                    @Override
                    public void onAnimationEnd(View view) {
                        anim.setListener(null);
                        dispatchAddFinished(holder);
                        // cleanup
                        view.setTranslationY(0);
                        view.setAlpha(1);
                    }

                    @Override
                    public void onAnimationCancel(View view) {
                    }
                }).start();
    }

    private void animateRemoveImpl(final RecyclerView.ViewHolder holder) {
        View view = holder.itemView;
        final ViewPropertyAnimatorCompat anim = ViewCompat.animate(view);
        anim
                // translate horizontally to provide slide out to right
                .translationX(view.getWidth())
                // fade out
                .alpha(0)
                .setDuration(getRemoveDuration())
                .setInterpolator(new AccelerateInterpolator())
                .setListener(new ViewPropertyAnimatorListener() {
                    @Override
                    public void onAnimationStart(View view) {
                        dispatchRemoveStarting(holder);
                    }

                    @Override
                    public void onAnimationEnd(View view) {
                        anim.setListener(null);
                        dispatchRemoveFinished(holder);
                        // cleanup
                        view.setTranslationX(0);
                        view.setAlpha(1);
                    }

                    @Override
                    public void onAnimationCancel(View view) {
                    }
                }).start();
    }


    @Override
    public boolean animateMove(RecyclerView.ViewHolder holder, int fromX, int fromY, int toX, int toY) {
        // don't handle animateMove because there should only be add/remove animations
        dispatchMoveFinished(holder);
        return false;
    }
    @Override
    public boolean animateChange(RecyclerView.ViewHolder oldHolder, RecyclerView.ViewHolder newHolder, int fromLeft, int fromTop, int toLeft, int toTop) {
        // don't handle animateChange because there should only be add/remove animations
        if (newHolder != null) {
            dispatchChangeFinished(newHolder, false);
        }
        dispatchChangeFinished(oldHolder, true);
        return false;
    }
    @Override
    public void endAnimation(RecyclerView.ViewHolder item) { }
    @Override
    public void endAnimations() { }
    @Override
    public boolean isRunning() { return false; }
}

Este es el resultado final:

Actualización: Mientras leía la publicación otra vez, descubrí una mejor solución.

Esta solución actualizada no requiere engañar al adaptador con identificadores aleatorios para que piense que todos los elementos se eliminaron y solo se agregaron elementos nuevos. Si aplicamos la 2) Versión avanzada : cómo notificar al adaptador acerca de los cambios en el conjunto de datos, podemos decirle al adapter que se eliminaron todos los elementos anteriores y se agregaron todos los nuevos elementos:

int oldSize = oldItems.size();
oldItems.clear();
// Notify the adapter all previous items were removed
notifyItemRangeRemoved(0, oldSize);

oldItems.addAll(items);
// Notify the adapter all the new items were added
notifyItemRangeInserted(0, items.size());

// don't call notifyDataSetChanged
//notifyDataSetChanged();

El SlidingAnimator presentado anteriormente todavía es necesario para animar los cambios.





android android-fragments android-viewpager fragmentpageradapter fragmentstatepageradapter