本文為 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
| public class WaitlistContract {
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 {
private static final String DATABASE_NAME = "waitlist.db";
private static final int DATABASE_VERSION = 1;
public WaitlistDbHelper(Context context){ super(context, DATABASE_NAME, null, DATABASE_VERSION); }
@Override public void onCreate(SQLiteDatabase sqLiteDatabase) { 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);"; sqLiteDatabase.execSQL(SQL_CREATE_WAITLIST_TABLE); }
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 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));
WaitlistDbHelper dbHelper = new WaitlistDbHelper(this);
mDb = dbHelper.getWritableDatabase();
Cursor cursor = getAllGuests(); mAdapter = new GuestListAdapter(this, cursor);
waitlistRecyclerView.setAdapter(mAdapter); } 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; private Cursor mCursor;
public GuestListAdapter(Context context, Cursor cursor) { this.mContext = context; this.mCursor = cursor; }
@Override public GuestViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { LayoutInflater inflater = LayoutInflater.from(mContext); View view = inflater.inflate(R.layout.guest_list_item, parent, false); return new GuestViewHolder(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 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);
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); }
private long addNewGuest(String name, int partySize) { 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); }
public void addToWaitlist(View view) {
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(mNewGuestNameEditText.getText().toString(), partySize);
mAdapter.swapCursor(getAllGuests());
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)); long id = mCursor.getLong(mCursor.getColumnIndex(WaitlistContract.WaitlistEntry._ID)); holder.itemView.setTag(id); }
|
在 MainActivity 中定義一個刪除資料的 function 。
1 2 3 4 5 6 7 8
|
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) {
@Override public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) { return false; } @Override public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) { long id = (long) viewHolder.itemView.getTag(); removeGuest(id); mAdapter.swapCursor(getAllGuests()); } }).attachToRecyclerView(waitlistRecyclerView);
|