在原始的SQLite游标上注册ContentObserver()

16
我看到的所有使用registerContentObserver()的例子都是通过ContentProvider接口实现的。但是Cursor也有一个registerContentObserver()调用,因此我想也许Android开发人员已经编写了一些深度魔法,允许在活动结果集中的行更改时获取SQLite游标的更新。不论是我做错了什么,还是没有这样的魔法。以下是我正在使用的代码,期望当我单击按钮更新第1行的值时,我将得到一个onChange()回调。任何想法?
public class MainActivity extends Activity {
    private static final String DATABASE_NAME = "test.db";
    public static final String TAG = "dbtest";

    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

        Button make = (Button)findViewById(R.id.btn_make_record);
        make.setOnClickListener(new OnClickListener() {
            public void onClick(View v) {
                MyOpenHelper oh = new MyOpenHelper(v.getContext());
                SQLiteDatabase wdb = oh.getWritableDatabase();
                ContentValues cv = new ContentValues();
                cv.put("value", String.valueOf(System.currentTimeMillis()));
                if (wdb.insert(MyOpenHelper.TABLE_NAME, null, cv) == -1) {
                    Log.d(TAG, "Unable to insert row");
                } else {
                    Log.d(TAG, "Inserted row ");
                }
                wdb.close();
            }
        });

        Button update = (Button)findViewById(R.id.btn_update_record);
        update.setOnClickListener(new OnClickListener() {   
            public void onClick(View v) {
                MyOpenHelper oh = new MyOpenHelper(v.getContext());
                SQLiteDatabase wdb = oh.getWritableDatabase();
                ContentValues cv = new ContentValues();
                cv.put("value", String.valueOf(System.currentTimeMillis()));
                int count = wdb.update(MyOpenHelper.TABLE_NAME, cv, "_id = ?", new String[] {"1"});
                Log.d(TAG, "Updated " + count + " row(s)");
                wdb.close();
            }
        });

        MyOpenHelper oh = new MyOpenHelper(this);
        SQLiteDatabase rdb = oh.getReadableDatabase();
        Cursor c = rdb.query(MyOpenHelper.TABLE_NAME, null, "_id = ?", new String[] {"1"}, null, null, null);
        startManagingCursor(c);
        contentObserver = new MyContentObserver(new Handler());
        c.registerContentObserver(contentObserver);
    }

    private class MyContentObserver extends ContentObserver {
        MyContentObserver(Handler handler) {
            super(handler);
        }

        public boolean deliverSelfNotifications() {
            return true;
        }

        public void onChange(boolean selfChange) {
            super.onChange(selfChange);
            Log.d(TAG, "Saw a change in row # 1");
        }
    }

    MyContentObserver contentObserver;

    public class MyOpenHelper extends SQLiteOpenHelper {
        private static final int DATABASE_VERSION = 1;
        private static final String TABLE_NAME = "test";
        private static final String TABLE_CREATE =
                "CREATE TABLE " + TABLE_NAME + " (" +
                "_id INTEGER PRIMARY KEY AUTOINCREMENT," +
                "value TEXT);";

        MyOpenHelper(Context context) {
             super(context, DATABASE_NAME, null, DATABASE_VERSION);
        }

        @Override
        public void onCreate(SQLiteDatabase db) {
            db.execSQL(TABLE_CREATE);
        }

        @Override
        public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
          // TODO Auto-generated method stub    
        }
    }
}

嘿,伙计,被接受的答案是错误的,我知道那是你自己的答案,但它不是正确的答案。 - Daniele Segato
2个回答

21

这是完全可能的。

你需要在某个地方定义一个 Uri(可以是常量)。

我建议使用类似于以下格式:

public static final Uri URI_MY_TABLE = 
    Uri.parse("sqlite://com.example.<your-app-package>/table");

然后当你更新数据库数据时,你调用:

context.getContentResolver().notifyChange(Constants.URI_MY_TABLE, null);

从您编写的CursorLoader中,可以执行以下操作:

final ForceLoadContentObserver observer;

public MyCursorLoader(final Context context, ...) {
    super(context);
    // ...
    this.observer = new ForceLoadContentObserver();
}

@Override
public Cursor loadInBackground() {
    SQLiteDatabase db = this.dbHelper.getReadableDatabase();
    final Cursor c = queryDatabase(db);
    if (c != null) {
        // Ensure the cursor window is filled
        c.getCount();
        // this is to force a reload when the content change
        c.registerContentObserver(this.observer);
        // this make sure this loader will be notified when
        // a notifyChange is called on the URI_MY_TABLE
        c.setNotificationUri(getContext().getContentResolver(),
            Constants.URI_MY_TABLE);
    }
    return c;
}

ForceLoadContentObserverLoader类内部的一个公共静态内部类,如果您使用的是支持库,则其为 android.support.v4.content.Loader.ForceLoadContentObserver

在同一个 Loader 中,确保在数据更改时强制重新加载(forceReload)

@Override
protected void onStartLoading() {
    if (this.cursor != null) {
        deliverResult(this.cursor);
    }
    // this takeContentChanged() is important!
    if (takeContentChanged() || this.cursor == null) {
        forceLoad();
    }
}

如果您不知道如何编写CursorLoader,可以从这里入手:使用无ContentProvider的CursorLoader

您的CursorAdapter现在应该像这样初始化:

public MyCursorAdapter(Context context, Cursor c) {
    super(context, c, 0);
}
ContentResolver 会负责通知您设置的 uri 上的观察者。
这正是 ContentProvider 的工作方式。
如果您不使用 Loader,您可以执行相同的操作,重要的修改如下:
- 当更改数据时,请调用 ContentResolver.notifyChange(URI,null); - 当加载游标时,请调用 Cursor.setNotificationUri(ContentResolver, URI); 这样,当您注册观察者时,您将在底层数据发生更改时收到通知。

惊人的解决方案,Daniele!!我能在iOS上做同样的事情吗? - Geet Choubey
我真的怀疑Geet Choubey,Loaders和ContentResolver是Android框架的一部分 :-) - Daniele Segato
有最新的实现吗?因为这个问题是4年前回答的。但是这个实现非常棒,有没有最新的发展? - M.Yogeshwaran
我认为你今天仍然可以使用同样的东西。无论如何,我不再使用加载器,而是转向使用RxJava和其他模式来处理这种情况。 - Daniele Segato

15

没有人接受吗?那么这是一个很好的借口让我自己深入挖掘并弄清楚。对于那些可能会在我之后感到好奇的人,如果您下载平台源代码,则要查看的相关文件为:

frameworks/base/core/java/android/database/sqlite/SQLiteCursor.java frameworks/base/core/java/android/database/AbstractCursor.java

看起来mContentObservable用于向程序的不同部分发出信号,以便从Cursor中的缓存结果集运行,并且可观察对象是来自缓存结果集的信号,以通知使用者何时已倾倒/更新缓存。它不会绑定到SQLite实现以在基础数据存储被操纵时触发事件。

并不奇怪,但考虑到文档,我认为可能我漏掉了什么。


我不明白的是什么让ContentProvider变得特别?如果ContentProvider由SQLiteDatabase支持,那么它的registerContentObserver()如何做到正确呢? - jfritz42
1
它通过内容解析器进行连接。当提供程序在解析器上对URI调用notifyChange时,解析器负责回调所有观察者。 - mikerowehl
@jfritz42 一个ContentProvider负责读取和写入。因此,即使它由SQLite数据库支持,在持久化写操作后,它也可以调用任何监听器。 - Felix
内容提供者的“特殊”之处在于:1)它是唯一一种以受控方式向外部应用程序公开您的应用程序数据(甚至文件)的方法。2)它是由Android管理的单例,您自己无法做到这一点。例如,如果您的应用程序中有多个进程(使用同步适配器),CP将使您确保仅使用单个访问数据库。 Sqlite数据库是一个文件,您不能同时使用两个帮助程序(除非启用写前日志记录)。 - Daniele Segato

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接