2013年9月7日 星期六

單元二: 手機的資料儲存 --- SQLite + ContentProvider + Loader

上一篇, 說明了 SQLite 直接調用的方法,

我們在這一篇裡, 想再說明另一種調用方式: SQLite + ContentProvider + Loader.
這種方式是透過 Loader 去調用 ContentProvider,
ContentProvider 再呼叫 SQLite 執行增刪改查的動作.


使用 Loader 有兩個好處,
一. 會異步更新資料 (Asynchronous Loading Data), 也就是不會佔到 Main Thread 的資源,
二. 可以監視 Data 的改變, 當 Data 改變時會通知 Main Thread.

在 Android Developer 的文件裡, 是建議我們使用 Loader 的.

SQLite + ContentProvider + Loader 的使用

使用上我們一樣分成幾個步驟:
1. 寫 ProductTable, 在這個 class 裡存放我們的資料檔名稱, 以及 provider 需要的路徑
2. 寫 ProductDBHelper ( 繼承自 SQLiteOpenHelper )
3. 寫 ProductProvider ( 繼承自 ContentProvider )
4. 在主程式裡調用
5. 在 AndroidManifest.xml 裡註冊 provider

我們的目標一樣是做上一篇的那張表, 並顯示在  ListView 上.

1. 寫 ProductTable

這個 ProductTable 繼承自 BaseColumns,
BaseColumns 包含了 _ID, _COUNT 兩個常數,
所以我們在設置常數的時候, 可以直接調用.

這裡我們要設置檔案的 Table 名稱, Table Column 的名稱,
以及 Provider 需要的 uri 路徑.

ProdcutTable.class
 public final class ProductTable implements BaseColumns {  
   // This class cannot be instantiated  
   private ProductTable() {}  
   /**  
    * The table name offered by this provider  
    */  
   public static final String TABLE_NAME = "product";  
   /**  
    * The content:// style URL for this table  
    */  
   public static final Uri CONTENT_URI = Uri.parse("content://" + MainActivity.AUTHORITY + "/"+TABLE_NAME);  
   /**  
    * The content URI base for a single row of data. Callers must  
    * append a numeric row id to this Uri to retrieve a row  
    */  
   public static final Uri CONTENT_ID_URI_BASE  
       = Uri.parse("content://" + MainActivity.AUTHORITY + "/"+TABLE_NAME+"/");  
   /**  
    * The MIME type of {@link #CONTENT_URI}.  
    */  
   public static final String CONTENT_TYPE  
       = "vnd.android.cursor.dir/vnd.example.api-demos-throttle";  
   /**  
    * The MIME type of a {@link #CONTENT_URI} sub-directory of a single row.  
    */  
   public static final String CONTENT_ITEM_TYPE  
       = "vnd.android.cursor.item/vnd.example.api-demos-throttle";  
   /**  
    * The default sort order for this table  
    */  
   public static final String DEFAULT_SORT_ORDER = _ID+" COLLATE LOCALIZED ASC";  
   /**  
    * Column name for the single column holding our data.  
    * <P>Type: TEXT</P>  
    */  
   public static final String COLUMN_NAME_DATA = "product_name";  
 }  

2. 寫 ProductDBHelper

ProductDBHelper 繼承自 SQLiteOpenHelper, 用來執行 SQLite 的指令.

ProductDBHelper.class
 public class ProductDBHelper extends SQLiteOpenHelper {  
   private static final String DATABASE_NAME = "productDB2.db";  
   private static final int DATABASE_VERSION = 2;  
   ProductDBHelper(Context context) {  
     // calls the super constructor, requesting the default cursor factory.  
     super(context, DATABASE_NAME, null, DATABASE_VERSION);  
   }  
   @Override  
   public void onCreate(SQLiteDatabase db) {  
     db.execSQL("CREATE TABLE " + ProductTable.TABLE_NAME + " ("  
         + ProductTable._ID + " INTEGER PRIMARY KEY,"  
         + ProductTable.COLUMN_NAME_DATA + " TEXT"  
         + ");");  
   }  
   @Override  
   public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {  
     // Kills the table and existing data  
     db.execSQL("DROP TABLE IF EXISTS notes");  
     // Recreates the database with a new version  
     onCreate(db);  
   }  
 }  

3. 寫 ProductProvider

我們透過 ProductProvider 來調用 ProductDBHelper,
這樣之後在主程式裡使用的時候, 就只需要管理 ProductProvider 這個接口就好.

ProductProvider.class
 public class ProductProvider extends ContentProvider {  
   // A projection map used to select columns from the database  
   private final HashMap<String, String> mNotesProjectionMap;  
   // Uri matcher to decode incoming URIs.  
   private final UriMatcher mUriMatcher;  
   // The incoming URI matches the main table URI pattern  
   private static final int MAIN = 1;  
   // The incoming URI matches the main table row ID URI pattern  
   private static final int MAIN_ID = 2;  
   // Handle to a new DatabaseHelper.  
   private ProductDBHelper mOpenHelper;  
   /**  
    * Global provider initialization.  
    */  
   public ProductProvider() {  
     // Create and initialize URI matcher.  
     mUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);  
     mUriMatcher.addURI(MainActivity.AUTHORITY, ProductTable.TABLE_NAME, MAIN);  
     mUriMatcher.addURI(MainActivity.AUTHORITY, ProductTable.TABLE_NAME + "/#", MAIN_ID);  
     // Create and initialize projection map for all columns. This is  
     // simply an identity mapping.  
     mNotesProjectionMap = new HashMap<String, String>();  
     mNotesProjectionMap.put(ProductTable._ID, ProductTable._ID);  
     mNotesProjectionMap.put(ProductTable.COLUMN_NAME_DATA, ProductTable.COLUMN_NAME_DATA);  
   }  
   /**  
    * Perform provider creation.  
    */  
   @Override  
   public boolean onCreate() {  
     mOpenHelper = new ProductDBHelper(getContext());  
     // Assumes that any failures will be reported by a thrown exception.  
     return true;  
   }  
   /**  
    * Handle incoming queries.  
    */  
   @Override  
   public Cursor query(Uri uri, String[] projection, String selection,  
       String[] selectionArgs, String sortOrder) {  
     // Constructs a new query builder and sets its table name  
     SQLiteQueryBuilder qb = new SQLiteQueryBuilder();  
     qb.setTables(ProductTable.TABLE_NAME);  
     switch (mUriMatcher.match(uri)) {  
       case MAIN:  
         // If the incoming URI is for main table.  
         qb.setProjectionMap(mNotesProjectionMap);  
         break;  
       case MAIN_ID:  
         // The incoming URI is for a single row.  
         qb.setProjectionMap(mNotesProjectionMap);  
         qb.appendWhere(ProductTable._ID + "=?");  
         selectionArgs = DatabaseUtilsCompat.appendSelectionArgs(selectionArgs,  
             new String[] { uri.getLastPathSegment() });  
         break;  
       default:  
         throw new IllegalArgumentException("Unknown URI " + uri);  
     }  
     if (TextUtils.isEmpty(sortOrder)) {  
       sortOrder = ProductTable.DEFAULT_SORT_ORDER;  
     }  
     SQLiteDatabase db = mOpenHelper.getReadableDatabase();  
     Cursor c = qb.query(db, projection, selection, selectionArgs,  
         null /* no group */, null /* no filter */, sortOrder);  
     c.setNotificationUri(getContext().getContentResolver(), uri);  
     return c;  
   }  
   /**  
    * Return the MIME type for an known URI in the provider.  
    */  
   @Override  
   public String getType(Uri uri) {  
     switch (mUriMatcher.match(uri)) {  
       case MAIN:  
         return ProductTable.CONTENT_TYPE;  
       case MAIN_ID:  
         return ProductTable.CONTENT_ITEM_TYPE;  
       default:  
         throw new IllegalArgumentException("Unknown URI " + uri);  
     }  
   }  
   /**  
    * Handler inserting new data.  
    */  
   @Override  
   public Uri insert(Uri uri, ContentValues initialValues) {  
     if (mUriMatcher.match(uri) != MAIN) {  
       // Can only insert into to main URI.  
       throw new IllegalArgumentException("Unknown URI " + uri);  
     }  
     ContentValues values;  
     if (initialValues != null) {  
       values = new ContentValues(initialValues);  
     } else {  
       values = new ContentValues();  
     }  
     // 如果傳進來的 initialValues 是 null, 讓資料為 ""  
     if (values.containsKey(ProductTable.COLUMN_NAME_DATA) == false) {  
       values.put(ProductTable.COLUMN_NAME_DATA, "");  
     }  
     SQLiteDatabase db = mOpenHelper.getWritableDatabase();  
     long rowId = db.insert(ProductTable.TABLE_NAME, null, values);  
     // If the insert succeeded, the row ID exists.  
     if (rowId > 0) {  
       Uri noteUri = ContentUris.withAppendedId(ProductTable.CONTENT_ID_URI_BASE, rowId);  
       getContext().getContentResolver().notifyChange(noteUri, null);  
       return noteUri;  
     }  
     throw new SQLException("Failed to insert row into " + uri);  
   }  
   /**  
    * Handle deleting data.  
    */  
   @Override  
   public int delete(Uri uri, String where, String[] whereArgs) {  
     SQLiteDatabase db = mOpenHelper.getWritableDatabase();  
     String finalWhere;  
     int count;  
     switch (mUriMatcher.match(uri)) {  
       case MAIN:  
         // If URI is main table, delete uses incoming where clause and args.  
         count = db.delete(ProductTable.TABLE_NAME, where, whereArgs);  
         break;  
         // If the incoming URI matches a single note ID, does the delete based on the  
         // incoming data, but modifies the where clause to restrict it to the  
         // particular note ID.  
       case MAIN_ID:  
         // If URI is for a particular row ID, delete is based on incoming  
         // data but modified to restrict to the given ID.  
         finalWhere = DatabaseUtilsCompat.concatenateWhere(  
                   ProductTable._ID + " = " + ContentUris.parseId(uri), where);  
         count = db.delete(ProductTable.TABLE_NAME, finalWhere, whereArgs);  
         break;  
       default:  
         throw new IllegalArgumentException("Unknown URI " + uri);  
     }  
     getContext().getContentResolver().notifyChange(uri, null);  
     return count;  
   }  
   /**  
    * Handle updating data.  
    */  
   @Override  
   public int update(Uri uri, ContentValues values, String where, String[] whereArgs) {  
     SQLiteDatabase db = mOpenHelper.getWritableDatabase();  
     int count;  
     String finalWhere;  
     switch (mUriMatcher.match(uri)) {  
       case MAIN:  
         // If URI is main table, update uses incoming where clause and args.  
         count = db.update(ProductTable.TABLE_NAME, values, where, whereArgs);  
         break;  
       case MAIN_ID:  
         // If URI is for a particular row ID, update is based on incoming  
         // data but modified to restrict to the given ID.  
         finalWhere = DatabaseUtilsCompat.concatenateWhere(  
                   ProductTable._ID + " = " + ContentUris.parseId(uri), where);  
         count = db.update(ProductTable.TABLE_NAME, values, finalWhere, whereArgs);  
         break;  
       default:  
         throw new IllegalArgumentException("Unknown URI " + uri);  
     }  
     getContext().getContentResolver().notifyChange(uri, null);  
     return count;  
   }  
 }  

這裡用了一個技巧是使用 UriMatcher 透過路徑來分辨是要對 table 或者是 row 進行刪,查的動作, 也是挺實用的.

4. 在主程式裡調用

在 MainActivity 要使用 LoaderManager 必須繼承自 FragmentActivity,
並且 implements LoaderManager.LoaderCallbacks<Cursor>,
這樣就能有 Loader  的回呼函數: onCreateLoader(), onLoadFinished(), onLoaderReset().

實作上, 我們取得 ContentResolver (可以用來執行 ContentProvider 裡的方法),
接著存入幾筆資料, 再利用 Loader 讀回來.

MainActivity.class
 public class MainActivity extends FragmentActivity implements LoaderManager.LoaderCallbacks<Cursor>{  
      /**  
    * The authority we use to get to our sample provider.  
    */  
   public static final String AUTHORITY = "com.example.learnsqliteusingloader";  
   private ListView mainList;  
  // This is the Adapter being used to display the list's data.  
   private SimpleCursorAdapter mAdapter;  
      @SuppressLint("NewApi")  
      @Override  
      protected void onCreate(Bundle savedInstanceState) {  
           super.onCreate(savedInstanceState);  
           setContentView(R.layout.activity_main);  
           mainList = (ListView) findViewById (R.id.main_list);  
           // Create an empty adapter we will use to display the loaded data.  
     mAdapter = new SimpleCursorAdapter(this,  
         R.layout.item_list, null,  
         new String[] { ProductTable.COLUMN_NAME_DATA },  
         new int[] { R.id.text_list }, 0);  
     mainList.setAdapter(mAdapter);  
           // 寫入資料  
     ContentResolver cr = getContentResolver();  
     for (int i=0;i<11;i++){  
          ContentValues values = new ContentValues();  
          values.put(ProductTable.COLUMN_NAME_DATA, "cup_"+ Integer.toString(i));  
          cr.insert(ProductTable.CONTENT_URI, values);  
     }  
     // Prepare the loader. Either re-connect with an existing one,  
     // or start a new one.  
     getSupportLoaderManager().initLoader(0, null, this);  
      }  
      @Override  
      public boolean onCreateOptionsMenu(Menu menu) {  
           // Inflate the menu; this adds items to the action bar if it is present.  
           getMenuInflater().inflate(R.menu.main, menu);  
           return true;  
      }  
      static final String[] PROJECTION = new String[] {  
           ProductTable._ID,  
           ProductTable.COLUMN_NAME_DATA,  
   };  
      @Override  
      public Loader<Cursor> onCreateLoader(int arg0, Bundle arg1) {  
           CursorLoader cl = new CursorLoader(this, ProductTable.CONTENT_URI,  
         PROJECTION, null, null, null);  
     return cl;  
      }  
      @Override  
      public void onLoadFinished(Loader<Cursor> loader, Cursor data) {  
           mAdapter.swapCursor(data);  
      }  
      @Override  
      public void onLoaderReset(Loader<Cursor> loader) {  
           mAdapter.swapCursor(null);  
      }  
 }  

5. 在 AndroidManifest.xml 裡註冊 provider

最後要在 AndroidManifest.xml 裡註冊 provider,
特別要注意 authority 的路徑要跟在 MainActivity.class 設的 AUTHORITY 一樣.

因為我們在 ProductTable.class 裡設的許多路徑, 也是跟著這個 AUTHORITY 設的.
必須要是這個路徑才能調用我們的 ProductProvider

AndroidManifest.xml
 <provider android:name=".ProductProvider"  
          android:authorities="com.example.learnsqliteusingloader" />  


範例圖: (原始碼連結)






沒有留言:

張貼留言