如果不使用Room、LiveData、ViewModel只使用SqliteOpenHelper會怎樣? 上篇 "查詢"

關於學習Android的資料庫運作,
現在很少直接操作SQLiteOpenHelper
通常會直接學習使用Room和ContentProvider來操作資料,
並配合MMVM架構去撰寫程式碼。

但直接讓新手學這種較多抽象的東西,
如果沒實作過糟糕的方式...恐怕無法真正理解這些做法好在哪?
以及重點差異。

基於上述理由我想用SQLiteOpenHelper實作一個沒有清楚分割UI和Model的案例。

最棒的學習方式是從問題中學習並解決它,
所以我們來假設一個需求「能查看&修改產品清單的App」
(當然只限於Android端)。

產品可視為一個資料表Products,
每一個產品需要有產品名稱、價格、庫存量,最後還需要一個唯一值的ID,總共4個Column要定義。

規劃的架構如下:

Step1 建立一個Contract來管理資料庫、資料表會用到的常量。


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"
}
}
}
將條目定義成常量的好處在於IDE會提供自動補全,
可以避免打錯字這類低階錯誤,

Step2 建立資料類別Product


package com.blogspot.zongjia.products.Sqlite
data class Product(val name: String, val price: Int, val quality: Int)
view raw Product.kt hosted with ❤ by GitHub
將每一筆資料都轉成Product物件處理,方便RecyclerView的Adapter處理。

Step3 建立ProductSqlOpenHelper

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,並實現類的onCreateonUpgrade
通常onCreate做的就是建立資料表,onUpgrade則是當資料表結構有變動之類的進行更新的函數(當VERSION_NUMBER改變)。

另外我又定義了兩個方法,
查詢資料用的方法getAllProducts

 回傳的結果型態是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()。

Step4 "產品列表"畫面RecyclerView


原本就有的activity_main還會需要再兩個XML ,
一個RecyclerView和一個itemView。
<?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>
<?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>
<?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。
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
}
}
建構元參數List<Product> products是填充每個item的資料來源。

需要定義好ProductViewHolder和實作三個方法。
onCreateViewHolder()中指定item要填充的layout(此為content_main_item.xml)
getItemCount() 回傳 products的大小。
onBindViewHolder將當前position位置的item資料填入holder中對應的View(此為nameTextViewsummaryTextView)中

Step6 設定MainActivity


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()
}
}
view raw MainActivity.kt hosted with ❤ by GitHub

定義RecyclerView的Adapter和LayoutManager

layoutManager定義為LinearLayout(也可是GridLayout等等),
沒設定也不報錯算是初學蠻常忘的東西。
adapter的productList是從Sqlite查詢得到的,

管理ProductSqlOpenHelper的生命週期

建立一個myHelper屬性,
onCreate時賦值給它一個ProductSqlOpenHelper。
onDestroy時進行關閉的動作myHelper
結果

下篇 "新增、刪除、修改"

留言

這個網誌中的熱門文章

Kotlin玩Android第03篇 Listview(01 ArrayAdapter)

使用Intent在Activity間傳遞資料(bundle)

golang本地測試如何繞過防火牆詢問允許