Udacity | Developing Android Apps | 在 SQLite 中存儲數據 課程筆記

本文為 Udacity Developing Android Apps 「在 SQLite 中存儲數據」的課程筆記。
(這個章節好像是中文版才有;在中文版的第八章、英文版的第六到第七章之間。)

章節目標

此章節的最後會完成一個讓店員安排客人等候的應用。
多個店員可以同時使用 App 將客人的資料新增到資料庫,並查詢排序的結果;當客人入座時,可以將等候的資料刪除。

學習目標如下:

  • 了解如何在 Android 中使用 SQLite
    • 使用 Contract 類別
    • 定義資料庫 Schema
    • 撰寫 Query 以新增、查詢及刪除資料
  • 調整 RecyclerView 顯示資料的方式

官方文件: Save data using SQLite

Contract

Contract 類別用來連接 Android 和 SQL 資料庫中。
其中會使用 BaseColumns interface 來實作資料表名稱和欄位。
BaseColumns 已有預設一個 _ID 欄位,作為資料表的 primary key 。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 自訂一個 Contract 類別
public class WaitlistContract {

// 避免 Contract 誤被實體化,將 constructor 設為 private
private WaitlistContract() {}

// 定義資料表名稱和欄位
public static class WaitlistEntry implements BaseColumns {

// 資料表名稱
public static final String TABLE_NAME = "waitlist";

// 欄位名稱
public static final String COLUMN_GUEST_NAME = "guestName";
public static final String COLUMN_PARTY_SIZE = "partySize";
public static final String COLUMN_TIMESTAMP = "timestamp";
}
}

SQLiteOpenHelper

SQLiteOpenHelper 類別是用來創建資料庫,並在 Schema 改變時更新資料庫。
其中 onCreate 方法是資料庫被創建時的操作, onUpgrade 方法則是被更新時的內容,確保資料有被更新到。
我們可以撰寫一個 DBHelper 來 extends SQLiteOpenHelper 。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public class WaitlistDbHelper extends SQLiteOpenHelper {

// 資料庫存在 Android 中的檔案名稱
private static final String DATABASE_NAME = "waitlist.db";

// 資料庫的版本號,每次有更新的時候就要自己加一,讓使用者依據這個版本號而被要求更新
// 從 Contract 那邊更動 Schema 時,版本也要增加
private static final int DATABASE_VERSION = 1;

// Constructor 沒有需要 override 的,所以用 super 呼叫父類別的 constructor
// 其中第三個參數應該要填 CursorFactory ,這邊不需要所以為 null
public WaitlistDbHelper(Context context){
super(context, DATABASE_NAME, null, DATABASE_VERSION);
}

// WaitlistDbHelper 被實體化時就會呼叫 onCreate 來創建資料表
@Override
public void onCreate(SQLiteDatabase sqLiteDatabase)
{
// SQL DLL
// 想改 Schema 的話,只要到 Contract 改就可以
final String SQL_CREATE_WAITLIST_TABLE =
"create table " + WaitlistContract.WaitlistEntry.TABLE_NAME + "(" +
WaitlistContract.WaitlistEntry._ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " +
WaitlistContract.WaitlistEntry.COLUMN_GUEST_NAME + " TEXT NOT NULL," +
WaitlistContract.WaitlistEntry.COLUMN_PARTY_SIZE + " INTEGER NOT NULL," +
WaitlistContract.WaitlistEntry.COLUMN_TIMESTAMP + " TIMESTAMP DEFAULT CURRENT_TIMESTAMP);";
// 執行 query
sqLiteDatabase.execSQL(SQL_CREATE_WAITLIST_TABLE);
}

// 當有新的版本號時會呼叫 onUpgrade
public void onUpgrade(SQLiteDatabase sqLiteDatabase, int i, int i1)
{
sqLiteDatabase.execSQL("DROP TABLE IF EXISTS " + WaitlistContract.WaitlistEntry.TABLE_NAME + ";");
onCreate(sqLiteDatabase);
}
}

查詢所有 tuple

有了 Contract 和 DBHelper 後,就要在 Activity 中實體化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
public class MainActivity extends AppCompatActivity {

private GuestListAdapter mAdapter;
// 宣告一個 SQLiteDatabase 變數
SQLiteDatabase mDb;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

RecyclerView waitlistRecyclerView;

waitlistRecyclerView = (RecyclerView) this.findViewById(R.id.all_guests_list_view);

waitlistRecyclerView.setLayoutManager(new LinearLayoutManager(this));

// new 一個 DbHelper
WaitlistDbHelper dbHelper = new WaitlistDbHelper(this);

// 從 DbHelper 取得一個可寫的 DB
mDb = dbHelper.getWritableDatabase();

// 取得 Cousor ,並將需要的參數傳入 Adapter 中
Cursor cursor = getAllGuests();
mAdapter = new GuestListAdapter(this, cursor);

waitlistRecyclerView.setAdapter(mAdapter);
}
// 用 query 取得 Cousor
private Cursor getAllGuests() {
return mDb.query(
WaitlistContract.WaitlistEntry.TABLE_NAME,
null,
null,
null,
null,
null,
WaitlistContract.WaitlistEntry.COLUMN_TIMESTAMP
);
}
}

Adapter 要依據 Cursor 調整顯示的內容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
public class GuestListAdapter extends RecyclerView.Adapter<GuestListAdapter.GuestViewHolder> {

private Context mContext;
// 增加一個 member data
private Cursor mCursor;

// 將新的 member data 加入 constructor 中
public GuestListAdapter(Context context, Cursor cursor) {
this.mContext = context;
this.mCursor = cursor;
}

@Override
public GuestViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
// Get the RecyclerView item layout
LayoutInflater inflater = LayoutInflater.from(mContext);
View view = inflater.inflate(R.layout.guest_list_item, parent, false);
return new GuestViewHolder(view);
}

// 將資料放入 View 中
@Override
public void onBindViewHolder(GuestViewHolder holder, int position) {
if (!this.mCursor.moveToPosition(position))
{
return;
}
String name = mCursor.getString(mCursor.getColumnIndex(WaitlistContract.WaitlistEntry.COLUMN_GUEST_NAME));
int partySize = mCursor.getInt(mCursor.getColumnIndex(WaitlistContract.WaitlistEntry.COLUMN_PARTY_SIZE));
holder.nameTextView.setText(name);
holder.partySizeTextView.setText(String.valueOf(partySize));
}

@Override
public int getItemCount() {
return this.mCursor.getCount();
}

class GuestViewHolder extends RecyclerView.ViewHolder {

TextView nameTextView;
TextView partySizeTextView;

public GuestViewHolder(View itemView) {
super(itemView);
nameTextView = (TextView) itemView.findViewById(R.id.name_text_view);
partySizeTextView = (TextView) itemView.findViewById(R.id.party_size_text_view);
}

}
}

透過 EditText 新增 Tuple

我們可以在 MainActivity 增加 EditText 讓使用者輸入排隊客人的名稱和人數,並將資料傳到資料庫。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
public class MainActivity extends AppCompatActivity {

private GuestListAdapter mAdapter;
private SQLiteDatabase mDb;

// 宣告 EditText 變數
EditText mNewGuestNameEditText, mNewPartySizeEditText;

String LOG_TAG = MainActivity.class.getSimpleName();

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

RecyclerView waitlistRecyclerView;

waitlistRecyclerView = (RecyclerView) this.findViewById(R.id.all_guests_list_view);

// 取得 EditText 實體
mNewGuestNameEditText = (EditText) findViewById(R.id.person_name_edit_text);
mNewPartySizeEditText = (EditText) findViewById(R.id.party_count_edit_text);

waitlistRecyclerView.setLayoutManager(new LinearLayoutManager(this));

WaitlistDbHelper dbHelper = new WaitlistDbHelper(this);

mDb = dbHelper.getWritableDatabase();

Cursor cursor = getAllGuests();

mAdapter = new GuestListAdapter(this, cursor);

waitlistRecyclerView.setAdapter(mAdapter);
}

// 宣告一個新增資料到資料庫的 function
private long addNewGuest(String name, int partySize)
{
// 宣告一個 ContentValues 實體
ContentValues cv = new ContentValues();
// 將資料放進對應欄位
cv.put(WaitlistContract.WaitlistEntry.COLUMN_GUEST_NAME, name);
cv.put(WaitlistContract.WaitlistEntry.COLUMN_PARTY_SIZE, partySize);
// 將資料傳到資料庫
return mDb.insert(WaitlistContract.WaitlistEntry.TABLE_NAME, null, cv);
}

// 宣告一個點擊新增按鈕時會觸發的 function
public void addToWaitlist(View view) {

// 若 EditText 為空則 return
if(mNewPartySizeEditText.getText().length() == 0 || mNewGuestNameEditText.getText().length() == 0)
{
return;
}

int partySize = 1;

// 檢查人數是否為整數
try{
partySize = Integer.parseInt(mNewPartySizeEditText.getText().toString());
}
catch(Exception e){
Log.e(LOG_TAG, "Failed to parse party size to integer: " + e.getMessage());
}

// 呼叫 addNewGuest
addNewGuest(mNewGuestNameEditText.getText().toString(), partySize);

// 更新 Adapter 的 cursor (因為資料有更動)
mAdapter.swapCursor(getAllGuests());

// 清除 EditText
mNewGuestNameEditText.getText().clear();
mNewPartySizeEditText.getText().clear();
}
}

透過滑動刪除 Tuple

接下來的部分是讓使用者透過滑動來刪除項目。
首先我們可以在 Adapter 的 onBindViewHolder 中用 setTag 將畫面上的項目編上 id 。

1
2
3
4
5
6
7
8
9
10
11
12
13
public void onBindViewHolder(GuestViewHolder holder, int position) {
if (!mCursor.moveToPosition(position))
return;

String name = mCursor.getString(mCursor.getColumnIndex(WaitlistContract.WaitlistEntry.COLUMN_GUEST_NAME));
int partySize = mCursor.getInt(mCursor.getColumnIndex(WaitlistContract.WaitlistEntry.COLUMN_PARTY_SIZE));
holder.nameTextView.setText(name);
holder.partySizeTextView.setText(String.valueOf(partySize));
// 取出 cursor 目前指到的 tuple 的 id
long id = mCursor.getLong(mCursor.getColumnIndex(WaitlistContract.WaitlistEntry._ID));
// 用 setTag 將 id 放在 itemView 上
holder.itemView.setTag(id);
}

在 MainActivity 中定義一個刪除資料的 function 。

1
2
3
4
5
6
7
8
/**
* 刪除資料庫中特定 id 的資料
* @param id 資料的 id
* @return 是否刪除成功
*/
private boolean removeGuest(Long id){
return mDb.delete(WaitlistContract.WaitlistEntry.TABLE_NAME, WaitlistContract.WaitlistEntry._ID +"="+id,null)>0;
}

並在 onCreate 中用 ItemTouchHelper 來處理滑動。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

new ItemTouchHelper(new ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT) {

// 用不到所以 return false
@Override
public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) {
return false;
}
// 滑動時觸發的內容
@Override
public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
// 取得放在 itemView 中的 id
long id = (long) viewHolder.itemView.getTag();
// 呼叫 removeGuest
removeGuest(id);
// 更新 Cursor
mAdapter.swapCursor(getAllGuests());
}
// 將 ItemTouchHelper 綁到 RecyclerView
}).attachToRecyclerView(waitlistRecyclerView);