Android記事本示例剖析之三中講了Activity的生命周期,並通過實驗的方式研究了Activity的狀態轉換機制,之後又介紹了自定義控件。本文繼續Android記事本示例的分析,主要講解NoteEditor類和Content Provider機制。
深入剖析NoteEditor類
首先來弄清楚“日志編輯“的狀態轉換,通過Android記事本示例剖析之三中的方法來做下面這樣一個實驗,首先進入“日志編輯”時會觸發onCreate和onResume,然後用戶通過Option Menu選擇“Edit title”後,會觸發onSaveInstanceState和onPause,最後,用戶回到編輯界面,則再次觸發onResume。
最終通過LogCat可以得到下圖:
那麼下面就按照上述順序對此類進行剖析。首先是onCreate方法,一開始先獲取導致進入“日志編輯”界面的intent,分析其操作類型可得知是“編輯日志”還是“新增日撒志”。
Java代碼
- final Intent intent = getIntent();
- // Do some setup based on the action being performed.
- final String action = intent.getAction();
若是“編輯日志”,則設置當前狀態為“編輯”,並保存待編輯日志的URI。
Java代碼
- mState = STATE_EDIT;
- mUri = intent.getData();
若是“新增日志”,則設置當前狀態為“新增”,並通過content provider向數據庫中新增一個“空白日志”,後者返回“空白日志”的URI。
Java代碼
- mState = STATE_INSERT;
- mUri = getContentResolver().insert(intent.getData(), null);
然後不管是“編輯”或“新增”,都需要從數據庫中讀取日志信息(當然,若是“新增”,讀出來的肯定是空數據)。
Java代碼
- mCursor = managedQuery(mUri, PROJECTION, null, null, null);
最後,類似於web應用中使用的Session,這裡也將日志文本保存在InstanceState中,因此,若此activity的實例此前是處於stop狀態,則我們可以從它那取出它原本的文本數據。
Java代碼
- if (savedInstanceState != null)
- {
- mOriginalContent = savedInstanceState.getString(ORIGINAL_CONTENT);
- }
第二個來分析onResume函數,首先把游標置於第一行(也只有一行)。
Java代碼
- mCursor.moveToFirst();
然後取出“正文”字段,這時有一個比較有趣的技巧,“設置文本”並不是調用setText,而是調用的setTextKeepState,後者相對於前者有一個優點,就是當界面此前stop掉,現在重新resume回來,那麼此前光標所在位置仍然得以保存。而若使用setText,則光標會重置到行首。
Java代碼
- String note = mCursor.getString(COLUMN_INDEX_NOTE);
- mText.setTextKeepState(note);
最後,將當前編輯的正文保存到一個字符串變量中,用於當activity被暫停時使用。
Java代碼
- if (mOriginalContent == null)
- {
- mOriginalContent = note;
- }
通過前面的圖可以得知,activity被暫停時,首先調用的是onSaveInstanceState函數。
Java代碼
- outState.putString(ORIGINAL_CONTENT, mOriginalContent);
這裡就僅僅將當前正編輯的正文保存到InstanceState中(類似於Session)。最後來看onPause函數,這裡首先要考慮的是若activity正要關閉,並且編輯區沒有正文,則將此日志刪除。
Java代碼
- if (isFinishing() && (length == 0) && !mNoteOnly)
- {
- setResult(RESULT_CANCELED);
- deleteNote();
- }
否則的話,就更新日志信息。
Java代碼
- ContentValues values = new ContentValues();
- if (!mNoteOnly)
- {
- values.put(Notes.MODIFIED_DATE, System.currentTimeMillis());
- if (mState == STATE_INSERT)
- {
- String title = text.substring(0, Math.min(30, length));
- if (length > 30)
- {
- int lastSpace = title.lastIndexOf(' ');
- if (lastSpace > 0)
- {
- title = title.substring(0, lastSpace);
- }
- }
- values.put(Notes.TITLE, title);
- }
- }
- values.put(Notes.NOTE, text);
- getContentResolver().update(mUri, values, null, null);
- }
在生成Option Menu的函數onCreateOptionsMenu中,我們再一次看到下面這段熟悉的代碼了:
Java代碼
- Intent intent = new Intent(null, getIntent().getData());
- intent.addCategory(Intent.CATEGORY_ALTERNATIVE);
- menu.addIntentOptions(Menu.CATEGORY_ALTERNATIVE, 0, 0,
- new ComponentName(this, NoteEditor.class), null, intent, 0, null);
這種生成動態菜單的機制在Android示例程序剖析之記事本(二:Android菜單)這篇文章中已經介紹過了,就不贅述了。最後,來看下放棄日志和刪除日志的實現,由於還沒有接觸到底層的content provider,這裡都是通過getContentResolver()提供的update,delete,insert來向底層的content provider發出請求,由後者完成實際的數據庫操作。
Java代碼
- private final void cancelNote()
- {
- if (mCursor != null)
- {
- if (mState == STATE_EDIT)
- {
- // Put the original note text back into the database
- mCursor.close();
- mCursor = null;
- ContentValues values = new ContentValues();
- values.put(Notes.NOTE, mOriginalContent);
- getContentResolver().update(mUri, values, null, null);
- }
- else if (mState == STATE_INSERT)
- {
- // We inserted an empty note, make sure to delete it
- deleteNote();
- }
- }
- setResult(RESULT_CANCELED);
- finish();
- }
- private final void deleteNote()
- {
- if (mCursor != null)
- {
- mCursor.close();
- mCursor = null;
- getContentResolver().delete(mUri, null, null);
- mText.setText("");
- }
- }
NotePadProvider類分析
NotePadProvider就是所謂的content provider,它繼承自android.content.ContentProvider,也是負責數據庫層的核心類,主要提供五個功能:
1)查詢數據
2)修改數據
3)添加數據
4)刪除數據
5)返回數據類型
這五個功能分別對應下述五個可以重載的方法:
Java代碼
- public int delete(Uri uri, String selection, String[] selectionArgs)
- {
- return 0;
- }
- public String getType(Uri uri)
- {
- return null;
- }
- public Uri insert(Uri uri, ContentValues values)
- {
- return null;
- }
- public boolean onCreate()
- {
- return false;
- }
- public Cursor query(Uri uri, String[] projection, String selection,
- String[] selectionArgs, String sortOrder)
- {
- return null;
- }
- public int update(Uri uri, ContentValues values, String selection,
- String[] selectionArgs)
- {
- return 0;
- }
這些都要你自己實現,不同的實現就是對應不同的content-provider。但是activity使用content-provider不是直接創建一個對象,然後調用這些具體方法。
而是調用managedQuery,getContentResolver().delete,update等來實現,這些函數其實是先找到符合條件的content-provider,然後再調用具體content-provider的函數來實現,那又是怎麼找到content-provider,就是通過uri中的authority來找到content-provider,這些都是通過系統完成,應用程序不用操心,這樣就達到了有效地隔離應用和內容提供者的具體實現的目的。
有了以上初步知識後,我們來看NotePadProvider是如何為上層提供數據庫層支持的。下面這三個字段指明了數據庫名稱,數據庫版本,數據表名稱。
Java代碼
- private static final String DATABASE_NAME = "note_pad.db";
- private static final int DATABASE_VERSION = 2;
- private static final String NOTES_TABLE_NAME = "notes";
實際的數據庫操作其實都是通過一個私有靜態類DatabaseHelper實現的,其構造函數負責創建指定名稱和版本的數據庫,onCreate函數則創建指定名稱和各個數據域的數據表(就是簡單的建表SQL語句)。onUpgrade負責刪除數據表,再重新建表。
Java代碼
- private static class DatabaseHelper extends SQLiteOpenHelper
- {
- DatabaseHelper(Context context)
- {
- super(context, DATABASE_NAME, null, DATABASE_VERSION);
- }
- @Override
- public void onCreate(SQLiteDatabase db)
- {
- db.execSQL("CREATE TABLE " + NOTES_TABLE_NAME + " ("
- + Notes._ID + " INTEGER PRIMARY KEY,"
- + Notes.TITLE + " TEXT,"
- + Notes.NOTE + " TEXT,"
- + Notes.CREATED_DATE + " INTEGER,"
- + Notes.MODIFIED_DATE + " INTEGER"
- + ");");
- }
- @Override
- public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion)
- {
- Log.w(TAG, "Upgrading database from version " + oldVersion + " to "
- + newVersion + ", which will destroy all old data");
- db.execSQL("DROP TABLE IF EXISTS notes");
- onCreate(db);
- }
- }
在Android示例程序剖析之記事本(一)這篇文章中我們已經見識到了getType函數的用處了,也正是通過它的解析,才能區分開到底是對全部日志還是對某一條日志進行操作。
Java代碼
- public String getType(Uri uri)
- {
- switch (sUriMatcher.match(uri))
- {
- case NOTES:
- return Notes.CONTENT_TYPE;
- case NOTE_ID:
- return Notes.CONTENT_ITEM_TYPE;
- default:
- throw new IllegalArgumentException("Unknown URI " + uri);
- }
- }
上面的sUriMatcher.match是用來檢測uri是否能夠被處理,而sUriMatcher.match(uri)返回值其實是由下述語句決定的。
Java代碼
- sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
- sUriMatcher.addURI(NotePad.AUTHORITY, "notes", NOTES);
- sUriMatcher.addURI(NotePad.AUTHORITY, "notes/#", NOTE_ID);
sNotesProjectionMap這個私有字段是用來在上層應用使用的字段和底層數據庫字段之間建立映射關系的,當然,這個程序裡兩處對應的字段都是一樣(但並不需要一樣)。
Java代碼
- private static HashMap<String, String> sNotesProjectionMap;
- static
- {
- sNotesProjectionMap = new HashMap<String, String>();
- sNotesProjectionMap.put(Notes._ID, Notes._ID);
- sNotesProjectionMap.put(Notes.TITLE, Notes.TITLE);
- sNotesProjectionMap.put(Notes.NOTE, Notes.NOTE);
- sNotesProjectionMap.put(Notes.CREATED_DATE, Notes.CREATED_DATE);
- sNotesProjectionMap.put(Notes.MODIFIED_DATE, Notes.MODIFIED_DATE);
- }
數據庫的增,刪,改,查操作基本都一樣,具體可以參考官方文檔,這裡就僅僅以刪除為例進行說明。一般可以分為三步來完成,首先打開數據庫。
Java代碼
- SQLiteDatabase db = mOpenHelper.getWritableDatabase();
然後根據URI指向的是日志列表還是某一篇日志,到數據庫中執行刪除動作。
Java代碼
- switch (sUriMatcher.match(uri)) {
- case NOTES:
- count = db.delete(NOTES_TABLE_NAME, where, whereArgs);
- break;
- case NOTE_ID:
- String noteId = uri.getPathSegments().get(1);
- count = db.delete(NOTES_TABLE_NAME, Notes._ID + "=" + noteId + (!TextUtils.isEmpty(where) ? " AND (" + where + ')' : ""), whereArgs);
- break;
- }
最後,一定記得通知上層:其傳遞下來的URI在底層數據庫中已經發生了變化。
Java代碼
- getContext().getContentResolver().notifyChange(uri, null);
改進NotePad
首先我想指出NotePad的一個bug,其實這個小bug在2月份就有人向官方報告了,參見http://code.google.com/p/android/issues/detail?id=1909。NoteEditor類中的變量mNoteOnly根本就是沒有用處的,因為它始終都是false,沒有任何變化,所以可以刪除掉。第二點是在NoteEditor類中,有下面這樣的語句:
Java代碼
- setResult(RESULT_OK, (new Intent()).setAction(mUri.toString()));
- setResult(RESULT_CANCELED);
可到底想展示什麼技術呢?實際上並沒有完整展現出來,這裡我對其進行修改後來指明。參見http://code.google.com/p/android/issues/detail?id=1671)。首先在NotesList類中增加一個變量
Java代碼
- private static final int REQUEST_INSERT = 100;//請求插入標識符
然後修改onOptionsItemSelected函數如下:
Java代碼
- @Override
- public boolean onOptionsItemSelected(MenuItem item)
- {
- switch (item.getItemId())
- {
- case MENU_ITEM_INSERT:
- this.startActivityForResult(new Intent(Intent.ACTION_INSERT, getIntent().getData()), REQUEST_INSERT);
- return true;
- }
- return super.onOptionsItemSelected(item);
- }
最後重載onActivityResult函數來處理接收到的activity result。
Java代碼
- protected void onActivityResult(int requestCode, int resultCode, Intent data)
- {
- if(requestCode == REQUEST_INSERT)
- {
- if(resultCode==RESULT_OK)
- {
- Log.d(TAG, "OK!!!");
- }
- else if(resultCode==RESULT_CANCELED)
- {
- Log.d(TAG, "CANCELED!!!");
- }
- }
- }
試試,當你在NoteEditor中保存或放棄日志時,觀察LogCat,你可以看到下面這樣的畫面: