web-dev-qa-db-fra.com

Différence entre `initLoader` et` restartLoader` dans `LoaderManager`

Je suis complètement perdu en ce qui concerne les différences entre les fonctions initLoader et restartLoader du LoaderManager:

  • Ils ont tous les deux la même signature.
  • restartLoader crée également un chargeur, s'il n'existe pas ("démarre un nouveau ou redémarre un chargeur existant dans ce gestionnaire").

Y a-t-il une relation entre les deux méthodes? Appeler restartLoader appelle-t-il toujours initLoader? Puis-je appeler restartLoader sans avoir à appeler initLoader? Est-il possible d’appeler deux fois initLoader pour actualiser les données? Quand devrais-je utiliser l'un des deux et (important!) Pourquoi?

127
theomega

Pour répondre à cette question, vous devez creuser dans le code LoaderManager. Bien que la documentation de LoaderManager elle-même ne soit pas assez claire (sinon cette question ne se poserait pas), la documentation de LoaderManagerImpl, une sous-classe de l'abstraction LoaderManager, est beaucoup plus éclairante.

initLoader

Appelez pour initialiser un identifiant particulier avec un chargeur. Si un chargeur est déjà associé à cet identifiant, celui-ci reste inchangé et tous les rappels précédents sont remplacés par les nouveaux rappelés. S'il n'y a pas actuellement de chargeur pour l'ID, un nouveau est créé et démarré.

Cette fonction doit généralement être utilisée lors de l'initialisation d'un composant afin de garantir la création du chargeur sur lequel il s'appuie. Cela lui permet de réutiliser les données d'un chargeur existant s'il en existe déjà. Ainsi, par exemple, lorsqu'une activité est recréée après un changement de configuration, il n'est pas nécessaire de recréer ses chargeurs.

restartLoader

Appelez pour recréer le chargeur associé à un identifiant particulier. Si un chargeur est actuellement associé à cet identifiant, il sera annulé/arrêté/détruit selon le cas. Un nouveau chargeur avec les arguments donnés sera créé et ses données vous seront fournies une fois disponibles.

[...] Après avoir appelé cette fonction, tous les chargeurs précédents associés à cet ID seront considérés comme non valides et vous ne recevrez aucune autre mise à jour de leurs données.

Il existe essentiellement deux cas:

  1. Le chargeur avec l'id n'existe pas: les deux méthodes vont créer un nouveau chargeur, donc il n'y a pas de différence
  2. Le chargeur avec l'id existe déjà: initLoader ne remplacera que les rappels passés en paramètre, mais n'annulera ni n'arrêtera le chargeur. Pour un CursorLoader, cela signifie que le curseur reste ouvert et actif (si c'était le cas avant l'appel initLoader). D'autre part, restartLoader annule, arrête et détruit le chargeur (et ferme la source de données sous-jacente comme un curseur) et crée un nouveau chargeur (qui créerait également un nouveau curseur et relancerait la requête si le chargeur est un CursorLoader). .

Voici le code simplifié pour les deux méthodes:

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

Comme nous pouvons le voir au cas où le chargeur n'existe pas (info == null), les deux méthodes créeront un nouveau chargeur (info = createAndInstallLoader (...)). Si le chargeur existe déjà, initLoader ne fait que remplacer les rappels (info.mCallbacks = ...) pendant que restartLoader désactive l'ancien chargeur (il sera détruit lorsque le nouveau chargeur aura terminé son travail), puis en crée un nouveau.

Cela dit, il est maintenant évident de savoir quand utiliser initLoader et quand restartLoader et pourquoi il est logique de recourir aux deux méthodes. initLoader est utilisé pour s'assurer qu'il existe un chargeur initialisé. S'il n'en existe pas, un nouveau est créé, s'il en existe déjà, il est réutilisé. Nous utilisons toujours cette méthode SAUF SI nous avons besoin d’un nouveau chargeur car la requête à exécuter a changé (pas les données sous-jacentes mais la requête proprement dite, comme dans une instruction SQL pour un CursorLoader), auquel cas nous appellerons restartLoader.

Le cycle de vie Activité/Fragment n'a rien à voir avec la décision d'utiliser l'une ou l'autre méthode (et il n'est pas nécessaire de garder une trace des appels en utilisant un indicateur à un coup comme Simon l'a suggéré)! Cette décision est prise uniquement sur la "nécessité" d'un nouveau chargeur. Si nous voulons exécuter la même requête, nous utilisons initLoader, si nous voulons exécuter une requête différente, nous utilisons restartLoader. Nous pourrions toujours utiliser restartLoader, mais ce serait inefficace. Après une rotation de l'écran ou si l'utilisateur quitte l'application et revient plus tard à la même activité, nous souhaitons généralement afficher le même résultat de la requête. Ainsi, le restartLoader recréerait inutilement le chargeur et rejeterait le résultat de la requête sous-jacent (potentiellement coûteux). .

Il est très important de comprendre la différence entre les données chargées et la "requête" permettant de les charger. Supposons que nous utilisons un CursorLoader interrogeant une table pour des commandes. Si une nouvelle commande est ajoutée à cette table, CursorLoader utilise onContentChanged () pour informer l'interface utilisateur à mettre à jour et afficher la nouvelle commande (il n'est pas nécessaire d'utiliser restartLoader dans ce cas). Si nous voulons n'afficher que les commandes en cours, nous avons besoin d'une nouvelle requête et nous utiliserions restartLoader pour renvoyer un nouveau CursorLoader reflétant la nouvelle requête.

Y a-t-il une relation entre les deux méthodes?

Ils partagent le code pour créer un nouveau chargeur mais ils font des choses différentes lorsqu'un chargeur existe déjà.

L'appel de restartLoader appelle-t-il toujours initLoader?

Non ça ne fait jamais.

Puis-je appeler restartLoader sans avoir à appeler initLoader?

Oui.

Est-il prudent d'appeler initLoader deux fois pour actualiser les données?

Il est prudent d'appeler initLoader deux fois, mais aucune donnée ne sera actualisée.

Quand devrais-je utiliser l'un des deux et (important!) Pourquoi?

Cela devrait (espérons-le) être clair après mes explications ci-dessus.

Modifications de la configuration

LoaderManager conserve son état lors des modifications de configuration (y compris les modifications d'orientation), de sorte que vous penserez qu'il ne nous reste plus rien à faire. Repensez-vous ...

Tout d'abord, LoaderManager ne conserve pas les rappels. Par conséquent, si vous ne faites rien, vous ne recevrez pas d'appels à vos méthodes de rappel comme onLoadFinished (), etc., ce qui endommagera très probablement votre application. Par conséquent, nous devons appeler au moins initLoader pour restaurer les méthodes de rappel (un restartLoader est bien sûr également possible). Le documentation indique:

Si, au moment de l'appel, l'appelant est dans son état démarré et que le chargeur demandé existe déjà et a généré ses données, alors le rappel onLoadFinished (Loader, D) sera appelé immédiatement (à l'intérieur de cette fonction) [...].

Cela signifie que si nous appelons initLoader après un changement d'orientation, nous obtiendrons immédiatement un appel onLoadFinished car les données sont déjà chargées (en supposant que c'était le cas avant le changement). Bien que cela semble simple, cela peut être délicat (n'aimons-nous pas tous Android ...).

Il faut distinguer deux cas:

  1. La gestion des modifications de configuration elle-même: c'est le cas des fragments qui utilisent setRetainInstance (true) ou d'une activité avec la balise Android: configChanges correspondante dans le manifeste. Ces composants ne recevront pas d’appel onCreate après, par exemple. rotation de l'écran, n'oubliez pas d'appeler initLoader/restartLoader dans une autre méthode de rappel (par exemple, onActivityCreated (Bundle)). Pour pouvoir initialiser le (s) chargeur (s), les identifiants de chargeur doivent être stockés (par exemple dans une liste). Comme le composant est conservé lors des modifications de configuration, nous pouvons simplement relier les identifiants de chargeur existants et appeler initLoader (loaderid, ...).
  2. Ne gère pas lui-même les modifications de configuration: dans ce cas, les chargeurs peuvent être initialisés dans onCreate, mais nous devons conserver manuellement les identifiants de chargeur, sinon nous ne pourrons pas effectuer les appels initLoader/restartLoader nécessaires. Si les identifiants sont stockés dans une ArrayList, nous ferions un
    outState.putIntegerArrayList (loaderIdsKey, loaderIdsArray) dans onSaveInstanceState et restaurez les identifiants dans onCreate: loaderIdsArray = savedInstanceState.getIntegerArrayList (loaderIdsKey) avant d'appeler initLoader.
200
Emanuel Moecklin

L'appel de initLoader lorsque le chargeur a déjà été créé (cela se produit généralement après des modifications de configuration, par exemple) indique à LoaderManager de transmettre immédiatement les données les plus récentes du chargeur à onLoadFinished. Si le chargeur n'a pas encore été créé (par exemple, lorsque l'activité/le fragment est lancé pour la première fois), l'appel à initLoader indique au LoaderManager d'appeler onCreateLoader pour créer le nouveau chargeur.

L'appel de restartLoader détruit un chargeur existant (ainsi que toutes les données existantes qui lui sont associées) et demande au LoaderManager d'appeler onCreateLoader pour créer le nouveau chargeur et lancer un nouveau chargement.


La documentation est assez claire à ce sujet aussi:

  • initLoader garantit qu'un chargeur est initialisé et actif. Si le chargeur n'existe pas déjà, il en est créé un (si l'activité/le fragment est actuellement démarré), le démarre. Sinon, le dernier chargeur créé est réutilisé.

  • restartLoader démarre un nouveau chargeur ou redémarre un chargeur existant dans ce gestionnaire, enregistre les rappels et (si l'activité/le fragment est actuellement démarré) commence à le charger. Si un chargeur avec le même identifiant a déjà été démarré, il sera automatiquement détruit à la fin du travail du nouveau chargeur. Le rappel sera livré avant que l'ancien chargeur ne soit détruit.

46
Alex Lockwood

J'ai récemment rencontré un problème avec plusieurs gestionnaires de chargeur et changements d'orientation de l'écran et je voudrais dire qu'après de nombreux essais et erreurs, le modèle suivant fonctionne pour moi dans les activités et les fragments:

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 d'autres termes, définissez un indicateur pour que initLoader soit toujours exécuté une fois et que restartLoader soit exécuté sur le 2nd et suivants passe par onResume )

De plus, n'oubliez pas d'attribuer des identifiants différents à chacun de vos chargeurs au sein d'une activité (ce qui peut poser un problème avec les fragments de cette activité si vous ne faites pas attention à la numérotation).


J'ai essayé d'utiliser initLoader seulement .... ne semblait pas fonctionner efficacement.

Essayé initLoader le onCreate avec des arguments nuls (les documents indiquent que c'est correct) & restartLoader (avec arguments valables) dans onResume .... docs ont tort & initLoader lève une exception nullpointer.

Essayé restartLoader seulement ... fonctionne pendant un certain temps, mais souffle lors de la réorientation du 5ème ou du 6ème écran.

Essayé initLoader dans onResume ; travaille à nouveau pendant un moment et puis souffle. (en particulier le "Appelé doRetain lorsqu'il n'est pas démarré:" ... erreur)

Essayé ce qui suit: (extrait d'une classe de couverture où l'identifiant du chargeur est passé au constructeur)

/**
 * 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 j'ai trouvé quelque part dans Stack-Overflow)

Encore une fois, cela a fonctionné pendant un certain temps, mais toujours jeté le pépin occasionnel.


D'après ce que je peux comprendre pendant le débogage, je pense qu'il y a quelque chose à voir avec l'état d'instance de sauvegarde/restauration qui nécessite que initLoader (/ s) exécuter dans la partie onCreate du cycle de vie si elles doivent survivre à une rotation du cycle. ( J'ai peut-être tort.)

dans le cas de gestionnaires qui ne peuvent pas être démarrés tant que les résultats ne sont pas renvoyés par un autre gestionnaire ou une autre tâche (c.-à-d. ne peuvent pas être initialisés dans onCreate ), je n'utilise que initLoader . (Je ne suis peut-être pas correct là-dessus mais cela semble fonctionner. Ces chargeurs secondaires ne faisant pas partie de l'état d'instance immédiat, vous pouvez donc utiliser initLoader correct dans ce cas)

lifecycle


En regardant les diagrammes et la documentation, j'aurais pensé qu'initLoader devrait aller dans onCreate & restartLoader dans onRestart for Activities, mais cela laisse Fragments en utilisant un motif différent et je n'ai pas eu le temps d'examiner si cela est réellement stable. Quelqu'un d'autre peut-il commenter s'il a du succès avec ce modèle d'activités?

16
Simon

initLoader réutilisera les mêmes paramètres si le chargeur existe déjà. Il retourne immédiatement si les anciennes données sont déjà chargées, même si vous les appelez avec de nouveaux paramètres. Le chargeur devrait idéalement notifier automatiquement l’activité de nouvelles données. Si l'écran pivotait, initLoader serait rappelé et les anciennes données seraient immédiatement affichées.

restartLoader est pour quand vous voulez forcer un rechargement et changer les paramètres aussi. Si vous deviez créer un écran de connexion à l'aide de chargeurs, vous n'appelleriez que restartLoader à chaque fois que le bouton a été cliqué. (Le bouton peut être cliqué plusieurs fois en raison d'informations d'identification incorrectes, etc.). Vous ne devez jamais appeler initLoader que lors de la restauration de l'état d'instance sauvegardée de l'activité dans le cas où l'écran pivoterait pendant la connexion.

0
Monstieur