Android實例剖析筆記(四)
NoteEditor深入分析
首先來弄清楚“日志編輯“的狀態轉換,通過上篇文章的方法來做下面這樣一個實驗,首先進入“日志編輯“時會觸發onCreate和onResume,然后用戶通過Option Menu選擇”Edit title”后,會觸發onSaveInstanceState和onPause,最后,用戶回到編輯界面,則再次觸發onResume。
最終通過LogCat可以得到下圖:
那么下面就按照上述順序對此類進行剖析。首先是onCreate方法,一開始先獲取導致進入“日志編輯”界面的intent,分析其操作類型可得知是“編輯日志”還是“新增日志”。
// Do some setup based on the action being performed.
final String action = intent.getAction();
若是“編輯日志”,則設置當前狀態為“編輯”,并保存待編輯日志的URI.
mUri = intent.getData();
若是“新增日志”,則設置當前狀態為“新增”,并通過content provider向數據庫中新增一個“空白日志”,后者返回“空白日志”的URI.
mUri = getContentResolver().insert(intent.getData(), null);
然后不管是“編輯”或“新增”,都需要從數據庫中讀取日志信息(當然,若是“新增”,讀出來的肯定是空數據)。
最后,類似于web應用中使用的Session,這里也將日志文本保存在InstanceState中,因此,若此activity的實例此前是處于stop狀態,則我們可以從它那取出它原本的文本數據.
{
mOriginalContent = savedInstanceState.getString(ORIGINAL_CONTENT);
}
第二個來分析onResume函數,首先把游標置于第一行(也只有一行)
然后取出“正文”字段,這時有一個比較有趣的技巧,“設置文本”并不是調用setText,而是調用的setTextKeepState,后者相對于前者有一個優點,就是當界面此前stop掉,現在重新resume回來,那么此前光標所在位置仍然得以保存。而若使用setText,則光標會重置到行首。
mText.setTextKeepState(note);
最后,將當前編輯的正文保存到一個字符串變量中,用于當activity被暫停時使用。
{
mOriginalContent = note;
}
通過前面的圖可以得知,activity被暫停時,首先調用的是onSaveInstanceState函數。
這里就僅僅將當前正編輯的正文保存到InstanceState中(類似于Session)。最后來看onPause函數,這里首先要考慮的是若activity正要關閉,并且編輯區沒有正文,則將此日志刪除。
{
setResult(RESULT_CANCELED);
deleteNote();
}
否則的話,就更新日志信息
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中,我們再一次看到下面這段熟悉的代碼了:
intent.addCategory(Intent.CATEGORY_ALTERNATIVE);
menu.addIntentOptions(Menu.CATEGORY_ALTERNATIVE, 0, 0,
new ComponentName(this, NoteEditor.class), null, intent, 0, null);
這種生成動態菜單的機制在Android實例剖析筆記(二)這篇文章中已經介紹過了,就不贅述了。最后,來看下放棄日志和刪除日志的實現,由于還沒有接觸到底層的content provider,這里都是通過getContentResolver()提供的update,delete,insert來向底層的content provider發出請求,由后者完成實際的數據庫操作。
{
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)返回數據類型
這五個功能分別對應下述五個可以重載的方法:
{
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是如何為上層提供數據庫層支持的。下面這三個字段指明了數據庫名稱,數據庫版本,數據表名稱。
private static final int DATABASE_VERSION = 2;
private static final String NOTES_TABLE_NAME = "notes";
實際的數據庫操作其實都是通過一個私有靜態類DatabaseHelper實現的,其構造函數負責創建指定名稱和版本的數據庫,onCreate函數則創建指定名稱和各個數據域的數據表(就是簡單的建表SQL語句)。onUpgrade負責刪除數據表,再重新建表。
{
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函數的用處了,也正是通過它的解析,才能區分開到底是對全部日志還是對某一條日志進行操作。
{
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)返回值其實是由下述語句決定的。
sUriMatcher.addURI(NotePad.AUTHORITY, "notes", NOTES);
sUriMatcher.addURI(NotePad.AUTHORITY, "notes/#", NOTE_ID);
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);
}
數據庫的增,刪,改,查操作基本都一樣,具體可以參考官方文檔,這里就僅僅以刪除為例進行說明。一般可以分為三步來完成,首先打開數據庫
然后根據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在底層數據庫中已經發生了變化。
對NotePad的改進
首先我想指出NotePad的一個bug,其實這個小bug在2月份就有人向官方報告了,參見http://code.google.com/p/android/issues/detail?id=1909。NoteEditor類中的變量mNoteOnly根本就是沒有用處的,因為它始終都是false,沒有任何變化,所以可以刪除掉。第二點是在NoteEditor類中,有下面這樣的語句:
setResult(RESULT_CANCELED);
可到底想展示什么技術呢?實際上并沒有完整展現出來,這里我對其進行修改后來指明。參見http://code.google.com/p/android/issues/detail?id=1671)。首先在NotesList類中增加一個變量
然后修改onOptionsItemSelected函數如下:
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。
{
if(requestCode == REQUEST_INSERT)
{
if(resultCode==RESULT_OK)
{
Log.d(TAG, "OK!!!");
}
else if(resultCode==RESULT_CANCELED)
{
Log.d(TAG, "CANCELED!!!");
}
}
}
試試,當你在NoteEditor中保存或放棄日志時,觀察LogCat,你可以看到下面這樣的畫面: