Użycie CursorLoader bez ContentProvider

107

Dokumentacja Android SDK mówi, że startManagingCursor()metoda jest pozbawiona wartości:

Ta metoda jest przestarzała. Zamiast tego użyj nowej klasy CursorLoader z LoaderManager; jest to również dostępne na starszych platformach za pośrednictwem pakietu zgodności z systemem Android. Ta metoda pozwala aktywności zająć się zarządzaniem cyklem życia danego kursora w oparciu o cykl życia działania. Oznacza to, że gdy aktywność zostanie zatrzymana, automatycznie wywoła dezaktywację () na danym kursorze, a po późniejszym ponownym uruchomieniu wywoła dla Ciebie requery (). Gdy aktywność zostanie zniszczona, wszystkie zarządzane kursory zostaną automatycznie zamknięte. Jeśli celujesz w HONEYCOMB lub nowszy, zamiast tego rozważ użycie LoaderManager, dostępnego przez getLoaderManager ()

Więc chciałbym użyć CursorLoader. Ale jak mogę go używać z niestandardowym CursorAdapteri bez ContentProvider, kiedy potrzebuję URI w konstruktorze CursorLoader?

sealskej
źródło
@Alex Lockwood, dlaczego używamy CursorAdapter bez ContentProvider, zasugeruj mi stackoverflow.com/questions/20419278/ ...
dlaczego używamy CursorAdapter bez ContentProvider, zasugeruj mi stackoverflow.com/questions/20419278/ ...

Odpowiedzi:

155

Napisałem prosty CursorLoader , który nie potrzebuje dostawcy treści:

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

Potrzebuje tylko AsyncTaskLoader klasy. Albo ten w systemie Android 3.0 lub nowszym, albo ten, który jest dostarczany z pakietem zgodności.

Ja też napisałemListLoader , która jest zgodna z LoadManageri jest używany do pobierania rodzajowe java.util.Listkolekcji.

Cristian
źródło
13
Znalazłem fajny przykład kodu, który to wykorzystuje - bitbucket.org/ssutee/418496_mobileapp/src/fc5ee705a2fd/demo/ ... - uznał to za bardzo przydatne!
Shushu
@Cristian Thanks za przykład. Jaka jest licencja związana z twoją klasą. Jak można go ponownie wykorzystać?
codinguser
2
Licencja to Apache 2.0; możesz go ponownie użyć, gdzie / kiedy chcesz. Daj mi znać, jeśli masz jakieś ulepszenia.
Cristian
14
Świetne rzeczy! Użytkownicy powinni być świadomi jednego ograniczenia, którym jest brak mechanizmu odświeżania po zmianach danych (tak jak powinny to robić
programy ładujące
1
@Jadeye tutaj masz człowiek: ListLoader i SupportListLoader
Cristian
23

Napisz własny moduł ładujący, który używa Twojej klasy bazy danych zamiast dostawcy treści. Najprostszym sposobem jest po prostu pobranie źródła CursorLoaderklasy z biblioteki zgodności i zastąpienie zapytań dostawców zapytaniami do własnej klasy pomocniczej db.

Nikolay Elenkov
źródło
1
To moim zdaniem najłatwiejszy sposób. W mojej aplikacji utworzyłem CursorLoaderdescendat do zarządzania kursorem SQLite, poza konstruktorem potrzebowałem tylko przesłonić loadInBackgroundmetodę, aby zastąpić zapytanie dostawcy moim zapytaniem kursora
Jose_GD
14

SimpleCursorLoader jest prostym rozwiązaniem, jednak nie obsługuje aktualizowania modułu ładującego w przypadku zmiany danych. CommonsWare ma bibliotekę loaderex, która dodaje SQLiteCursorLoader i obsługuje ponowne zapytania dotyczące zmian danych.

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

emmby
źródło
2
Jednak aby skorzystać z automatycznego ponownego wysyłania zapytań, musisz użyć tego samego modułu ładującego dla interfejsu użytkownika, a także dla aktualizacji, co ogranicza jego użyteczność dla usług działających w tle.
ge0rg
12

Trzecią opcją byłoby po prostu zastąpienie 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;
    }
};

Spowoduje to również ponowne wysłanie zapytania do kursora po zmianie bazy danych.

Jedyne zastrzeżenie: musisz zdefiniować innego obserwatora, ponieważ Google w swojej nieskończonej mądrości zdecydował, że ich pakiet będzie prywatny. Jeśli umieścisz klasę w tym samym pakiecie, co oryginalna (lub zgodna), możesz faktycznie użyć oryginalnego obserwatora. Obserwator jest bardzo lekkim obiektem i nie jest używany nigdzie indziej, więc nie robi to dużej różnicy.

Timo Ohr
źródło
Z moich obserwacji w szybkich testach wynika, że ​​registerContentObserver będzie wywoływany w stosunku do kursora tylko wtedy, gdy kursor jest skierowany do dostawcy treści. Czy możesz to potwierdzić / zaprzeczyć?
Nick Campion,
1
Nie musi to być dostawca treści. Ale kursor musi być zarejestrowany w uri powiadomienia (setNotificationUri), a następnie musi zostać powiadomiony przez kogoś (zwykle ContentProvider, ale może to być cokolwiek) przez wywołanie ContentResolver.notifyChange.
Timo Ohr,
4
Tak. na twoim CustomLoader loadInBackground() , przed zwróceniem kursora, powiedz, że cursor.setNotificationUri(getContext().getContentResolver(), uri);uri może pochodzić z losowego ciągu, takiego jak Uri.parse("content://query_slot1"). Wygląda na to, że nie obchodzi go, czy uri naprawdę istnieje, czy nie. A kiedy zrobiłem operację na DB. Say getContentResolver().notifyChange(uri, null);załatwi sprawę. Następnie mogę utworzyć kilka "slotów zapytań uri" w stałym pliku dla aplikacji z niewielką liczbą zapytań. Testuję wstawianie rekordu DB w czasie wykonywania i wydaje się, że działa, ale nadal wątpię, czy jest to dobra praktyka onit. Jakieś sugestie?
Yeung,
Używam tej metody z sugestią @Yeung i wszystko działa, łącznie z automatycznym przeładowywaniem kursora przy aktualizacji bazy danych.
DavidH
czy nie potrzebuje wyrejestrowania się z treścią?
GPack
2

Trzecia opcja zaproponowana przez Timo Ochra, wraz z komentarzami Yeunga, dają najprostszą odpowiedź (brzytwa Ockhama). Poniżej znajduje się przykład kompletnej klasy, która działa dla mnie. Istnieją dwie zasady korzystania z tej klasy.

  1. Rozszerz tę klasę abstrakcyjną i zaimplementuj metody getCursor () i getContentUri ().
  2. Za każdym razem, gdy podstawowa baza danych ulegnie zmianie (np. Po wstawieniu lub usunięciu), należy wywołać

    getContentResolver().notifyChange(myUri, null);

    gdzie myUri to ta sama wartość, która została zwrócona przez implementację metody getContentUri ().

Oto kod klasy, której użyłem:

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
źródło