如果不使用Room、LiveData、ViewModel只使用SqliteOpenHelper會怎樣? 上篇 "查詢"
關於學習Android的資料庫運作,
現在很少直接操作SQLiteOpenHelper,
通常會直接學習使用Room和ContentProvider來操作資料,
並配合MMVM架構去撰寫程式碼。
但直接讓新手學這種較多抽象的東西,
如果沒實作過糟糕的方式...恐怕無法真正理解這些做法好在哪?
以及重點差異。
基於上述理由我想用SQLiteOpenHelper實作一個沒有清楚分割UI和Model的案例。
最棒的學習方式是從問題中學習並解決它,
所以我們來假設一個需求「能查看&修改產品清單的App」
(當然只限於Android端)。
產品可視為一個資料表Products,
每一個產品需要有產品名稱、價格、庫存量,最後還需要一個唯一值的ID,總共4個Column要定義。
規劃的架構如下:
將條目定義成常量的好處在於IDE會提供自動補全,
可以避免打錯字這類低階錯誤,
將每一筆資料都轉成Product物件處理,方便RecyclerView的Adapter處理。
原本就有的activity_main還會需要再兩個XML ,
一個RecyclerView和一個itemView。
建構元參數List<Product> products是填充每個item的資料來源。
需要定義好ProductViewHolder和實作三個方法。
onCreateViewHolder()中指定item要填充的layout(此為content_main_item.xml)
getItemCount() 回傳 products的大小。
onBindViewHolder將當前position位置的item資料填入holder中對應的View(此為nameTextView和summaryTextView)中
沒設定也不報錯算是初學蠻常忘的東西。
adapter的productList是從Sqlite查詢得到的,
當onCreate時賦值給它一個ProductSqlOpenHelper。
當onDestroy時進行關閉的動作myHelper
結果
現在很少直接操作SQLiteOpenHelper,
通常會直接學習使用Room和ContentProvider來操作資料,
並配合MMVM架構去撰寫程式碼。
但直接讓新手學這種較多抽象的東西,
如果沒實作過糟糕的方式...恐怕無法真正理解這些做法好在哪?
以及重點差異。
基於上述理由我想用SQLiteOpenHelper實作一個沒有清楚分割UI和Model的案例。
最棒的學習方式是從問題中學習並解決它,
所以我們來假設一個需求「能查看&修改產品清單的App」
(當然只限於Android端)。
產品可視為一個資料表Products,
每一個產品需要有產品名稱、價格、庫存量,最後還需要一個唯一值的ID,總共4個Column要定義。
規劃的架構如下:
Step1 建立一個Contract來管理資料庫、資料表會用到的常量。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package com.blogspot.zongjia.products.Sqlite | |
import android.provider.BaseColumns | |
interface ProductContract: BaseColumns { | |
companion object { | |
const val DATABASE_NAME = "product_db" | |
} | |
class ProductEntry { | |
companion object { | |
const val TABLE_NAME = "products" | |
const val COLUMN_ID = BaseColumns._ID | |
const val COLUMN_NAME = "product_name" | |
const val COLUMN_PRICE = "product_price" | |
const val COLUMN_QUALITY = "product_quality" | |
} | |
} | |
} |
可以避免打錯字這類低階錯誤,
Step2 建立資料類別Product
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package com.blogspot.zongjia.products.Sqlite | |
data class Product(val name: String, val price: Int, val quality: Int) |
Step3 建立ProductSqlOpenHelper
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package com.blogspot.zongjia.products.Sqlite | |
import android.content.ContentValues | |
import android.content.Context | |
import android.database.sqlite.SQLiteDatabase | |
import android.database.sqlite.SQLiteOpenHelper | |
import android.util.Log | |
import com.blogspot.zongjia.products.Sqlite.ProductContract.ProductEntry | |
class ProductSqlOpenHelper(context: Context): | |
SQLiteOpenHelper(context, DATABASE_NAME, null, VERSION_NUMBER) { | |
companion object { | |
val LOG_TAG = ProductSqlOpenHelper.javaClass.simpleName | |
const val DATABASE_NAME = ProductContract.DATABASE_NAME | |
const val VERSION_NUMBER = 1 | |
} | |
override fun onCreate(db: SQLiteDatabase?) { | |
Log.d(LOG_TAG, "onCreate新的資料表 ${ProductEntry.TABLE_NAME}") | |
db?.apply { | |
db.execSQL( | |
"CREATE TABLE ${ProductEntry.TABLE_NAME} (" + | |
"${ProductEntry.COLUMN_ID} INTEGER PRIMARY KEY," + | |
"${ProductEntry.COLUMN_NAME} TEXT NOT NULL," + | |
"${ProductEntry.COLUMN_PRICE} INTEGER NOT NULL," + | |
"${ProductEntry.COLUMN_QUALITY} INTEGER NOT NULL DEFAULT 0)" | |
) | |
for (i in 1..3) { | |
insertProduct(db, Product("蘋果 $i 世", 587, 5354)) | |
insertProduct(db, Product("三字經 $i 世", 449, 62)) | |
insertProduct(db, Product("假面騎士KABUTO $i 世", 777777, 1)) | |
insertProduct(db, Product("地球儀 $i 世", 1234, 98)) | |
} | |
} | |
} | |
override fun onUpgrade(db: SQLiteDatabase?, oldVersion: Int, newVersion: Int) { | |
if (newVersion > oldVersion) { | |
Log.d(LOG_TAG, "成功onUpgrade Ver.$oldVersion -> Ver.$newVersion") | |
// 先刪除舊有的Table Products | |
db?.execSQL("DROP TABLE IF EXISTS table_name ", | |
arrayOf<String>(ProductEntry.TABLE_NAME)) | |
// 重新創建一次Table Products | |
onCreate(db) | |
} else { | |
Log.d(LOG_TAG, "失敗onUpgrade Ver.$oldVersion -> Ver.$newVersion") | |
} | |
} | |
fun insertProduct(db:SQLiteDatabase, product: Product): Long { | |
val contentValues = ContentValues() | |
contentValues.put(ProductEntry.COLUMN_NAME, product.name) | |
contentValues.put(ProductEntry.COLUMN_PRICE, product.price) | |
contentValues.put(ProductEntry.COLUMN_QUALITY, product.quality) | |
val insertId = db.insert(ProductEntry.TABLE_NAME, | |
null, | |
contentValues) | |
// 回傳 | |
return insertId | |
} | |
fun getAllProducts(db: SQLiteDatabase): List<Product> { | |
val projection = arrayOf(ProductEntry.COLUMN_NAME, | |
ProductEntry.COLUMN_PRICE, | |
ProductEntry.COLUMN_QUALITY) | |
val queryCursor = db.query(ProductEntry.TABLE_NAME, | |
projection, | |
null, | |
null, | |
null, | |
null, | |
null | |
) | |
val productList = mutableListOf<Product>() | |
queryCursor?.let {cursor -> | |
while (cursor.moveToNext()) { | |
// 取得Column在此cursor的索引 | |
val nameIndex = cursor.getColumnIndex(ProductEntry.COLUMN_NAME) | |
val priceIndex = cursor.getColumnIndex(ProductEntry.COLUMN_PRICE) | |
val qualityIndex = cursor.getColumnIndex(ProductEntry.COLUMN_QUALITY) | |
// 取得要填入每個item的值 | |
val productName = cursor.getString(nameIndex) | |
val productPrice = cursor.getInt(priceIndex) | |
val productQuality = cursor.getInt(qualityIndex) | |
// 建立一個當前的Product,並塞到product列表中 | |
val currentProduct = Product(productName, productPrice, productQuality) | |
productList.add(currentProduct) | |
} | |
} | |
// 關閉查詢Cursor的連接 | |
queryCursor.close() | |
// 回傳 從Sql查詢取得的product列表 | |
return productList | |
} | |
} | |
繼承父類SqliteOpenHelper,並實現類的onCreate和onUpgrade。
通常onCreate做的就是建立資料表,onUpgrade則是當資料表結構有變動之類的進行更新的函數(當VERSION_NUMBER改變)。
另外我又定義了兩個方法,
查詢資料用的方法getAllProducts
回傳的結果型態是List<Product>,之後將會提供給Adapter使用。
參數只需要SqliteDatabase,
回傳的結果就是一個Product的List(List<Product>),
之後會提供給Adapter使用。
參數只需要SqliteDatabase,
回傳的結果就是一個Product的List(List<Product>),
之後會提供給Adapter使用。
插入Product用的方法insertProduct
參數包掛db、Product代表要插入Products資料表的資料,
回傳新增item的id,如果新增失敗回傳-1,
因為db.insert會回傳插入item的id,當錯誤發生會回傳-1。
方法內部使用ContentValues以Key-Value的形式將Product插入資料表。
註: 你應該在Cursor用完後關閉,但千萬不要隨便關閉db.close()
最好是在onDestroy呼叫時再進行關閉,
且關閉也最好用Helper的close()而不是database的close()。
回傳新增item的id,如果新增失敗回傳-1,
因為db.insert會回傳插入item的id,當錯誤發生會回傳-1。
方法內部使用ContentValues以Key-Value的形式將Product插入資料表。
註: 你應該在Cursor用完後關閉,但千萬不要隨便關閉db.close()
最好是在onDestroy呼叫時再進行關閉,
且關閉也最好用Helper的close()而不是database的close()。
Step4 "產品列表"畫面RecyclerView
原本就有的activity_main還會需要再兩個XML ,
一個RecyclerView和一個itemView。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?xml version="1.0" encoding="utf-8"?> | |
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" | |
xmlns:app="http://schemas.android.com/apk/res-auto" | |
xmlns:tools="http://schemas.android.com/tools" | |
android:layout_width="match_parent" | |
android:layout_height="match_parent" | |
tools:context=".MainActivity"> | |
<include layout="@layout/content_main"/> | |
</android.support.constraint.ConstraintLayout> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?xml version="1.0" encoding="utf-8"?> | |
<android.support.v7.widget.RecyclerView xmlns:android="http://schemas.android.com/apk/res/android" | |
xmlns:tools="http://schemas.android.com/tools" | |
android:id="@+id/recycler_view" | |
android:layout_width="match_parent" | |
android:layout_height="match_parent" | |
tools:listitem="@layout/content_main_item"></android.support.v7.widget.RecyclerView> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?xml version="1.0" encoding="utf-8"?> | |
<LinearLayout | |
xmlns:android="http://schemas.android.com/apk/res/android" | |
xmlns:tools="http://schemas.android.com/tools" | |
android:layout_width="match_parent" | |
android:layout_height="wrap_content" | |
android:orientation="vertical" | |
android:paddingTop="8dp" | |
android:paddingLeft="16dp" | |
android:background="@color/itemBackgroundColor"> | |
<TextView | |
android:id="@+id/name" | |
android:layout_width="match_parent" | |
android:layout_height="wrap_content" | |
android:layout_marginBottom="8dp" | |
tools:text="這是商品名稱" | |
android:textAppearance="@android:style/TextAppearance.Material.Large" | |
/> | |
<TextView | |
android:id="@+id/summary" | |
android:layout_width="match_parent" | |
android:layout_height="wrap_content" | |
tools:text="這是商品描述" | |
android:paddingLeft="8dp" | |
android:textAppearance="@android:style/TextAppearance.Material.Medium"/> | |
</LinearLayout> |
Step5 建立Adapter
Adapter的功用主要是將資料正確填充在列表的每個item。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package com.blogspot.zongjia.products.Sqlite | |
import android.support.annotation.MainThread | |
import android.support.v7.widget.RecyclerView | |
import android.view.LayoutInflater | |
import android.view.View | |
import android.view.ViewGroup | |
import android.widget.TextView | |
import com.blogspot.zongjia.products.R | |
class ProductListAdapter(var products: List<Product>): RecyclerView.Adapter<ProductListAdapter.ProductViewHolder>(){ | |
inner class ProductViewHolder(itemView: View): RecyclerView.ViewHolder(itemView) { | |
val nameTextView = itemView.findViewById<TextView>(R.id.name) | |
val summaryTextView = itemView.findViewById<TextView>(R.id.summary) | |
} | |
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ProductViewHolder { | |
val itemView = LayoutInflater. | |
from(parent.context). | |
inflate(R.layout.content_main_item, parent, false) | |
return ProductViewHolder(itemView) | |
} | |
override fun getItemCount(): Int { | |
return products.size | |
} | |
override fun onBindViewHolder(holder: ProductViewHolder, position: Int) { | |
val currentProduct = products[position] | |
val name = currentProduct.name | |
val price = currentProduct.price | |
val quality = currentProduct.quality | |
val summary = holder.itemView.context.getString(R.string.item_summary_string, | |
price, | |
quality) | |
// rebinding the data to the item view | |
holder.nameTextView.text = name | |
holder.summaryTextView.text = summary | |
} | |
} |
需要定義好ProductViewHolder和實作三個方法。
onCreateViewHolder()中指定item要填充的layout(此為content_main_item.xml)
getItemCount() 回傳 products的大小。
onBindViewHolder將當前position位置的item資料填入holder中對應的View(此為nameTextView和summaryTextView)中
Step6 設定MainActivity
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package com.blogspot.zongjia.products | |
import android.os.Bundle | |
import android.support.v7.app.AppCompatActivity | |
import android.support.v7.widget.LinearLayoutManager | |
import android.widget.LinearLayout | |
import com.blogspot.zongjia.products.Sqlite.* | |
import kotlinx.android.synthetic.main.content_main.* | |
class MainActivity : AppCompatActivity() { | |
lateinit var myHelper: ProductSqlOpenHelper | |
override fun onCreate(savedInstanceState: Bundle?) { | |
super.onCreate(savedInstanceState) | |
setContentView(R.layout.activity_main) | |
myHelper = ProductSqlOpenHelper(this) | |
val productList = myHelper.getAllProducts(myHelper.readableDatabase) | |
recycler_view.layoutManager = LinearLayoutManager(this, | |
LinearLayout.VERTICAL, | |
false) | |
val adapter = ProductListAdapter(productList) | |
recycler_view.adapter = adapter | |
} | |
override fun onDestroy() { | |
super.onDestroy() | |
myHelper.close() | |
} | |
} |
定義RecyclerView的Adapter和LayoutManager
layoutManager定義為LinearLayout(也可是GridLayout等等),沒設定也不報錯算是初學蠻常忘的東西。
adapter的productList是從Sqlite查詢得到的,
管理ProductSqlOpenHelper的生命週期
建立一個myHelper屬性,當onCreate時賦值給它一個ProductSqlOpenHelper。
當onDestroy時進行關閉的動作myHelper
結果
下篇 "新增、刪除、修改"
留言
張貼留言