Differenza tra initLoader e restartLoader in LoaderManager


129

Mi sono completamente perso per quanto riguarda le differenze tra initLoaderle restartLoaderfunzioni di e LoaderManager:

  • Entrambi hanno la stessa firma.
  • restartLoader crea anche un caricatore, se non esiste ("Avvia un nuovo o riavvia un caricatore esistente in questo gestore").

C'è qualche relazione tra i due metodi? La chiamata chiama restartLoadersempre initLoader? Posso chiamare restartLoadersenza dover chiamare initLoader? È sicuro chiamare initLoaderdue volte per aggiornare i dati? Quando dovrei usare uno dei due e perché ?

Risposte:


202

Per rispondere a questa domanda è necessario scavare nel LoaderManagercodice. Mentre la documentazione per LoaderManager non è abbastanza chiara (o non ci sarebbe questa domanda), la documentazione per LoaderManagerImpl, una sottoclasse del LoaderManager astratto, è molto più illuminante.

initLoader

Chiama per inizializzare un determinato ID con un Caricatore. Se a questo ID è già associato un Caricatore, viene lasciato invariato e tutti i callback precedenti vengono sostituiti con quelli appena forniti. Se al momento non esiste un Caricatore per l'ID, ne viene creato e avviato uno nuovo.

Questa funzione dovrebbe essere generalmente utilizzata durante l'inizializzazione di un componente, per garantire che venga creato un Caricatore su cui si basa. Ciò consente di riutilizzare i dati di un Caricatore esistente se ne esiste già uno, quindi ad esempio quando un'attività viene ricreata dopo una modifica della configurazione, non è necessario ricreare i suoi caricatori.

restartLoader

Chiama per ricreare il Caricatore associato a un determinato ID. Se al momento è presente un Caricatore associato a questo ID, questo verrà annullato / arrestato / distrutto come appropriato. Verrà creato un nuovo Caricatore con gli argomenti indicati e i suoi dati ti verranno consegnati una volta disponibili.

[...] Dopo aver chiamato questa funzione, tutti i Caricatori precedenti associati a questo ID saranno considerati non validi e non riceverai ulteriori aggiornamenti dei dati da essi.

Esistono sostanzialmente due casi:

  1. Il caricatore con ID non esiste: entrambi i metodi creeranno un nuovo caricatore, quindi non vi è alcuna differenza
  2. Il caricatore con l'id esiste già: initLoadersostituirà solo i callback passati come parametro ma non annullerà o arresterà il caricatore. Ciò CursorLoadersignifica che il cursore rimane aperto e attivo (se così fosse prima della initLoaderchiamata). `restartLoader, d'altra parte, annullerà, fermerà e distruggerà il caricatore (e chiuderà l'origine dati sottostante come un cursore) e creerà un nuovo caricatore (che creerebbe anche un nuovo cursore e riascolterebbe la query se il caricatore è un cursore cursore).

Ecco il codice semplificato per entrambi i metodi:

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

Come possiamo vedere nel caso in cui il caricatore non esista (info == null) entrambi i metodi creeranno un nuovo caricatore (info = createAndInstallLoader (...)). Nel caso in cui il caricatore esista già initLoadersostituisce solo i callback (info.mCallbacks = ...) mentre restartLoaderinattiva il vecchio caricatore (verrà distrutto quando il nuovo caricatore completa il suo lavoro) e ne crea uno nuovo.

Così detto è ora chiaro quando usare initLoadere quando usare restartLoadere perché ha senso avere i due metodi. initLoaderviene utilizzato per garantire l'esistenza di un caricatore inizializzato. Se non esiste nessuno ne viene creato uno nuovo, se ne esiste già uno viene riutilizzato. Usiamo sempre questo metodo A MENO che non sia necessario un nuovo caricatore perché la query da eseguire è cambiata (non i dati sottostanti ma la query effettiva come nell'istruzione SQL per un CursorLoader), nel qual caso chiameremo restartLoader.

Il ciclo di vita di attività / frammento non ha nulla a che fare con la decisione di utilizzare l'uno o l'altro metodo (e non è necessario tenere traccia delle chiamate usando un flag one-shot come suggerito da Simon)! Questa decisione viene presa esclusivamente sulla base della "necessità" di un nuovo caricatore. Se vogliamo eseguire la stessa query che utilizziamo initLoader, se vogliamo eseguire una query diversa che utilizziamo restartLoader.

Potremmo sempre usare, restartLoaderma sarebbe inefficiente. Dopo una rotazione dello schermo o se l'utente si allontana dall'app e ritorna successivamente alla stessa attività, di solito vogliamo mostrare lo stesso risultato della query e quindi restartLoaderricreare inutilmente il caricatore e chiudere il risultato della query (potenzialmente costoso) sottostante.

È molto importante comprendere la differenza tra i dati caricati e la "query" per caricare tali dati. Supponiamo di utilizzare un CursorLoader che esegue una query su una tabella per gli ordini. Se un nuovo ordine viene aggiunto a quella tabella, CursorLoader utilizza onContentChanged () per informare l'interfaccia utente per aggiornare e mostrare il nuovo ordine (non è necessario utilizzarlo restartLoaderin questo caso). Se vogliamo visualizzare solo ordini aperti, abbiamo bisogno di una nuova query e utilizzeremo restartLoaderper restituire un nuovo CursorLoader che riflette la nuova query.


C'è qualche relazione tra i due metodi?

Condividono il codice per creare un nuovo Caricatore ma fanno cose diverse quando esiste già un Caricatore.

La chiamata chiama restartLoadersempre initLoader?

No, non lo fa mai.

Posso chiamare restartLoadersenza dover chiamare initLoader?

Sì.

È sicuro chiamare initLoaderdue volte per aggiornare i dati?

È sicuro chiamare initLoaderdue volte, ma nessun dato verrà aggiornato.

Quando dovrei usare uno dei due e perché ?


Ciò dovrebbe (si spera) essere chiaro dopo le mie spiegazioni sopra.

Modifiche alla configurazione

Un LoaderManager mantiene il suo stato attraverso le modifiche alla configurazione (comprese le modifiche all'orientamento), quindi si potrebbe pensare che non ci sia più niente da fare. Pensa di nuovo...

Prima di tutto, un LoaderManager non mantiene i callback, quindi se non fai nulla non riceverai chiamate ai tuoi metodi di callback simili onLoadFinished()e simili e molto probabilmente si romperà la tua app.

Pertanto DOVRIAMO chiamare almeno initLoaderper ripristinare i metodi di callback ( restartLoaderovviamente è anche possibile a). La documentazione afferma:

Se al momento della chiamata il chiamante è nel suo stato di avvio e il caricatore richiesto esiste già e ha generato i suoi dati, il callback onLoadFinished(Loader, D)verrà chiamato immediatamente (all'interno di questa funzione) [...].

Ciò significa che se chiamiamo initLoaderdopo una modifica dell'orientamento, riceveremo onLoadFinishedimmediatamente una chiamata perché i dati sono già caricati (supponendo che fosse il caso prima della modifica). Anche se può sembrare semplice, può essere difficile (non amiamo tutti Android ...).

Dobbiamo distinguere tra due casi:

  1. Gestisce le modifiche alla configurazione stessa: questo è il caso dei frammenti che usano setRetainInstance (true) o per un'attività con il android:configChangestag corrispondente nel manifest. Questi componenti non riceveranno una chiamata onCreate dopo, ad esempio, una rotazione dello schermo, quindi tieni presente di chiamare initLoader/restartLoaderun altro metodo di callback (ad es onActivityCreated(Bundle). In). Per poter inizializzare il / i Caricatore / i, è necessario memorizzare gli ID del caricatore (ad es. In un elenco). Poiché il componente viene mantenuto attraverso le modifiche alla configurazione, possiamo semplicemente passare in loop sugli ID caricatore esistenti e chiamare initLoader(loaderid, ...).
  2. Non gestisce da solo le modifiche alla configurazione: in questo caso i caricatori possono essere inizializzati in onCreate ma dobbiamo conservare manualmente gli ID del caricatore o non saremo in grado di effettuare le chiamate initLoader / restartLoader necessarie. Se gli ID sono archiviati in una ArrayList, eseguiremmo un
    outState.putIntegerArrayList(loaderIdsKey, loaderIdsArray)errore in onSaveInstanceState e ripristineremmo gli ID in onCreate: loaderIdsArray = savedInstanceState.getIntegerArrayList(loaderIdsKey)prima di effettuare le chiamate initLoader.

: +1: un ultimo punto. Se usi initLoader(e tutti i callback sono finiti, Loader è inattivo) dopo una rotazione non otterrai un onLoadFinishedcallback ma se lo usi restartLoaderlo farai?
Blundell,

Non corretto. Il metodo initLoader chiama il metodo onLoadFinished () prima che ritorni (se il caricatore è avviato e ha dati). Ho aggiunto un paragrafo sulle modifiche alla configurazione per spiegarlo in modo più dettagliato.
Emanuel Moecklin,

6
ah certo, una combinazione della tua risposta e di @ alexlockwood ti dà il quadro completo. Immagino che la risposta per gli altri sia: usa initLoader se la tua query è statica e riavviaLoader se vuoi cambiare la query
Blundell,

1
Ciò lo convoca bene: "usa initLoader se la tua query è statica e riavviaLoader se vuoi cambiare la query"
Emanuel Moecklin,

1
@Mhd. Tahawi non stai modificando i callback, li imposti solo dove dovrebbero andare. Dopo una rotazione dello schermo devono essere reimpostati perché Android non li terrà in giro per evitare perdite di memoria. Sei libero di impostarli su quello che vuoi purché facciano la cosa giusta.
Emanuel Moecklin,

46

La chiamata initLoaderquando il Caricatore è già stato creato (questo in genere accade dopo le modifiche alla configurazione, ad esempio) indica a LoaderManager di consegnare onLoadFinishedimmediatamente i dati più recenti del Caricatore . Se il Caricatore non è già stato creato (all'avvio dell'attività / frammento, ad esempio), la chiamata a initLoaderindica a LoaderManager di chiamare onCreateLoaderper creare il nuovo Caricatore.

La chiamata restartLoaderdistrugge un caricatore già esistente (così come tutti i dati esistenti ad esso associati) e dice al LoaderManager di chiamare onCreateLoaderper creare il nuovo caricatore e avviare un nuovo carico.


La documentazione è abbastanza chiara anche su questo:

  • initLoaderassicura che un Caricatore sia inizializzato e attivo. Se il caricatore non esiste già, ne viene creato uno e (se l'attività / frammento è attualmente avviato) avvia il caricatore. Altrimenti viene riutilizzato l'ultimo caricatore creato.

  • restartLoaderavvia un nuovo o riavvia un Caricatore esistente in questo gestore, registra i callback su di esso e (se l'attività / frammento è attualmente avviato) inizia a caricarlo. Se un caricatore con lo stesso ID è stato precedentemente avviato, verrà automaticamente distrutto quando il nuovo caricatore completa il suo lavoro. Il callback verrà recapitato prima che il vecchio caricatore venga distrutto.


@TomanMoney Ho spiegato cosa significa nella mia risposta. Di quale parte sei confuso?
Alex Lockwood,

hai appena ripassato il documento. Ma il documento non fornisce indicazioni su dove ogni metodo dovrebbe essere usato e perché è male rovinarlo. Nella mia esperienza, solo chiamare restartLoader e mai chiamare initLoader funziona bene. Quindi questo è ancora confuso.
Tom anMoney,

3
@TomanMoney Di solito si usa initLoader()in onCreate()/ onActivityCreated()quando l'attività / frammento si avvia per la prima volta. In questo modo, quando l'utente apre per la prima volta un'attività, il caricatore verrà creato per la prima volta ... ma su qualsiasi modifica successiva della configurazione in cui l'intera attività / frammento deve essere distrutta, la seguente chiamata initLoader()restituirà semplicemente il vecchio Loaderanziché crearne uno nuovo. Di solito si utilizza restartLoader()quando è necessario modificare la Loaderquery (ad esempio si desidera ottenere dati filtrati / ordinati, ecc.).
Alex Lockwood,

4
Sono ancora confuso sulla decisione dell'API di avere entrambi i metodi, poiché hanno la stessa firma. Perché l'API non potrebbe essere un singolo metodo startLoader () che fa sempre la "cosa giusta"? Penso che questa sia la parte che confonde molte persone.
Tom anMoney il

1
@TomanMoney La documentazione qui dice: developer.android.com/guide/components/loaders.html . "Si riconnettono automaticamente al cursore dell'ultimo caricatore quando vengono ricreati dopo una modifica della configurazione. Pertanto, non è necessario eseguire nuovamente la query dei propri dati."
IgorGanapolsky,

16

Di recente ho riscontrato un problema con più gestori del caricatore e modifiche all'orientamento dello schermo e vorrei dire che dopo un sacco di tentativi ed errori, il seguente schema funziona per me sia in Attività che in Frammenti:

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.

(in altre parole, impostare alcuni flag in modo che initLoader sia sempre eseguito una volta e che restartLoader sia eseguito al 2 ° passaggio e successivo su onResume )

Inoltre, ricorda di assegnare ID diversi per ciascuno dei tuoi caricatori all'interno di un'attività (che può essere un po 'un problema con i frammenti all'interno di quell'attività se non stai attento con la tua numerazione)


Ho provato a usare solo initLoader .... non sembrava funzionare in modo efficace.

Ho provato initLoader su onCreate con argomenti null (i documenti dicono che è ok) e restartLoader (con argomenti validi) in onResume .... i documenti sono sbagliati e initLoader genera un'eccezione nullpointer.

Riavvio provato Solo il caricatore ... funziona per un po 'ma soffia sul 5 ° o 6 ° ri-orientamento dello schermo.

Ho provato initLoader in onResume ; funziona ancora per un po 'e poi soffia. (in particolare l'errore "Chiamato doRetain quando non avviato:" ...)

Ho provato quanto segue: (estratto da una classe di copertura che ha l'ID caricatore passato nel costruttore)

/**
 * 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);
    }
}

(che ho trovato da qualche parte in Stack-Overflow)

Ancora una volta, questo ha funzionato per un po ', ma ha comunque gettato il problema occasionale.


Da quello che posso capire durante il debug, penso che ci sia qualcosa a che fare con lo stato dell'istanza save / restore che richiede che initLoader (/ s) sia eseguito nella parte onCreate del ciclo di vita se devono sopravvivere a una rotazione del ciclo . ( Potrei sbagliarmi.)

nel caso di Manager che non possono essere avviati fino a quando i risultati non ritornano da un altro manager o attività (cioè non possono essere inizializzati in onCreate ), utilizzo solo initLoader . (Potrei non essere corretto in questo, ma sembra funzionare. Questi caricatori secondari non fanno parte dello stato di istanza immediato, quindi l'utilizzo di initLoader potrebbe effettivamente essere corretto in questo caso)

ciclo vitale


Guardando i diagrammi e i documenti, avrei pensato che initLoader dovrebbe andare in onCreate & restartLoader in onRestart for Activities ma che lascia i frammenti usando un modello diverso e non ho avuto il tempo di indagare se questo è effettivamente stabile. Qualcun altro può commentare se ha successo con questo schema per le attività?


/ @ Simon è corretto al 100% e questa dovrebbe essere la risposta accettata. Non credevo abbastanza alla sua risposta e ho trascorso diverse ore cercando di trovare modi diversi per farlo funzionare. Non appena ho spostato la chiamata initLoader su onCreate le cose hanno iniziato a funzionare. È quindi necessario il flag one-shot per tenere conto dei tempi in cui viene chiamato onStart ma non onCreate
CjS

2
"Riprovato riavvio Il caricatore funziona solo per un po 'ma soffia sul ri-orientamento del 5 ° o 6 ° schermo." Lo fa? L'ho appena provato e ho ruotato lo schermo cento volte e non ho fatto esplodere. Che tipo di eccezione stai ricevendo?
Tom anMoney,

-1 Apprezzo lo sforzo di ricerca alla base di questa risposta, ma la maggior parte dei risultati non sono corretti.
Emanuel Moecklin,

1
@IgorGanapolsky quasi tutto. Se leggi e comprendi la mia risposta, capirai cosa fanno initLoader e restartLoader e quando utilizzarli e capirai anche perché quasi tutte le conclusioni di Simon sono sbagliate. Non esiste alcuna connessione tra il ciclo di vita di un frammento / attività e la decisione su quando utilizzare initLoader / restartLoader (con un avvertimento che spiego nelle modifiche alla configurazione). Simon conclude da prove ed errori che il ciclo di vita è la chiave per comprendere i due metodi, ma non lo è.
Emanuel Moecklin,

@IgorGanapolsky Non sto cercando di pubblicizzare la mia risposta. Sto semplicemente cercando di aiutare altri sviluppatori e impedire loro di utilizzare i risultati di Simon per le loro app. Una volta capito a cosa servono i due metodi, l'intera cosa diventa abbastanza ovvia e semplice da implementare.
Emanuel Moecklin,

0

initLoaderriutilizzerà gli stessi parametri se il caricatore esiste già. Restituisce immediatamente se i vecchi dati sono già caricati, anche se li chiami con nuovi parametri. Il caricatore dovrebbe idealmente avvisare automaticamente l'attività di nuovi dati. Se lo schermo ruotasse, initLoaderverrebbe richiamato di nuovo e i vecchi dati verrebbero immediatamente visualizzati.

restartLoaderè per quando si desidera forzare una ricarica e modificare anche i parametri. Se dovessi creare una schermata di accesso utilizzando i caricatori, chiameresti solo restartLoaderogni volta che si faceva clic sul pulsante. (È possibile fare clic più volte sul pulsante a causa di credenziali errate, ecc.). Chiameresti mai solo initLoaderquando ripristini lo stato di istanza salvato dell'attività nel caso in cui lo schermo fosse ruotato mentre era in corso un accesso.


-1

Se il caricatore esiste già, restartLoader interromperà / annullerà / distruggerà quello precedente, mentre initLoader lo inizializzerà semplicemente con il callback specificato. Non riesco a scoprire cosa fanno i vecchi callback in questi casi, ma immagino che verranno abbandonati.

Ho scansionato tramite http://grepcode.com/file/repository.grepcode.com/java/ext/com.google.android/android/4.0.1_r1/android/app/LoaderManager.java ma non riesco a scoprire quale sia l'esatto la differenza è, a parte questo, i metodi fanno cose diverse. Quindi direi, utilizzare initLoader la prima volta e riavviare per le seguenti volte, anche se non posso dire con certezza cosa farà esattamente ciascuno di essi.


E cosa farà initLoaderin questo caso?
theomega,

-1

Il caricatore di Init al primo avvio utilizza il metodo loadInBackground (), al secondo avvio verrà omesso. Quindi, a mio avviso, la soluzione migliore è:

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

Ho impiegato molto tempo a trovare questa soluzione - restartLoader (...) non ha funzionato correttamente nel mio caso. L'unico forceLoad () consente di terminare il thread di caricamento precedente senza callback (quindi tutte le transazioni db saranno completate correttamente) e ricomincia il nuovo thread. Sì, richiede del tempo extra, ma è più stabile. Solo l'ultimo thread avviato prenderà il callback. Pertanto, se si desidera eseguire test con l'interruzione delle transazioni db - benvenuto, provare a restartLoader (...), altrimenti forceLoad (). L'unica comodità di restartLoader (...) è quella di fornire nuovi dati iniziali, intendo parametri. E per favore non dimenticare di distruggere loader nel metodo onDetach () del frammento adatto in questo caso. Inoltre, tieni presente che alcune volte, quando hai un'attività e, lasciali dire, 2 frammenti con Loader ciascuno comprensivo di attività: raggiungerai solo 2 Loader Manager, quindi Activity condivide il suo LoaderManager con frammenti, che viene mostrato sullo schermo per primo durante il caricamento. Prova LoaderManager.enableDebugLogging (true); per vedere i dettagli in ciascun caso.


2
-1 per il wrapping della chiamata getLoader(0)in a try { ... } catch (Exception e) { ... }.
Alex Lockwood,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.