Diferencia entre initLoader y restartLoader en LoaderManager

129

Estoy completamente perdido con respecto a las diferencias entre initLoadery las restartLoaderfunciones de LoaderManager:

  • Ambos tienen la misma firma.
  • restartLoader también crea un cargador, si no existe ("Inicia un cargador nuevo o reinicia un cargador existente en este administrador").

¿Hay alguna relación entre los dos métodos? ¿ restartLoaderLlamar siempre llama initLoader? ¿Puedo llamar restartLoadersin tener que llamar initLoader? ¿Es seguro llamar initLoaderdos veces para actualizar los datos? ¿Cuándo debo usar uno de los dos y por qué ?

theomega
fuente

Respuestas:

202

Para responder a esta pregunta, debe profundizar en el LoaderManagercódigo. Si bien la documentación de LoaderManager en sí misma no es lo suficientemente clara (o no habría esta pregunta), la documentación de LoaderManagerImpl, una subclase del LoaderManager abstracto, es mucho más esclarecedora.

initLoader

Llame para inicializar una identificación particular con un cargador. Si este ID ya tiene un cargador asociado, no se modifica y las devoluciones de llamada anteriores se reemplazan por las recién proporcionadas. Si actualmente no hay un cargador para la ID, se crea y se inicia uno nuevo.

Esta función generalmente se debe usar cuando un componente se está inicializando, para garantizar que se cree un cargador en el que se basa. Esto le permite reutilizar los datos de un cargador existente si ya hay uno, de modo que, por ejemplo, cuando se vuelve a crear una actividad después de un cambio de configuración, no es necesario volver a crear sus cargadores.

restartLoader

Llame para volver a crear el cargador asociado con una ID en particular. Si actualmente hay un cargador asociado con esta ID, se cancelará / detendrá / destruirá según corresponda. Se creará un nuevo cargador con los argumentos dados y se le entregarán sus datos una vez que estén disponibles.

[...] Después de llamar a esta función, cualquier Cargador anterior asociado con esta ID se considerará inválido y no recibirá más actualizaciones de datos de ellos.

Básicamente hay dos casos:

  1. El cargador con la identificación no existe: ambos métodos crearán un nuevo cargador, por lo que no hay diferencia allí
  2. El cargador con el ID ya existe: initLoadersolo reemplazará las devoluciones de llamada pasadas como parámetro, pero no cancelará ni detendrá el cargador. Para a CursorLoadereso significa que el cursor permanece abierto y activo (si ese fue el caso antes de la initLoaderllamada). `restartLoader, por otro lado, cancelará, detendrá y destruirá el cargador (y cerrará la fuente de datos subyacente como un cursor) y creará un nuevo cargador (que también crearía un nuevo cursor y volvería a ejecutar la consulta si el cargador es un CursorLoader).

Aquí está el código simplificado para ambos métodos:

initLoader

LoaderInfo info = mLoaders.get(id);
if (info == null) {
    // Loader doesn't already exist -> create new one
    info = createAndInstallLoader(id, args, LoaderManager.LoaderCallbacks<Object>)callback);
} else {
   // Loader exists -> only replace callbacks   
   info.mCallbacks = (LoaderManager.LoaderCallbacks<Object>)callback;
}

restartLoader

LoaderInfo info = mLoaders.get(id);
if (info != null) {
    LoaderInfo inactive = mInactiveLoaders.get(id);
    if (inactive != null) {
        // does a lot of stuff to deal with already inactive loaders
    } else {
        // Keep track of the previous instance of this loader so we can destroy
        // it when the new one completes.
        info.mLoader.abandon();
        mInactiveLoaders.put(id, info);
    }
}
info = createAndInstallLoader(id, args,  (LoaderManager.LoaderCallbacks<Object>)callback);

Como podemos ver en caso de que el cargador no exista (info == nulo) ambos métodos crearán un nuevo cargador (info = createAndInstallLoader (...)). En caso de que el cargador ya exista, initLoadersolo reemplaza las devoluciones de llamada (info.mCallbacks = ...) mientras restartLoaderinactiva el cargador anterior (se destruirá cuando el nuevo cargador complete su trabajo) y luego cree uno nuevo.

Por lo tanto, ahora está claro cuándo usar initLoadery cuándo usar restartLoadery por qué tiene sentido tener los dos métodos. initLoaderse utiliza para garantizar que haya un cargador inicializado. Si no existe ninguno, se crea uno nuevo, si ya existe, se reutiliza. Siempre usamos este método A MENOS QUE necesitemos un nuevo cargador porque la consulta a ejecutar ha cambiado (no los datos subyacentes sino la consulta real como en la instrucción SQL para un CursorLoader), en cuyo caso llamaremos restartLoader.

¡El ciclo de vida de la Actividad / Fragmento no tiene nada que ver con la decisión de usar uno u otro método (y no hay necesidad de hacer un seguimiento de las llamadas usando una bandera de un solo disparo como lo sugirió Simon)! Esta decisión se toma únicamente en función de la "necesidad" de un nuevo cargador. Si queremos ejecutar la misma consulta que utilizamos initLoader, si queremos ejecutar una consulta diferente que utilizamos restartLoader.

Siempre podríamos usar, restartLoaderpero eso sería ineficiente. Después de una rotación de pantalla o si el usuario se aleja de la aplicación y regresa más tarde a la misma Actividad, generalmente queremos mostrar el mismo resultado de la consulta y, por lo tanto, restartLoadervolvería a crear innecesariamente el cargador y descartaría el resultado de la consulta subyacente (potencialmente costosa).

Es muy importante comprender la diferencia entre los datos que se cargan y la "consulta" para cargar esos datos. Supongamos que usamos un CursorLoader que consulta una tabla para pedidos. Si se agrega un nuevo pedido a esa tabla, el CursorLoader usa onContentChanged () para informar a la IU que actualice y muestre el nuevo pedido (no es necesario usarlo restartLoaderen este caso). Si queremos mostrar solo las órdenes abiertas, necesitamos una nueva consulta y la restartLoaderusaríamos para devolver un nuevo CursorLoader que refleje la nueva consulta.


¿Hay alguna relación entre los dos métodos?

Comparten el código para crear un nuevo cargador, pero hacen cosas diferentes cuando ya existe un cargador.

¿ restartLoaderLlamar siempre llama initLoader?

No, nunca lo hace.

¿Puedo llamar restartLoadersin tener que llamar initLoader?

Si.

¿Es seguro llamar initLoaderdos veces para actualizar los datos?

Es seguro llamar initLoaderdos veces, pero no se actualizarán los datos.

¿Cuándo debo usar uno de los dos y por qué ?


Eso debería (con suerte) quedar claro después de mis explicaciones anteriores.

Cambios de configuración

Un LoaderManager conserva su estado a través de los cambios de configuración (incluidos los cambios de orientación), por lo que pensaría que no nos queda nada por hacer. Piensa otra vez...

En primer lugar, un LoaderManager no retiene las devoluciones de llamada, por lo que si no hace nada, no recibirá llamadas a sus métodos de devolución de llamada, onLoadFinished()y similares, y eso probablemente interrumpirá su aplicación.

Por lo tanto, DEBEMOS llamar al menos initLoaderpara restaurar los métodos de devolución de llamada (a restartLoader, por supuesto, también es posible). La documentación dice:

Si en el punto de la llamada, la persona que llama está en su estado iniciado, y el cargador solicitado ya existe y ha generado sus datos, onLoadFinished(Loader, D)se llamará inmediatamente a la devolución de llamada (dentro de esta función) [...].

Eso significa que si llamamos initLoaderdespués de un cambio de orientación, recibiremos una onLoadFinishedllamada de inmediato porque los datos ya están cargados (suponiendo que ese fuera el caso antes del cambio). Si bien eso parece sencillo, puede ser complicado (no todos amamos Android ...).

Tenemos que distinguir entre dos casos:

  1. Maneja cambios en la configuración: este es el caso de los Fragmentos que usan setRetainInstance (true) o para una Actividad con la android:configChangesetiqueta correspondiente en el manifiesto. Estos componentes no recibirán una llamada onCreate después de, por ejemplo, una rotación de pantalla, así que tenga en cuenta llamar initLoader/restartLoadera otro método de devolución de llamada (por ejemplo, en onActivityCreated(Bundle)). Para poder inicializar el (los) Cargador (es), los identificadores del cargador deben almacenarse (por ejemplo, en una Lista). Debido a que el componente se retiene a través de los cambios de configuración, simplemente podemos recorrer los identificadores del cargador existentes y llamar initLoader(loaderid, ...).
  2. No maneja los cambios de configuración por sí mismo: en este caso, los cargadores se pueden inicializar en onCreate pero necesitamos retener manualmente los identificadores del cargador o no podremos realizar las llamadas necesarias initLoader / restartLoader. Si los identificadores se almacenan en una ArrayList, haríamos un
    outState.putIntegerArrayList(loaderIdsKey, loaderIdsArray)en onSaveInstanceState y restauraríamos los identificadores en onCreate: loaderIdsArray = savedInstanceState.getIntegerArrayList(loaderIdsKey)antes de hacer la (s) llamada (s) initLoader.
Emanuel Moecklin
fuente
: +1: Un último punto. Si usa initLoader(y todas las devoluciones de llamada han finalizado, Loader está inactivo) después de una rotación, no recibirá una onLoadFinisheddevolución de llamada, pero si lo usa, restartLoader¿lo hará?
Blundell
Incorrecto. El método initLoader llama al método onLoadFinished () antes de que regrese (si el cargador se inicia y tiene datos). Agregué un párrafo sobre los cambios de configuración para explicar esto con más detalle.
Emanuel Moecklin
66
Ah, por supuesto, una combinación de su respuesta y @ alexlockwood da la imagen completa. Supongo que la respuesta para otros es, use initLoader si su consulta es estática y restartLoader si desea cambiar la consulta
Blundell
1
Eso lo convoca muy bien: "use initLoader si su consulta es estática y reinicieLoader si desea cambiar la consulta"
Emanuel Moecklin
1
@Mhd. Tahawi, no estás cambiando las devoluciones de llamada, solo las configuras donde sea que vayan. Después de una rotación de pantalla, deben volver a configurarse porque Android no los mantendrá para evitar pérdidas de memoria. Eres libre de configurarlos para lo que quieras siempre que hagan lo correcto.
Emanuel Moecklin
46

Llamar initLoadercuando el Loader ya se ha creado (por lo general, esto ocurre después de los cambios de configuración, por ejemplo) le dice al LoaderManager que entregue los datos más recientes del Loader de onLoadFinishedinmediato. Si el cargador no se ha creado (por ejemplo, cuando se inicia la actividad / fragmento por primera vez), la llamada a initLoaderle indica al LoaderManager que llame onCreateLoaderpara crear el nuevo cargador.

Las llamadas restartLoaderdestruyen un cargador ya existente (así como cualquier dato existente asociado con él) y le dice al LoaderManager que llame onCreateLoaderpara crear el nuevo cargador e iniciar una nueva carga.


La documentación también es bastante clara sobre esto:

  • initLoaderasegura que un cargador esté inicializado y activo. Si el cargador aún no existe, se crea uno y (si la actividad / fragmento se ha iniciado actualmente) inicia el cargador. De lo contrario, se reutiliza el último cargador creado.

  • restartLoaderinicia un nuevo o reinicia un cargador existente en este administrador, registra las devoluciones de llamada y (si la actividad / fragmento está actualmente iniciado) comienza a cargarlo. Si se ha iniciado previamente un cargador con la misma identificación, se destruirá automáticamente cuando el nuevo cargador complete su trabajo. La devolución de llamada se entregará antes de que se destruya el cargador antiguo.

Alex Lockwood
fuente
@TomanMoney Le expliqué lo que significa en mi respuesta. ¿De qué parte estás confundido?
Alex Lockwood
acabas de rehacer el documento. Pero el documento no indica dónde se debe usar cada método y por qué es malo estropearlo. En mi experiencia, solo llamar a restartLoader y nunca llamar a initLoader funciona bien. Entonces esto sigue siendo confuso.
Tom anMoney
3
@TomanMoney Usualmente usas initLoader()en onCreate()/ onActivityCreated()cuando la actividad / fragmento se inicia por primera vez. De esta manera, cuando el usuario abre una actividad por primera vez, se creará el cargador por primera vez ... pero en cualquier cambio de configuración posterior en el que deba destruirse toda la actividad / fragmento, la siguiente llamada initLoader()simplemente devolverá la antigua en Loaderlugar de creando uno nuevo. Usualmente lo usa restartLoader()cuando necesita cambiar la Loaderconsulta de (es decir, desea obtener datos filtrados / ordenados, etc.).
Alex Lockwood
44
Todavía estoy confundido acerca de la decisión de API de tener ambos métodos, ya que tienen la misma firma. ¿Por qué la API no puede ser un único método startLoader () que hace lo "correcto" cada vez? Creo que esta es la parte que confunde a mucha gente.
Tom anMoney
1
@TomanMoney La documentación aquí dice: developer.android.com/guide/components/loaders.html . "Se reconectan automáticamente al cursor del último cargador cuando se vuelven a crear después de un cambio de configuración. Por lo tanto, no necesitan volver a consultar sus datos".
IgorGanapolsky
16

Recientemente me topé con un problema con varios administradores de cargadores y cambios en la orientación de la pantalla y me gustaría decir que después de muchas pruebas y errores, el siguiente patrón me funciona tanto en Actividades como en Fragmentos:

onCreate: call initLoader(s)
          set a one-shot flag
onResume: call restartLoader (or later, as applicable) if the one-shot is not set.
          unset the one-shot in either case.

(en otras palabras, establecer algún indicador para que initLoader está siempre ejecuta una vez y que restartLoader se ejecuta en el segundo y subsiguientes pases a través onResume )

Además, recuerde asignar diferentes identificadores para cada uno de sus cargadores dentro de una Actividad (que puede ser un problema con los fragmentos dentro de esa actividad si no tiene cuidado con su numeración)


Intenté usar initLoader solo ... no parecía funcionar de manera efectiva.

Intentado initLoader en onCreate con argumentos nulos (docs dicen que esto está bien) y restartLoader (con argumentos válidos) en onResume .... documentos están equivocados y initLoader una excepción NullPointer.

Intentado restartLoader única ... funciona por un tiempo, pero el 5 de golpes o de la pantalla 6 de reorientación.

Probamos initLoader en onResume ; nuevamente funciona por un tiempo y luego sopla. (específicamente el "Llamado doRetain cuando no se inició:" ... error)

Intenté lo siguiente: (extracto de una clase de cubierta que tiene el id del cargador pasado al constructor)

/**
 * start or restart the loader (why bother with 2 separate functions ?) (now I know why)
 * 
 * @param manager
 * @param args
 * @deprecated use {@link #restart(LoaderManager, Bundle)} in onResume (as appropriate) and {@link #initialise(LoaderManager, Bundle)} in onCreate 
 */
@Deprecated 
public void start(LoaderManager manager, Bundle args) {
    if (manager.getLoader(this.id) == null) {
        manager.initLoader(this.id, args, this);
    } else {
        manager.restartLoader(this.id, args, this);
    }
}

(que encontré en algún lugar de Stack-Overflow)

Una vez más, esto funcionó por un tiempo, pero aún arrojó la falla ocasional.


Por lo que puedo descubrir durante la depuración, creo que hay algo que ver con el estado de instancia de guardar / restaurar que requiere que initLoader (/ s) se ejecuten en la parte onCreate del ciclo de vida si van a sobrevivir a un giro del ciclo . ( Puedo estar equivocado.)

En el caso de los administradores que no pueden iniciarse hasta que los resultados vuelvan de otro administrador o tarea (es decir, no pueden inicializarse en onCreate ), solo uso initLoader . (Puede que no sea correcto en esto, pero parece funcionar. Estos cargadores secundarios no son parte del estado de instancia inmediato, por lo que usar initLoader puede ser correcto en este caso)

ciclo vital


Mirando los diagramas y documentos, habría pensado que initLoader debería ir en onCreate & restartLoader en onRestart for Activities pero eso deja a Fragments usando un patrón diferente y no he tenido tiempo de investigar si esto es realmente estable. ¿Alguien más puede comentar si tienen éxito con este patrón de actividades?

Simón
fuente
/ @ Simon es 100% correcto y esta debería ser la respuesta aceptada. No creía su respuesta y pasé varias horas tratando de encontrar diferentes formas de hacer que esto funcione. Tan pronto como moví la llamada initLoader a onCreate, las cosas comenzaron a funcionar. Luego, necesita el indicador de una sola vez para tener en cuenta las veces que se llama a OnStart pero no a onCreate
CjS
2
"Intenté reiniciar el cargador solamente ... funciona por un tiempo, pero sopla en la reorientación de la 5ta o 6ta pantalla". ¿Lo hace? Simplemente lo intenté y giré la pantalla cien veces y no exploté. ¿Qué tipo de excepción estás recibiendo?
Tom anMoney
-1 Aprecio el esfuerzo de investigación detrás de esta respuesta, pero la mayoría de los resultados son incorrectos.
Emanuel Moecklin
1
@IgorGanapolsky casi todo. Si lees y entiendes mi respuesta, entenderás qué hacen initLoader y restartLoader y cuándo usar cuál y también entenderás por qué casi todas las conclusiones de Simon están equivocadas. No hay conexión entre el ciclo de vida de un fragmento / actividad y la decisión de cuándo usar initLoader / restartLoader (con una advertencia que explico en los cambios de configuración). Simon concluye de ensayo y error que el ciclo de vida es la clave para comprender los dos métodos, pero no lo es.
Emanuel Moecklin
@IgorGanapolsky No estoy tratando de anunciar mi propia respuesta. Simplemente estoy tratando de ayudar a otros desarrolladores y evitar que usen los resultados de Simon para sus propias aplicaciones. Una vez que comprenda para qué sirven los dos métodos, todo se vuelve bastante obvio y fácil de implementar.
Emanuel Moecklin
0

initLoaderreutilizará los mismos parámetros si el cargador ya existe. Regresa inmediatamente si los datos antiguos ya están cargados, incluso si lo llama con nuevos parámetros. Lo ideal es que el cargador notifique automáticamente la actividad de nuevos datos. Si la pantalla girara, se initLoadervolvería a llamar y los datos antiguos se mostrarían de inmediato.

restartLoaderes para cuando quieres forzar una recarga y cambiar los parámetros también. Si tuviera que hacer una pantalla de inicio de sesión con cargadores, llamaría solo restartLoadercada vez que se haga clic en el botón. (Se puede hacer clic en el botón varias veces debido a credenciales incorrectas, etc.). Solo llamaría initLoaderal restaurar el estado de instancia guardada de la actividad en caso de que la pantalla se girara mientras se iniciaba una sesión.

Monstieur
fuente
-1

Si el cargador ya existe, restartLoader detendrá / cancelará / destruirá el antiguo, mientras que initLoader solo lo inicializará con la devolución de llamada dada. No puedo averiguar qué hacen las antiguas devoluciones de llamada en estos casos, pero supongo que simplemente serán abandonadas.

Escaneé a través de http://grepcode.com/file/repository.grepcode.com/java/ext/com.google.android/android/4.0.1_r1/android/app/LoaderManager.java pero no puedo averiguar cuál es exactamente La diferencia es que, aparte de eso, los métodos hacen cosas diferentes. Entonces diría que use initLoader la primera vez y reinicie las siguientes veces, aunque no puedo decir con certeza qué hará exactamente cada uno de ellos.

koljaTM
fuente
¿Y qué hará initLoaderen este caso?
theomega
-1

El cargador de inicio en el primer inicio usa el método loadInBackground (), en el segundo inicio se omitirá. Entonces, mi opinión, la mejor solución es:

Loader<?> loa; 
try {
    loa = getLoaderManager().getLoader(0);
} catch (Exception e) {
    loa = null;
}
if (loa == null) {
    getLoaderManager().initLoader(0, null, this);
} else {
    loa.forceLoad();
}

//////////////////////////////////////////////////// //////////////////////////

protected SimpleCursorAdapter mAdapter;

private abstract class SimpleCursorAdapterLoader 
    extends AsyncTaskLoader <Cursor> {

    public SimpleCursorAdapterLoader(Context context) {
        super(context);
    }

    @Override
    protected void onStartLoading() {
        if (takeContentChanged() || mAdapter.isEmpty()) {
            forceLoad();
        }
    }

    @Override
    protected void onStopLoading() {
        cancelLoad();
    }

    @Override
    protected void onReset() {
        super.onReset();
        onStopLoading();
    }
}

Pasé mucho tiempo para encontrar esta solución: restartLoader (...) no funcionó correctamente en mi caso. El único forceLoad () permite finalizar el subproceso de carga anterior sin devolución de llamada (por lo que tendrá todas las transacciones de db finalizadas correctamente) y comienza de nuevo nuevo subproceso. Sí, exige algo de tiempo extra, pero es más estable. Solo el último subproceso iniciado tendrá devolución de llamada. Por lo tanto, si desea realizar pruebas con la interrupción de sus transacciones de db, de nada, intente reiniciarLoader (...), de lo contrario forceLoad (). La única conveniencia de restartLoader (...) es entregar nuevos datos iniciales, es decir, parámetros. Y no olvide destruir el cargador en el método onDetach () del Fragmento adecuado en este caso. También tenga en cuenta que algunas veces, cuando tiene una actividad y, déjenles decir, 2 fragmentos con el cargador cada actividad inclusiva: llegarás a solo 2 gestores del cargador, por lo que Activity comparte su LoaderManager con los fragmentos, que se muestran primero en la pantalla durante la carga. Pruebe LoaderManager.enableDebugLogging (true); para ver los detalles en cada caso determinado.

usuario1700099
fuente
2
-1 para ajustar la llamada a getLoader(0)a try { ... } catch (Exception e) { ... }.
Alex Lockwood