Différence entre initLoader et restartLoader dans LoaderManager

129

Je suis complètement perdu en ce qui concerne les différences entre les initLoaderet les restartLoaderfonctions 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? L'appel appelle-t-il restartLoadertoujours initLoader? Puis-je appeler restartLoadersans avoir à appeler initLoader? Est-il sécuritaire d'appeler initLoaderdeux fois pour actualiser les données? Quand devrais-je utiliser l'un des deux et pourquoi ?

theomega
la source

Réponses:

202

Pour répondre à cette question, vous devez fouiller dans le LoaderManagercode. Bien que la documentation de LoaderManager elle-même ne soit pas assez claire (ou il n'y aurait pas cette question), la documentation de LoaderManagerImpl, une sous-classe du résumé LoaderManager, est beaucoup plus éclairante.

initLoader

Appelez pour initialiser un ID particulier avec un chargeur. Si cet ID est déjà associé à un chargeur, il reste inchangé et tous les rappels précédents sont remplacés par les nouveaux fournis. 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, pour garantir la création d'un 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à un, de sorte que, par exemple, lorsqu'une activité est recréée après un changement de configuration, il n'a pas besoin de recréer ses chargeurs.

redémarrer le chargeur

Appelez pour recréer le chargeur associé à un ID particulier. S'il y a actuellement un chargeur associé à cet ID, 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 livrées une fois disponibles.

[...] Après avoir appelé cette fonction, tous les chargeurs précédents associés à cet identifiant seront considérés comme invalides et vous ne recevrez plus de mises à jour de données de leur part.

Il existe essentiellement deux cas:

  1. Le chargeur avec l'identifiant n'existe pas: les deux méthodes créeront un nouveau chargeur donc il n'y a pas de différence
  2. Le chargeur avec l'identifiant existe déjà: initLoaderne remplacera que les callbacks passés en paramètre mais n'annulera ni n'arrêtera le chargeur. Pour a CursorLoadercela signifie que le curseur reste ouvert et actif (si c'était le cas avant l' initLoaderappel). `restartLoader, d'autre part, annulera, arrêtera et détruira le chargeur (et fermera la source de données sous-jacente comme un curseur) et créera un nouveau chargeur (qui créerait également un nouveau curseur et réexécutera 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;
}

redémarrer le chargeur

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 (...)). Dans le cas où le chargeur existe déjà, il initLoaderne remplace que les rappels (info.mCallbacks = ...) tout en restartLoaderdésactivant l'ancien chargeur (il sera détruit lorsque le nouveau chargeur aura terminé son travail) puis en crée un nouveau.

Ainsi dit, il est maintenant clair quand utiliser initLoaderet quand utiliser restartLoaderet pourquoi il est logique d'avoir les deux méthodes. initLoaderest utilisé pour s'assurer qu'il y a un chargeur initialisé. S'il n'en existe pas, un nouveau est créé, s'il en existe déjà un, il est réutilisé. Nous utilisons toujours cette méthode À MOINS QUE nous ayons besoin d'un nouveau chargeur car la requête à exécuter a changé (pas les données sous-jacentes mais la requête réelle comme dans l'instruction SQL pour un CursorLoader), auquel cas nous allons appeler 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 suivre les appels en utilisant un indicateur à un coup comme Simon l'a suggéré)! Cette décision est prise uniquement en fonction du "besoin" d'un nouveau chargeur. Si nous voulons exécuter la même requête que nous utilisons initLoader, si nous voulons exécuter une requête différente que nous utilisons restartLoader.

Nous pourrions toujours utiliser restartLoadermais ce serait inefficace. Après une rotation d'écran ou si l'utilisateur quitte l'application et retourne plus tard à la même activité, nous voulons généralement afficher le même résultat de requête et donc restartLoaderrecréer inutilement le chargeur et rejeter 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» pour charger ces données. Supposons que nous utilisons un CursorLoader interrogeant une table pour les commandes. Si une nouvelle commande est ajoutée à cette table, le CursorLoader utilise onContentChanged () pour informer l'interface utilisateur de mettre à jour et d'afficher la nouvelle commande (inutile d'utiliser restartLoaderdans ce cas). Si nous voulons afficher uniquement les commandes ouvertes, nous avons besoin d'une nouvelle requête et nous l'utiliserons restartLoaderpour 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 appelle-t-il restartLoadertoujours initLoader?

Non, ça ne le fait jamais.

Puis-je appeler restartLoadersans avoir à appeler initLoader?

Oui.

Est-il sécuritaire d'appeler initLoaderdeux fois pour actualiser les données?

Vous pouvez appeler initLoaderdeux fois en toute sécurité, mais aucune donnée ne sera actualisée.

Quand devrais-je utiliser l'un des deux et pourquoi ?


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

Changements de configuration

Un LoaderManager conserve son état lors des changements de configuration (y compris les changements d'orientation), vous pensez donc qu'il ne nous reste plus rien à faire. Détrompez-vous ...

Tout d'abord, un LoaderManager ne conserve pas les rappels, donc si vous ne faites rien, vous ne recevrez pas d'appels vers vos méthodes de rappel comme onLoadFinished()et autres et cela cassera très probablement votre application.

Par conséquent, nous DEVONS appeler au moins initLoaderpour restaurer les méthodes de rappel (cela restartLoaderest bien sûr également possible). La documentation déclare:

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 callback onLoadFinished(Loader, D)sera appelé immédiatement (à l'intérieur de cette fonction) [...].

Cela signifie que si nous appelons initLoaderaprès un changement d'orientation, nous recevrons un onLoadFinishedappel tout de suite 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 (nous n'aimons pas tous Android ...).

Il faut distinguer deux cas:

  1. Gère les changements de configuration eux-mêmes: c'est le cas pour les fragments qui utilisent setRetainInstance (true) ou pour une activité avec la android:configChangesbalise correspondante dans le manifeste. Ces composants ne recevront pas d'appel onCreate après, par exemple, une rotation d'écran, alors gardez à l'esprit d'appeler initLoader/restartLoaderune autre méthode de rappel (par exemple, in onActivityCreated(Bundle)). Pour pouvoir initialiser le (s) chargeur (s), les identifiants de chargeur doivent être stockés (par exemple dans une liste). Étant donné que le composant est conservé lors des modifications de configuration, nous pouvons simplement effectuer une boucle sur les ID de chargeur existants et appeler initLoader(loaderid, ...).
  2. Ne gère pas les changements de configuration lui-même: dans ce cas, les chargeurs peuvent être initialisés dans onCreate mais nous devons conserver manuellement les identifiants de chargeur ou nous ne pourrons pas effectuer les appels initLoader / restartLoader nécessaires. Si les identifiants sont stockés dans une ArrayList, nous ferons un
    outState.putIntegerArrayList(loaderIdsKey, loaderIdsArray)in onSaveInstanceState et restaurons les ids dans onCreate: loaderIdsArray = savedInstanceState.getIntegerArrayList(loaderIdsKey)avant de faire le (s) appel (s) initLoader.
Emanuel Moecklin
la source
: +1: Un dernier point. Si vous utilisez initLoader(et que tous les rappels sont terminés, Loader est inactif) après une rotation, vous n'obtiendrez pas de onLoadFinishedrappel, mais si vous l'utilisez, restartLoadervous le ferez?
Blundell
Incorrect. La méthode initLoader appelle la méthode onLoadFinished () avant son retour (si le chargeur est démarré et contient des données). J'ai ajouté un paragraphe sur les changements de configuration pour expliquer cela plus en détail.
Emanuel Moecklin
6
ah bien sûr, une combinaison de votre réponse et de @ alexlockwood donne une image complète. Je suppose que la réponse pour les autres est, utilisez initLoader si votre requête est statique et redémarrezLoader si vous souhaitez modifier la requête
Blundell
1
Cela le convoque bien: "utilisez initLoader si votre requête est statique et redémarrezLoader si vous voulez changer la requête"
Emanuel Moecklin
1
@Mhd. Tahawi, vous ne changez pas les rappels, vous les définissez uniquement là où ils devraient aller. Après une rotation de l'écran, ils doivent être réinitialisés car Android ne les gardera pas pour éviter les fuites de mémoire. Vous êtes libre de les configurer comme vous le souhaitez, à condition qu'ils agissent correctement.
Emanuel Moecklin
46

L'appel initLoaderlorsque le chargeur a déjà été créé (cela se produit généralement après des modifications de configuration, par exemple) indique au LoaderManager de fournir onLoadFinishedimmédiatement les données les plus récentes du chargeur . Si le chargeur n'a pas déjà été créé (lors du premier lancement de l'activité / du fragment, par exemple), l'appel à initLoaderindique au LoaderManager d'appeler onCreateLoaderpour créer le nouveau chargeur.

L'appel restartLoaderdétruit un Loader déjà existant (ainsi que toutes les données existantes qui lui sont associées) et indique au LoaderManager d'appeler onCreateLoaderpour créer le nouveau Loader et lancer un nouveau chargement.


La documentation est assez claire à ce sujet aussi:

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

  • restartLoaderdémarre un nouveau ou redémarre un chargeur existant dans ce gestionnaire, y 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 lorsque le nouveau chargeur aura terminé son travail. Le rappel sera délivré avant la destruction de l'ancien chargeur.

Alex Lockwood
la source
@TomanMoney J'ai expliqué ce que cela signifie dans ma réponse. De quelle partie êtes-vous confus?
Alex Lockwood
vous venez de refaire le doc. Mais la documentation ne donne aucune indication sur l'endroit où chaque méthode devrait être utilisée et pourquoi il est mauvais de la gâcher. D'après mon expérience, appeler simplement restartLoader et ne jamais appeler initLoader fonctionne bien. Donc, c'est encore déroutant.
Tom anMoney
3
@TomanMoney Habituellement, vous utilisez initLoader()in onCreate()/ onActivityCreated()lorsque l'activité / le fragment démarre pour la première fois. De cette façon, lorsque l'utilisateur ouvre une activité pour la première fois, le chargeur sera créé pour la première fois ... mais sur tout changement de configuration ultérieur où toute l'activité / le fragment doit être détruit, l'appel suivant à initLoader()retournera simplement l'ancien Loaderau lieu de créer un nouveau. Habituellement, vous utilisez restartLoader()lorsque vous devez modifier la Loaderrequête de (c'est-à-dire que vous souhaitez obtenir des données filtrées / triées, etc.).
Alex Lockwood
4
Je suis toujours confus quant à la décision de l'API d'avoir les deux méthodes, car elles ont la même signature. Pourquoi l'API ne pourrait-elle pas être une méthode startLoader () unique qui fait la «bonne chose» à chaque fois? Je pense que c'est la partie qui déroute beaucoup de gens.
Tom anMoney
1
@TomanMoney La documentation ici dit: developer.android.com/guide/components/loaders.html . "Ils se reconnectent automatiquement au dernier curseur du chargeur lorsqu'ils sont recréés après un changement de configuration. Ainsi, ils n'ont pas besoin de réinterroger leurs données."
IgorGanapolsky
16

J'ai récemment rencontré un problème avec plusieurs gestionnaires de chargeurs et des 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é le 2ème et les passages suivants via onResume )

N'oubliez pas non plus d'attribuer des identifiants différents pour chacun de vos chargeurs dans une activité (ce qui peut être un peu un problème avec des fragments au sein de cette activité si vous ne faites pas attention à votre numérotation)


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

InitLoader essayé sur onCreate avec des arguments nuls (les documents disent que c'est ok) & restartLoader (avec des arguments valides) dans onResume .... les documents sont erronés & initLoader lève une exception de pointeur nul.

Redémarrage essayé Le chargeur uniquement ... fonctionne pendant un certain temps mais souffle sur la réorientation du 5e ou 6e écran.

J'ai essayé initLoader dans onResume ; fonctionne à nouveau pendant un certain temps et puis souffle. (en particulier l'erreur "Appelé doRetain lorsqu'il n'est pas démarré:" ...)

J'ai essayé ce qui suit: (extrait d'une classe de couverture dont l'ID 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 a toujours jeté un problème occasionnel.


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

dans le cas des gestionnaires qui ne peuvent pas être démarrés tant que les résultats ne sont pas revenus d'un autre gestionnaire ou d'une autre tâche (c'est-à-dire ne peuvent pas être initialisés dans onCreate ), j'utilise uniquement initLoader . (Je n'ai peut-être pas raison, mais cela semble fonctionner. Ces chargeurs secondaires ne font pas partie de l'état d'instance immédiat, donc utiliser initLoader peut être correct dans ce cas)

cycle de la vie


En regardant les diagrammes et les documents, j'aurais pensé que initLoader devrait aller dans onCreate & restartLoader dans onRestart for Activities, mais cela laisse Fragments utilisant un modèle différent et je n'ai pas eu le temps de vérifier si cela est réellement stable. Quelqu'un d'autre peut-il dire s'il réussit avec ce modèle d'activités?

Simon
la source
/ @ Simon est correct à 100% et cela devrait être la réponse acceptée. Je n'ai pas tout à fait cru sa réponse et j'ai passé plusieurs heures à essayer de trouver différentes façons de faire ce travail. Dès que j'ai déplacé l'appel initLoader vers onCreate, les choses ont commencé à fonctionner. Vous avez ensuite besoin du drapeau à un coup pour tenir compte des heures d'appel de onStart, mais pas de onCreate
CjS
2
"J'ai essayé de redémarrer le chargeur uniquement ... fonctionne pendant un certain temps mais souffle sur la réorientation du 5ème ou 6ème écran." Cela fait? Je l'ai juste essayé et j'ai tourné l'écran une centaine de fois et je n'ai pas explosé. Quel genre d'exception obtenez-vous?
Tom anMoney
-1 J'apprécie l'effort de recherche derrière cette réponse mais la plupart des résultats sont incorrects.
Emanuel Moecklin
1
@IgorGanapolsky presque tout. Si vous lisez et comprenez ma réponse, vous comprendrez ce que font initLoader et restartLoader et quand les utiliser et vous comprendrez également pourquoi presque toutes les conclusions de Simon sont fausses. Il n'y a aucun lien entre le cycle de vie d'un fragment / activité et la décision d'utiliser initLoader / restartLoader (avec une mise en garde que j'explique sous les changements de configuration). Simon conclut par essais et erreurs que le cycle de vie est l'indice pour comprendre les deux méthodes, mais ce n'est pas le cas.
Emanuel Moecklin le
@IgorGanapolsky Je n'essaye pas d'annoncer ma propre réponse. J'essaie simplement d'aider d'autres développeurs et de les empêcher d'utiliser les résultats de Simon pour leurs propres applications. Une fois que vous comprenez à quoi servent les deux méthodes, tout devient assez évident et simple à mettre en œuvre.
Emanuel Moecklin
0

initLoaderré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 l'appelez avec de nouveaux paramètres. Le chargeur devrait idéalement notifier automatiquement l'activité des nouvelles données. Si l'écran tournait, initLoaderserait appelé à nouveau et les anciennes données seraient immédiatement affichées.

restartLoaderest pour lorsque vous souhaitez forcer un rechargement et modifier les paramètres également. Si vous deviez créer un écran de connexion à l'aide de chargeurs, vous n'appeleriez que restartLoaderchaque fois que vous cliqueriez sur le bouton. (Le bouton peut être cliqué plusieurs fois en raison d'informations d'identification incorrectes, etc.). Vous n'appeleriez jamais initLoaderlors de la restauration de l'état de l'instance enregistrée de l'activité dans le cas où l'écran serait pivoté alors qu'une connexion était en cours.

Monstieur
la source
-1

Si le chargeur existe déjà, restartLoader arrêtera / annulera / détruira l'ancien, tandis qu'initLoader l'initialisera simplement avec le rappel donné. Je ne peux pas savoir ce que font les anciens rappels dans ces cas, mais je suppose qu'ils seront simplement abandonnés.

J'ai scanné http://grepcode.com/file/repository.grepcode.com/java/ext/com.google.android/android/4.0.1_r1/android/app/LoaderManager.java mais je ne trouve pas de quoi exactement la différence est, à part cela, les méthodes font des choses différentes. Donc je dirais, utilisez initLoader la première fois et redémarrez pour les fois suivantes, bien que je ne puisse pas dire avec certitude ce que chacun d'eux fera exactement.

koljaTM
la source
Et que va initLoader- t - il faire dans ce cas?
theomega
-1

Initialiser le chargeur au premier démarrage utilise la méthode loadInBackground (), au deuxième démarrage, il sera omis. Donc, à mon avis, la meilleure solution est:

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

J'ai passé beaucoup de temps à trouver cette solution - restartLoader (...) ne fonctionnait pas correctement dans mon cas. Le seul forceLoad () permet de terminer le thread de chargement précédent sans rappel (ainsi vous aurez toutes les transactions db terminées correctement) et redémarre un nouveau thread. Oui, cela demande du temps supplémentaire, mais est plus stable. Seul le dernier thread démarré prendra le rappel. Ainsi, si vous voulez faire des tests avec l'interruption de vos transactions db - votre bienvenue, essayez de redémarrerLoader (...), sinon forceLoad (). La seule commodité de restartLoader (...) est de fournir de nouvelles données initiales, je veux dire des paramètres. Et n'oubliez pas de détruire loader dans la méthode onDetach () du Fragment approprié dans ce cas. Gardez également à l'esprit que parfois, lorsque vous avez une activité et, laissez-les dire, 2 fragments avec Loader pour chaque activité incluse - vous n'atteindrez que 2 gestionnaires de chargeur, donc Activity partage son LoaderManager avec le (s) fragment (s), qui s'affiche en premier à l'écran lors du chargement. Essayez LoaderManager.enableDebugLogging (true); pour voir les détails dans chaque cas.

user1700099
la source
2
-1 pour encapsuler l'appel getLoader(0)dans un fichier try { ... } catch (Exception e) { ... }.
Alex Lockwood