Uso de CursorLoader sem ContentProvider

107

A documentação do Android SDK diz que o startManagingCursor()método está obsoleto:

Este método está obsoleto. Em vez disso, use a nova classe CursorLoader com LoaderManager; isso também está disponível em plataformas mais antigas por meio do pacote de compatibilidade do Android. Este método permite que a atividade cuide do gerenciamento do ciclo de vida do Cursor fornecido para você com base no ciclo de vida da atividade. Ou seja, quando a atividade for interrompida, ela chamará automaticamente deactivate () no Cursor fornecido e, quando for reiniciada posteriormente, chamará a consulta de consulta () para você. Quando a atividade é destruída, todos os Cursores gerenciados serão fechados automaticamente. Se você está direcionando HONEYCOMB ou posterior, considere usar LoaderManager, disponível via getLoaderManager ()

Então, eu gostaria de usar CursorLoader. Mas como posso usá-lo com custom CursorAdaptere sem ContentProvider, quando preciso de URI no construtor de CursorLoader?

sealskej
fonte
@Alex Lockwood, por que estamos usando CursorAdapter sem ContentProvider, sugira-me stackoverflow.com/questions/20419278/…
por que estamos usando CursorAdapter sem ContentProvider, sugira-me stackoverflow.com/questions/20419278/…

Respostas:

155

Escrevi um CursorLoader simples que não precisa de um provedor de conteúdo:

import android.content.Context;
import android.database.Cursor;
import android.support.v4.content.AsyncTaskLoader;

/**
 * Used to write apps that run on platforms prior to Android 3.0. When running
 * on Android 3.0 or above, this implementation is still used; it does not try
 * to switch to the framework's implementation. See the framework SDK
 * documentation for a class overview.
 *
 * This was based on the CursorLoader class
 */
public abstract class SimpleCursorLoader extends AsyncTaskLoader<Cursor> {
    private Cursor mCursor;

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

    /* Runs on a worker thread */
    @Override
    public abstract Cursor loadInBackground();

    /* Runs on the UI thread */
    @Override
    public void deliverResult(Cursor cursor) {
        if (isReset()) {
            // An async query came in while the loader is stopped
            if (cursor != null) {
                cursor.close();
            }
            return;
        }
        Cursor oldCursor = mCursor;
        mCursor = cursor;

        if (isStarted()) {
            super.deliverResult(cursor);
        }

        if (oldCursor != null && oldCursor != cursor && !oldCursor.isClosed()) {
            oldCursor.close();
        }
    }

    /**
     * Starts an asynchronous load of the contacts list data. When the result is ready the callbacks
     * will be called on the UI thread. If a previous load has been completed and is still valid
     * the result may be passed to the callbacks immediately.
     * <p/>
     * Must be called from the UI thread
     */
    @Override
    protected void onStartLoading() {
        if (mCursor != null) {
            deliverResult(mCursor);
        }
        if (takeContentChanged() || mCursor == null) {
            forceLoad();
        }
    }

    /**
     * Must be called from the UI thread
     */
    @Override
    protected void onStopLoading() {
        // Attempt to cancel the current load task if possible.
        cancelLoad();
    }

    @Override
    public void onCanceled(Cursor cursor) {
        if (cursor != null && !cursor.isClosed()) {
            cursor.close();
        }
    }

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

        // Ensure the loader is stopped
        onStopLoading();

        if (mCursor != null && !mCursor.isClosed()) {
            mCursor.close();
        }
        mCursor = null;
    }
}

Só precisa da AsyncTaskLoaderaula. Tanto o do Android 3.0 ou superior, quanto o que vem com o pacote de compatibilidade.

Também escrevi umListLoader que é compatível com o LoadManagere é usado para recuperar uma java.util.Listcoleção genérica .

Cristian
fonte
13
Encontrou um bom exemplo de código que usa isso - bitbucket.org/ssutee/418496_mobileapp/src/fc5ee705a2fd/demo/… - achei muito útil!
Shushu de
@Cristian Obrigado pelo exemplo. Qual é a licença associada à sua classe. Como pode ser reutilizado?
codinguser
2
A licença é Apache 2.0; você pode reutilizá-lo onde / quando quiser. Deixe-me saber se você tem alguma melhoria.
Cristian
14
Coisas boas! Os usuários devem estar cientes de uma limitação, que é que não há mecanismo para atualizar as alterações de dados (como os carregadores devem fazer)
emmby
1
@Jadeye aqui você tem o homem: ListLoader e SupportListLoader
Cristian
23

Escreva seu próprio carregador que usa sua classe de banco de dados em vez de um provedor de conteúdo. A maneira mais fácil é simplesmente pegar a fonte da CursorLoaderclasse da biblioteca de compatibilidade e substituir as consultas do provedor por consultas à sua própria classe auxiliar de banco de dados.

Nikolay Elenkov
fonte
1
Esta é a maneira mais fácil na minha opinião. No meu aplicativo, criei um CursorLoaderdescendente para gerenciar um cursor SQLite, aparte do construtor. Só precisei substituir o loadInBackgroundmétodo para substituir a consulta do provedor pela consulta do meu cursor
Jose_GD
14

O SimpleCursorLoader é uma solução simples, porém não suporta a atualização do carregador quando os dados mudam. CommonsWare tem uma biblioteca loaderex que adiciona um SQLiteCursorLoader e oferece suporte a novas consultas sobre alterações de dados.

https://github.com/commonsguy/cwac-loaderex

embrulhar
fonte
2
No entanto, para fazer uso da nova consulta automática, você precisa usar o mesmo carregador para a IU e também para as atualizações, limitando sua usabilidade para serviços em segundo plano.
ge0rg
12

Uma terceira opção seria simplesmente substituir loadInBackground:

public class CustomCursorLoader extends CursorLoader {
    private final ForceLoadContentObserver mObserver = new ForceLoadContentObserver();

    @Override
    public Cursor loadInBackground() {
        Cursor cursor = ... // get your cursor from wherever you like

        if (cursor != null) {
            // Ensure the cursor window is filled
            cursor.getCount();
            cursor.registerContentObserver(mObserver);
        }

        return cursor;
    }
};

Isso também cuidará de consultar novamente o cursor quando o banco de dados for alterado.

Apenas ressalva: você terá que definir outro observador, já que o Google em sua infinita sabedoria decidiu tornar seu pacote privado. Se você colocar a classe no mesmo pacote que o original (ou compat), você pode usar o observador original. O observador é um objeto muito leve e não é usado em nenhum outro lugar, então isso não faz muita diferença.

Timo Ohr
fonte
Minha observação no teste rápido é que registerContentObserver só será chamado contra o cursor se o cursor for direcionado a um provedor de conteúdo. Você pode confirmar / negar isso?
Nick Campion
1
Não precisa ser necessariamente um ContentProvider. Mas o cursor precisa ser registrado em um uri de notificação (setNotificationUri) e, em seguida, precisa ser notificado por alguém (geralmente um ContentProvider, mas pode ser qualquer coisa) chamando ContentResolver.notifyChange.
Timo Ohr
4
Sim. no seu CustomLoader loadInBackground() , antes de retornar o cursor, digamos que cursor.setNotificationUri(getContext().getContentResolver(), uri);o uri pode apenas de String aleatória como Uri.parse("content://query_slot1"). Parece que não importa se o uri realmente existe ou não. E uma vez que fiz a operação no DB. Say getContentResolver().notifyChange(uri, null);faria o truque. Então, posso criar alguns "slots de uri de consulta" em um arquivo contant para o aplicativo com um pequeno número de consultas. Eu testo inserir o registro do banco de dados em tempo de execução e parece funcionar, mas ainda duvido que seja uma boa prática nele. Alguma sugestão?
Yeung de
Estou usando este método com a sugestão de @Yeung e tudo funciona, incluindo o recarregamento automático do cursor na atualização do banco de dados.
DavidH
não precisa de nenhum unregisterContentObserver?
GPack de
2

A terceira opção proposta por Timo Ohr, juntamente com os comentários de Yeung, fornecem a resposta mais simples (a navalha de Occam). Abaixo está um exemplo de uma classe completa que funciona para mim. Existem duas regras para usar esta classe.

  1. Estenda esta classe abstrata e implemente os métodos getCursor () e getContentUri ().
  2. Sempre que o banco de dados subjacente mudar (por exemplo, após uma inserção ou exclusão), certifique-se de chamar

    getContentResolver().notifyChange(myUri, null);

    onde myUri é o mesmo retornado de sua implementação do método getContentUri ().

Aqui está o código da aula que usei:

package com.example.project;

import android.content.Context;
import android.database.Cursor;
import android.content.CursorLoader;
import android.content.Loader;

public abstract class AbstractCustomCursorLoader extends CursorLoader
  {
    private final Loader.ForceLoadContentObserver mObserver = new Loader.ForceLoadContentObserver();

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

    @Override
    public Cursor loadInBackground()
      {
        Cursor cursor = getCursor();

        if (cursor != null)
          {
            // Ensure the cursor window is filled
            cursor.getCount();
            cursor.registerContentObserver(mObserver);
          }

        cursor.setNotificationUri(getContext().getContentResolver(), getContentUri());
        return cursor;
      }

    protected abstract Cursor getCursor();
    protected abstract Uri getContentUri();
  }
John Moore
fonte