Ai mới làm Content Provider (CP) thì sợ nó lắm, làm rồi mới thấy nó chẳng qua giống như công việc viết API cho ứng dụng khác gọi tới xài vậy! Nói nôm na là vậy còn cụ thể như thế nào thì đọc bài này rồi sẽ biết :v

1. ĐỊNH NGHĨA

  • Định nghĩa một cách “hàn lâm” thì là môt Interface kết nối dữ liệu của một tiến trình (app đang chạy) với một tiến trình khác (app khác).
  • Còn định nghĩa thô của tui là nó đơn giản là thằng cò đứng ra rêu rao dữ liệu trên một thằng ứng dụng cho các thằng ứng dụng khác (Nhưng tất nhiên là có chọn lọc xem cái gì được “rao bán” được quyết định ở thằng ứng dụng đầu mối).
Diagram 1: Lược đồ tương tác giữa ContentProvider với DB và các lớp khác.

Lược đồ trên hay nè, để giải thích cho nè:

  • Ở trên là một mô hình hoạt động “tiêu chuẩn” của các bên tham gia vào việc chia sẽ dữ liệu dựa trên lớp (layor) ContentProvider là lớp sẽ cung cấp các phương thức cơ bản CRUD (create, retrieve, update, and delete).
  • Ví dụ để lấy dữ liệu từ một ứng dụng nguồn thì bạn sử dụng ContentResolver.query() ở Activity/Fragment (UI) hiển thị dữ liệu, phương thức query() trên sẽ gọi ContentProvider.query() mà bạn định nghĩa ở lớp cấu hình Provider.
    Tuy nhiên việc truy vấn thực tế cũng mất thời gian (Tùy thuộc vào hàm truy vấn có hiệu quá hay không,…) nên cách tối ưu nhất là sẽ gọi ở các thread (luồng) khác nhau.
  • Giải pháp? hoặc là bạn dùng AsyncTask, hoặc có thể bạn Optimize Query Schema (Là viết câu query nó hợp lý á). Còn nếu không thì hãy dùng CursorLoader để chạy bất đồng (asynchronous) ở background (Thật chất nó cũng gọi qua ContentResolver thôi chỉ là nó gọi ở các thread khác nhau với từng truy vấn khác nhau để chúng chạy bất đồng bộ – không cần đợi nhau).
    Note nhỏ: CursorLoader được quản lý bỏi class LoaderManager và à thi bạn cũng không cần dùng nó nếu bạn chỉ truy vấn một bản 9-10 dòng gì đó.

    Question: Ủa mà chi vậy ??? đợi tí có sao đâu ???
    Vì nó có thể làm treo ứng dụng của bạn luôn á! bạn nhớ cái cửa số xinh xinh ghi là Application is not responding hay màn hình đen huyển kì diệu không? Nó đó!

2. XÂY DỰNG, HIỆN THỰC,… NÓI CHUNG LÀ CODE NÈ

Thôi nói lang mang đủ rồi! quan trọng là phải làm, không làm mà đòi ăn thì chỉ có ăn … :v

Bước đầu tiên là tạo một file để cấu hình, ở đây tui đặt tên là DbContentProvider, mấy bồ có thể đặt tên là SQliteCP hay CloudDbCP,… nói chung là tên nào cũng được nhưng mà nên có có chữ ContentProvider (CP) để phân biệt với các lớp tiện ích khác.

Sau đó hãy extends (kế thừa) lớp ContentProvider và override các phương thức yêu cầu bắc buộc:


public class DbContentProvider extends ContentProvider {
    @Override
    public boolean onCreate() {
        return false;
    }

    @Nullable
    @Override
    public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder) {
        return null;
    }

    @Nullable
    @Override
    public String getType(@NonNull Uri uri) {
        return null;
    }

    @Nullable
    @Override
    public Uri insert(@NonNull Uri uri, @Nullable ContentValues values) {
        return null;
    }

    @Override
    public int delete(@NonNull Uri uri, @Nullable String selection, @Nullable String[] selectionArgs) {
        return 0;
    }

    @Override
    public int update(@NonNull Uri uri, @Nullable ContentValues values, @Nullable String selection, @Nullable String[] selectionArgs) {
        return 0;
    }
}
  • onCreate(): cái tên nói lên tất cả, nó sẽ là phương thức được gọi đầu tiên sau khi hệ thống Android khởi tạo provider.

    Lưu ý: Provider sẽ không được tạo ra nếu như không có đối tượng ContentResolver truy cập ứng dụng của bạn để gọi các phương thức truy vấn sau:
  • query(): Phương thức dùng để truy vấn dữ liệu lên.
  • insert()
  • delete()
  • update()
  • getType(): Phương thức trả về kiểu MIME (“Multipurpose Internet Mail Extensions”) của dữ liệu tại các URI…
    – Hiểu đơn giản là dựa vào URI để xác định kiểu dữ liệu mà ứng dụng truy cập vào lấy lên là loại dữ liệu nào, từ đó xác định được sẽ cần làm gì với nó!
    Ví dụ: Uri là application/pdf thì ứng dụng truy cập sẽ hiểu à đây là file PDF vì vậy mình cần mở trình đọc PDF lên!

2.1 static rồi lại final….

Nói chung là phải định danh một số biến làm hằng số để xài. Ở ví dụ này ta sẽ có hai bảng là PRODUCT (Sản phẩm) và PRODUCER (Nhà sản xuất). Việc tạo hai cái lớp này thì mình sẽ không để vào vì… toàn là constructor và getter setter thui mà nè…lười copy paste vào quá à 🙁

static final String PROVIDER_NAME = "com.example.app"; // authority argm

    static final String PRODUCTS_URL = "content://" + PROVIDER_NAME + "/products";
    static final Uri PRODUCTS_CONTENT_URI = Uri.parse(PRODUCTS_URL);

    static final String PRODUCERS_URL = "content://" + PROVIDER_NAME + "/producers";
    static final Uri PRODUCERS_CONTENT_URI = Uri.parse(PRODUCERS_URL);

    private static final int PRODUCT = 100;
    private static final int PRODUCT_ID = 101;
    private static final int PRODUCER = 200;
    private static final int PRODUCER_ID = 201;

//    private static HashMap<String, String> PRODUCTS_PROJECTION_MAP;
//    private static HashMap<String, String> PRODUCERS_PROJECTION_MAP;

    private static final UriMatcher uriMatcher;

    static {
        uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
        uriMatcher.addURI(PROVIDER_NAME, "products", PRODUCT);
        uriMatcher.addURI(PROVIDER_NAME, "products/#", PRODUCT_ID);
        uriMatcher.addURI(PROVIDER_NAME, "producers", PRODUCER);
        uriMatcher.addURI(PROVIDER_NAME, "producers/#", PRODUCER_ID);
    }


  • PROVIDER_NAME: cái tên package_name của bạn lúc tạo dự án đó, có ai hỏi cái package_name là gì thì các bạn hãy mở điện thoại lên, vào ứng dụng quản lý thư mục > Android > data. Taraaaa, ở đây chính là nơi chứa package_name ứng dụng của bạn.
  • URL: luôn bắt đầu bằng content:// và kết thúc bằng tên bảng dữ liệu lưu trong cơ sở dữ liệu.
  • UriMatcher: là class giúp quản lý khi app bạn có ContentProvider hoặc các class khác cần phản hồi nhiều URI.
  • uriMatcher.addURI(PROVIDER_NAME, “products/#”, PRODUCT_ID);
    #: nếu như id là số.
    *: nếu như id đồng thời là kí tự và số hoặc kí tự thôi.
    PRODUCT_ID: là code để xác định Uri đó để làm gì, số này mình tự đặt (gán số vào biến này ỏ ngay trên đó mấy chế – ở đây là 101 con chó đốm) miễn đừng trùng với các code khác là được.

2.2 onCreate()

Thường thì ở phương thức này sẽ là nơi tạo Database (SQLiteDatabase) hoặc gọi một lớp tách biệt khác, nơi thực hiện công việc tạo Database, còn ở lớp cấu hình Provider này thì chỉ làm những gì liên quan thôi!

@Override
    public boolean onCreate() {
        DatabaseHelper dbHelper = new DatabaseHelper(getContext());
        return true;
    }

2.3 query()

Okay trước khi vào code thì coi qua bảng ghi chú sau đã:

Tham sốQuery Statement
tương ứng
Định nghĩa
UriFROM table_nameLà Uri ánh xạ tương ứng với
table_name trong provider
projectioncol,col,col,…Danh sách các cột trả về
selectionWHERE col = valuenhìn cột kế đủ hiểu rùi hen
selectionArgs– Nếu ở selection là: WHERE col = ?
– Thì ở đây thường sẽ là:
new String[] {String.valueOf(value)}
sortOrderORDER BY col,col,…Qúa dễ hiểu hen :v
Bảng tra cứu ý nghĩa các tham số trong quey( ).
@Nullable
    @Override
    public Cursor query(@NonNull Uri uri,
                        @Nullable String[] projection,
                        @Nullable String selection,
                        @Nullable String[] selectionArgs,
                        @Nullable String sortOrder) {
        final SQLiteDatabase db = dbHelper.getReadableDatabase();
        long _id;
        Cursor cursor;
        switch (uriMatcher.match(uri)) {
            case PRODUCT:
                cursor = db.query("products", projection, selection, selectionArgs, null, null, sortOrder);
                cursor = db.rawQuery("SELECT * FROM products", null);
                break;
            case PRODUCT_ID:
                _id = ContentUris.parseId(uri);

/// Way 1:
//                cursor = db.query("products", 
//                  projection, 
//                 "product_id=?", new String[]{String.valueOf(_id)},
//                  null, 
//                  null,
//                  sortOrder);

/// Way 2:
                cursor = db.rawQuery("SELECT * FROM products WHERE product_id=?", new String[]{String.valueOf(_id)});
                break;

            case PRODUCER:
/// Way 1:
//               cursor = db.query("producers", projection, selection, selectionArgs, null, null, sortOrder);

/// Way 2:
                cursor = db.rawQuery("SELECT * FROM producers", null);
                break;

            case PRODUCER_ID:
                _id = ContentUris.parseId(uri);
/// Way 1:
//                cursor = db.query("producers", projection, "producer_id=?", new String[]{String.valueOf(_id)},
//                        null, null,
//                        sortOrder);

/// Way 2:
                cursor = db.rawQuery("SELECT * FROM producers WHERE author_id=?", new String[]{String.valueOf(_id)});
                break;
            default:
                throw new UnsupportedOperationException("Unkown uri: " + uri);
        }
//        db.close();

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

Okay sẽ có hai cách để ghi câu query:

  • Cách tường minh (db.rawQuery()): Ghi chay luôn câu query của bạn, cá nhân mình thì thích cách này hơn vì nó rõ ràng và…mình quen rồi
  • Cách…ít tường minh hơn (db.query()): dùng cái bảng giải thích nhỏ nhỏ trên và gọi hàm truyền tham số vào :v

cursor.setNotificationUri(getContext().getContentResolver(), uri): notify khi nào Uri thay đổi để gọi hàm ContentResolver thuộc Activity hay Fragment gọi hàm query() này.

2.4 insert()

Và đây là hàm insert

@Nullable
    @Override
    public Uri insert(@NonNull Uri uri, 
                      @Nullable ContentValues values) {
        final SQLiteDatabase db = dbHelper.getWritableDatabase();
        long rowId;
        Uri returnUri;
        switch (uriMatcher.match(uri)) {
            case PRODUCT:
                rowId = db.insert("products", null, values);
                if (rowId > 0) {
                    returnUri = ContentUris.withAppendedId(PRODUCTS_CONTENT_URI, rowId);
                } else {
                    throw new UnsupportedOperationException("Unable to insert rows into: " + uri);
                }
                break;
            case PRODUCER:
                rowId = db.insert("producers", null, values);
                if (rowId > 0) {
                    returnUri = ContentUris.withAppendedId(PRODUCERS_CONTENT_URI, rowId);
                } else {
                    throw new UnsupportedOperationException("Unable to insert rows into: " + uri);
                }
                break;
            default:
                throw new UnsupportedOperationException("Unknown uri: " + uri);
        }
        ///java.lang.IllegalStateException: Cannot perform this operation because the connection pool has been closed.
//        db.close();
        /// Use this on the URI passed into the function to notify any observers that the uri has changed
        getContext().getContentResolver().notifyChange(uri, null);
        return returnUri;

2.5 update()

@Override
    public int update(@NonNull Uri uri, @Nullable ContentValues values, @Nullable String selection, @Nullable String[] selectionArgs) {
        final SQLiteDatabase db = dbHelper.getWritableDatabase();
        int rows;
        switch (uriMatcher.match(uri)) {
            case PRODUCT:
                rows = db.update("products", values, selection, selectionArgs);
                break;
            case PRODUCER:
                rows = db.update("producers", values, selection, selectionArgs);
                break;
            default:
                throw new UnsupportedOperationException("Unknown uri: " + uri);

        }
        if (rows != 0) {
            getContext().getContentResolver().notifyChange(uri, null);
        }
        return rows;
    }

2.6 delete()

 @Override
    public int delete(@NonNull Uri uri, @Nullable String selection, @Nullable String[] selectionArgs) {
        final SQLiteDatabase db = dbHelper.getWritableDatabase();
        int rows;
        switch (uriMatcher.match(uri)) {
            case PRODUCT:
                rows = db.delete("products", selection, selectionArgs);
                break;
            case PRODUCER:
                rows = db.delete("producers", selection, selectionArgs);
                break;
            default:
                throw new UnsupportedOperationException("Unknown uri: " + uri);
        }
        //Because null could delete all rows
        if (selection == null || rows != 0) {
            getContext().getContentResolver().notifyChange(uri, null);
        }
        return rows;
    }

3. AndroidManifest.xml

Ở đây khai báo mấy cái quan trọng nè:

<provider
            android:name=".DbContentProvider"
            android:authorities="com.example.app"
            android:enabled="true"
            android:exported="true" />
<permission
        android:name="com.example.app.WRITE_DATABASE"
        android:label="Write Database"
        android:protectionLevel="normal" />
<permission
        android:name="com.example.app.READ_DATABASE"
        android:label="Read Database"
        android:protectionLevel="normal" />

4. Tạm Kết

Okay xong rùi đó bây giờ lấy lên sử dụng, cách lấy thì, coi clip nhen ahihi vì bài này nó dài quá rồi :((

[Carry by: TungTTr –  Date: 15/09/2020]
[Update: 16/09/2020 ]