Diferença entre initLoader e restartLoader no LoaderManager

129

Estou completamente perdido em relação às diferenças entre initLoadere as restartLoaderfunções do LoaderManager:

  • Ambos têm a mesma assinatura.
  • restartLoader também cria um carregador, se ele não existir ("Inicia um novo ou reinicia um carregador existente neste gerenciador").

Existe alguma relação entre os dois métodos? Ligar restartLoadersempre chama initLoader? Posso ligar restartLoadersem ter que ligar initLoader? É seguro ligar initLoaderduas vezes para atualizar os dados? Quando devo usar um dos dois e por quê ?

theomega
fonte

Respostas:

202

Para responder a essa pergunta, você precisa digitar o LoaderManagercódigo. Embora a documentação para o próprio LoaderManager não seja clara o suficiente (ou não haveria essa pergunta), a documentação para o LoaderManagerImpl, uma subclasse do abstrato LoaderManager, é muito mais esclarecedora.

initLoader

Ligue para inicializar um ID específico com um carregador. Se esse ID já tiver um Loader associado, ele permanecerá inalterado e os retornos de chamada anteriores serão substituídos pelos recém-fornecidos. Se não houver atualmente um Loader para o ID, um novo será criado e iniciado.

Essa função geralmente deve ser usada quando um componente está sendo inicializado, para garantir que um Loader em que ele se baseia seja criado. Isso permite reutilizar os dados de um Carregador existente, se já houver um, de modo que, por exemplo, quando uma Atividade é recriada após uma alteração na configuração, ela não precisa recriar seus carregadores.

restartLoader

Ligue para recriar o Loader associado a um ID específico. Se houver atualmente um Loader associado a esse ID, ele será cancelado / parado / destruído conforme apropriado. Um novo Loader com os argumentos fornecidos será criado e seus dados entregues a você assim que estiverem disponíveis.

[...] Depois de chamar esta função, qualquer Carregador anterior associado a este ID será considerado inválido e você não receberá mais atualizações de dados.

Existem basicamente dois casos:

  1. O carregador com o ID não existe: ambos os métodos criarão um novo carregador, portanto não haverá diferença
  2. O carregador com o ID já existe: initLoadersubstituirá apenas os retornos de chamada passados ​​como parâmetro, mas não cancelará ou interromperá o carregador. Para um CursorLoaderque significa que o cursor permanece aberto e ativo (se foi esse o caso antes da initLoaderchamada). `restartLoader, por outro lado, irá cancelar, parar e destruir o carregador (e fechar a fonte de dados subjacente como um cursor) e criar um novo carregador (que também criaria um novo cursor e executaria novamente a consulta se o carregador estiver um CursorLoader).

Aqui está o código simplificado para os dois 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, caso o carregador não exista (info == null), ambos os métodos criarão um novo carregador (info = createAndInstallLoader (...)). Caso o carregador já exista, initLoadersubstitua apenas os retornos de chamada (info.mCallbacks = ...) enquanto restartLoaderinativa o carregador antigo (ele será destruído quando o novo carregador concluir seu trabalho) e, em seguida, criará um novo.

Assim dito, agora está claro quando usar initLoadere quando usar restartLoadere por que faz sentido ter os dois métodos. initLoaderé usado para garantir que haja um carregador inicializado. Se nenhum existir, um novo será criado, se já existir, será reutilizado. Sempre usamos esse método, a menos que seja necessário um novo carregador, porque a consulta a ser executada mudou (não os dados subjacentes, mas a consulta real, como na instrução SQL para um CursorLoader), caso em que chamaremos restartLoader.

O ciclo de vida da atividade / fragmento não tem nada a ver com a decisão de usar um ou outro método (e não há necessidade de acompanhar as chamadas usando um sinalizador único, como sugeriu Simon)! Esta decisão é tomada exclusivamente com base na "necessidade" de um novo carregador. Se queremos executar a mesma consulta que usamos initLoader, se queremos executar uma consulta diferente, usamos restartLoader.

Nós sempre poderíamos usar, restartLoadermas isso seria ineficiente. Após uma rotação da tela ou se o usuário sair do aplicativo e retornar posteriormente para a mesma atividade, geralmente queremos mostrar o mesmo resultado da consulta e, assim restartLoader, recriar desnecessariamente o carregador e dispensar o resultado da consulta subjacente (potencialmente caro).

É muito importante entender a diferença entre os dados carregados e a "consulta" para carregar esses dados. Vamos supor que usamos um CursorLoader consultando uma tabela para pedidos. Se um novo pedido for adicionado a essa tabela, o CursorLoader usa onContentChanged () para informar a interface do usuário para atualizar e mostrar o novo pedido (não é necessário usar restartLoaderneste caso). Se queremos exibir apenas pedidos em aberto, precisamos de uma nova consulta e restartLoaderusaríamos para retornar um novo CursorLoader refletindo a nova consulta.


Existe alguma relação entre os dois métodos?

Eles compartilham o código para criar um novo Loader, mas fazem coisas diferentes quando um carregador já existe.

Ligar restartLoadersempre chama initLoader?

Não, nunca faz.

Posso ligar restartLoadersem ter que ligar initLoader?

Sim.

É seguro ligar initLoaderduas vezes para atualizar os dados?

É seguro ligar initLoaderduas vezes, mas nenhum dado será atualizado.

Quando devo usar um dos dois e por quê ?


Espero que isso fique claro depois das minhas explicações acima.

Mudanças na configuração

Um LoaderManager mantém seu estado através de alterações na configuração (incluindo alterações de orientação), assim você acha que não há mais nada a fazer. Pense de novo...

Antes de tudo, um LoaderManager não retém os retornos de chamada; portanto, se você não fizer nada, não receberá chamadas para seus métodos de retorno de chamada, como assim por diante, onLoadFinished()e isso provavelmente interromperá seu aplicativo.

Portanto, temos que chamar pelo menos initLoaderpara restaurar os métodos de retorno de chamada (a restartLoaderé, é claro, possível também). A documentação declara:

Se no momento da chamada o chamador estiver em seu estado inicial e o carregador solicitado já existir e tiver gerado seus dados, o retorno de chamada onLoadFinished(Loader, D)será chamado imediatamente (dentro desta função) [...].

Isso significa que, se ligarmos initLoaderapós uma mudança de orientação, receberemos uma onLoadFinishedligação imediatamente porque os dados já estão carregados (assumindo que foi o caso antes da mudança). Embora isso pareça simples, pode ser complicado (nem todos gostamos do Android ...).

Temos que distinguir entre dois casos:

  1. Lida com as alterações na configuração: este é o caso de Fragmentos que usam setRetainInstance (true) ou de uma Atividade com a android:configChangestag correspondente no manifesto. Esses componentes não receberão uma chamada onCreate após, por exemplo, uma rotação de tela, portanto, lembre-se de chamar initLoader/restartLoaderoutro método de retorno de chamada (por exemplo, in onActivityCreated(Bundle)). Para poder inicializar o (s) carregador (es), os IDs do carregador precisam ser armazenados (por exemplo, em uma lista). Como o componente é retido nas alterações de configuração, podemos simplesmente fazer um loop sobre os IDs e chamadas existentes do carregador initLoader(loaderid, ...).
  2. Não lida com as alterações de configuração: nesse caso, os Carregadores podem ser inicializados no onCreate, mas precisamos manter manualmente os IDs do carregador ou não seremos capazes de fazer as chamadas necessárias initLoader / restartLoader. Se os IDs estiverem armazenados em um ArrayList, faremos um
    outState.putIntegerArrayList(loaderIdsKey, loaderIdsArray)em onSaveInstanceState e restauramos os IDs em onCreate: loaderIdsArray = savedInstanceState.getIntegerArrayList(loaderIdsKey)antes de fazermos as chamadas initLoader.
Emanuel Moecklin
fonte
: +1: um último ponto. Se você usar initLoader(e todos os retornos de chamada tiverem terminado, o Loader estiver ocioso) após uma rotação, você não receberá um onLoadFinishedretorno de chamada, mas, se o usar, restartLoaderirá?
precisa
Incorreta. O método initLoader chama o método onLoadFinished () antes de retornar (se o carregador for iniciado e tiver dados). Eu adicionei um parágrafo sobre alterações na configuração para explicar isso em mais detalhes.
Emanuel Moecklin
6
ah, é claro, uma combinação da sua resposta e da @ alexlockwood dá uma imagem completa. Acho que a resposta para os outros é, use initLoader Se a consulta é estático e restartLoader se você quiser alterar a consulta
Blundell
1
Isso convém muito bem: "use initLoader se sua consulta for estática e restartLoader se desejar alterar a consulta"
Emanuel Moecklin
1
@Mhd. Tahawi, você não está alterando os retornos de chamada, apenas os define para onde quer que eles devam ir. Após uma rotação da tela, eles precisam ser redefinidos, porque o Android não os manterá por perto para evitar vazamentos de memória. Você é livre para configurá-los para o que quiser, desde que eles façam a coisa certa.
Emanuel Moecklin
46

Chamar initLoaderquando o Loader já tiver sido criado (isso normalmente ocorre após alterações na configuração, por exemplo) informa ao LoaderManager para entregar os dados mais recentes do Loader onLoadFinishedimediatamente. Se o Loader ainda não tiver sido criado (quando a atividade / fragmento for iniciado, por exemplo), a chamada para initLoaderinforma ao LoaderManager onCreateLoaderpara criar o novo Loader.

A chamada restartLoaderdestrói um carregador já existente (assim como quaisquer dados existentes associados a ele) e pede ao LoaderManager para ligar onCreateLoaderpara criar o novo carregador e iniciar uma nova carga.


A documentação também é bastante clara sobre isso:

  • initLoadergarante que um carregador seja inicializado e ativo. Se o carregador ainda não existir, um será criado e (se a atividade / fragmento estiver iniciado no momento) inicia o carregador. Caso contrário, o último carregador criado será reutilizado.

  • restartLoaderinicia um novo ou reinicia um carregador existente neste gerenciador, registra os retornos de chamada para ele e (se a atividade / fragmento estiver iniciado no momento) começa a carregá-lo. Se um carregador com o mesmo ID tiver sido iniciado anteriormente, ele será destruído automaticamente quando o novo carregador concluir seu trabalho. O retorno de chamada será entregue antes que o carregador antigo seja destruído.

Alex Lockwood
fonte
@TomanMoney Expliquei o que isso significa na minha resposta. De que parte você está confuso?
Alex Lockwood
você acabou de revisar novamente o documento. Mas o documento não fornece nenhuma indicação de onde cada método deve ser usado e por que é ruim estragar tudo. Na minha experiência, basta chamar restartLoader e nunca chamar initLoader funciona bem. Então, isso ainda é confuso.
Tom anMoney
3
@TomanMoney Normalmente você usa initLoader()em onCreate()/ onActivityCreated()quando a atividade / fragmento é inicializada. Dessa maneira, quando o usuário abrir uma atividade pela primeira vez, o carregador será criado pela primeira vez ... mas em qualquer alteração subsequente na configuração em que toda a atividade / fragmento deva ser destruída, a seguinte chamada para initLoader()retornará a antiga em Loadervez de criando um novo. Geralmente você usa restartLoader()quando precisa alterar a Loaderconsulta do (ou seja, deseja obter dados filtrados / classificados, etc.).
Alex Lockwood
4
Ainda estou confuso sobre a decisão da API de ter os dois métodos, pois eles têm a mesma assinatura. Por que a API não poderia ser um único método startLoader () que faz a "coisa certa" toda vez? Eu acho que essa é a parte que confunde muita gente.
TomTomMoney #
1
@TomanMoney A documentação aqui diz: developer.android.com/guide/components/loaders.html . "Eles se reconectam automaticamente ao cursor do último carregador quando são recriados após uma alteração na configuração. Portanto, eles não precisam consultar novamente seus dados".
IgorGanapolsky 14/02
16

Recentemente, encontrei um problema com vários gerenciadores de carregadores e alterações na orientação da tela e gostaria de dizer que, após várias tentativas e erros, o seguinte padrão funciona para mim em Atividades e 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.

(em outras palavras, defina um sinalizador para que o initLoader seja sempre executado uma vez e o restartLoader seja executado na segunda e subsequente passagem pelo onResume )

Além disso, lembre-se de atribuir IDs diferentes para cada um dos seus carregadores em uma Atividade (o que pode ser um pouco problemático para fragmentos dessa atividade, se você não tomar cuidado com a numeração)


Tentei usar apenas o initLoader .... não parecia funcionar de maneira eficaz.

Tentei o initLoader no onCreate com argumentos nulos (os documentos dizem que isso está ok) e restartLoader (com argumentos válidos) no onResume .... os documentos estão errados e o initLoader lança uma exceção nullpointer.

Tentou restartLoader única ... obras para um tempo, mas golpes no 5º ou 6º tela re-orientação.

Tentei initLoader in onResume ; novamente funciona por um tempo e depois sopra. (especificamente o erro "Chamado doRetain quando não iniciado:" ...)

Tentei o seguinte: (trecho de uma classe de capa que possui o ID do carregador passado para o construtor)

/**
 * 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 eu encontrei em algum lugar no Stack-Overflow)

Novamente, isso funcionou por um tempo, mas ainda gerava uma falha ocasional.


Pelo que posso descobrir durante a depuração, acho que há algo a ver com o estado da instância de salvar / restaurar que requer que initLoader (s) seja executado na parte onCreate do ciclo de vida para sobreviver a uma rotação do ciclo . ( Eu posso estar errado.)

no caso de gerentes que não podem ser iniciados até que os resultados retornem de outro gerente ou tarefa (ou seja, não podem ser inicializados no onCreate ), eu uso apenas o initLoader . (Posso não estar correto nisso, mas parece funcionar. Esses carregadores secundários não fazem parte do estado imediato da instância; portanto, o uso do initLoader pode estar correto neste caso)

ciclo da vida


Olhando para os diagramas e documentos, eu pensaria que o initLoader deveria entrar no onCreate e restartLoader no onRestart for Activities, mas isso deixa os Fragments usando algum padrão diferente e não tive tempo de investigar se isso é realmente estável. Alguém mais pode comentar se eles obtiveram sucesso com esse padrão de atividades?

Simon
fonte
/ @ Simon está 100% correto e essa deve ser a resposta aceita. Não acreditei muito na resposta dele e passei várias horas tentando encontrar maneiras diferentes de fazer esse trabalho. Assim que mudei a chamada initLoader para onCreate, as coisas começaram a funcionar. Você precisa então a bandeira one-shot para a conta para os tempos onStart é chamado, mas não onCreate
CJS
2
"Tentei apenas o restartLoader ... funciona por um tempo, mas é acionado na 5ª ou na 6ª reorientação da tela." Faz? Eu apenas tentei e girei a tela centenas de vezes e não consegui explodir. Que tipo de exceção você está recebendo?
Tom anMoney
-1 Agradeço o esforço de pesquisa por trás dessa resposta, mas a maioria dos resultados está incorreta.
Emanuel Moecklin
1
@IgorGanapolsky quase tudo. Se você ler e entender minha resposta, entenderá o que o initLoader e o restartLoader fazem e quando usar quais e também entenderá por que quase todas as conclusões de Simon estão erradas. Não há conexão entre o ciclo de vida de um fragmento / atividade e a decisão de quando usar initLoader / restartLoader (com uma ressalva que explico nas alterações de configuração). Simon conclui por tentativa e erro que o ciclo de vida é a pista para entender os dois métodos, mas não é.
Emanuel Moecklin
@IgorGanapolsky Não estou tentando anunciar minha própria resposta. Estou apenas tentando ajudar outros desenvolvedores e impedi-los de usar os resultados de Simon para seus próprios aplicativos. Depois de entender para que servem os dois métodos, tudo se torna bastante óbvio e fácil de implementar.
Emanuel Moecklin
0

initLoaderreutilizará os mesmos parâmetros se o carregador já existir. Ele retorna imediatamente se os dados antigos já estiverem carregados, mesmo se você os chamar com novos parâmetros. Idealmente, o carregador deve notificar automaticamente a atividade de novos dados. Se a tela girasse, initLoaderseria chamada novamente e os dados antigos seriam exibidos imediatamente.

restartLoaderé para quando você deseja forçar uma recarga e alterar os parâmetros também. Se você fizesse uma tela de login usando carregadores, ligaria apenas restartLoadertoda vez que o botão fosse clicado. (O botão pode ser clicado várias vezes devido a credenciais incorretas, etc.). Você só ligaria initLoaderao restaurar o estado da instância salva da atividade caso a tela fosse girada enquanto um logon estava em andamento.

Monstieur
fonte
-1

Se o carregador já existir, restartLoader irá parar / cancelar / destruir o antigo, enquanto o initLoader apenas o inicializará com o retorno de chamada fornecido. Não consigo descobrir o que os antigos retornos de chamada fazem nesses casos, mas acho que eles serão abandonados.

Digitalizei através do http://grepcode.com/file/repository.grepcode.com/java/ext/com.google.android/android/4.0.1_r1/android/app/LoaderManager.java, mas não consigo descobrir qual é o valor exato A diferença é que, além disso, os métodos fazem coisas diferentes. Então, eu diria, use initLoader pela primeira vez e reinicie pelos seguintes horários, embora eu não possa dizer com certeza o que cada um deles fará exatamente.

koljaTM
fonte
E o que fará initLoaderneste caso?
theomega
-1

O carregador de inicialização na primeira inicialização usa o método loadInBackground (); na segunda inicialização, ele será omitido. Então, na minha opinião, a melhor solução é:

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

Gastei muito tempo para encontrar esta solução - restartLoader (...) não funcionou corretamente no meu caso. O único forceLoad () permite concluir o encadeamento de carregamento anterior sem retorno de chamada (para que todas as transações de banco de dados sejam concluídas corretamente) e inicie novamente um novo encadeamento. Sim, exige um tempo extra, mas é mais estável. Somente o último encadeamento iniciado receberá retorno de chamada. Portanto, se você quiser fazer testes com a interrupção de suas transações de banco de dados - de nada, tente reiniciar o Loader (...), caso contrário forceLoad (). A única conveniência do restartLoader (...) é fornecer novos dados iniciais, quero dizer parâmetros. E não esqueça de destruir o carregador no método onDetach () do fragmento adequado neste caso. Lembre-se também de que, algumas vezes, quando você tem uma atividade e, digamos, 2 fragmentos com o Loader, cada uma das atividades inclusivas - você alcançará apenas 2 Loader Managers, portanto o Activity compartilha seu LoaderManager com o (s) fragmento (s), que é mostrado primeiro na tela durante o carregamento. Tente LoaderManager.enableDebugLogging (true); para ver os detalhes em cada caso específico.

user1700099
fonte
2
-1 para agrupar a chamada getLoader(0)em a try { ... } catch (Exception e) { ... }.
Alex Lockwood