Keyword:SQLDelight,Driver
到23日,引入SQLDelight,到在Android上呈現DB資料
KMMDay23
在各平台上的SQLDelight的實作方法不同,因此也需要不同的Driver,好在這個Driver我們不用自己寫,在一開始引入SQLDelight時的Gradle,有幾行就是分別寫在各個平台專用的區塊,其中就有官方幫我們預先寫好的Driver.我們只需要在啟動SQLDelight時提供所需要的Driver就可以了.那今天我們就來學習如何使用
首先現在commonMain底下建立一層封裝,DatabaseHelper來幫助我們使用DB,在這裏需要一個SQLDriver和一條在背景執行IO操作的Coroutine,然後使用SQLDriver建立DB
//這邊是Kotlin喔
class DatabaseHelper(
sqlDriver: SqlDriver,//各平台自己提供自己的Driver
private val backgroundDispatcher: CoroutineDispatcher//背景執行用的Coroutine
) {
private val cafeDB: CafeDB = CafeDB(sqlDriver)//建立DB就是這麼簡單
}
當然有DB也有Coroutine就能夠直接進行資料庫的操作了,不過我們可以幫SQLDelight的Transacter多做一個擴展,讓他使用起來更方便.同樣的在commonMain下建立一個CoroutinesExtensions.kt來撰寫我們的擴展
//這是Kotlin喔
import com.squareup.sqldelight.Transacter
import com.squareup.sqldelight.TransactionWithoutReturn
import kotlinx.coroutines.withContext
import kotlin.coroutines.CoroutineContext
suspend fun Transacter.transactionWithContext(
coroutineContext: CoroutineContext,//執行用的Coroutine
noEnclosing: Boolean = false,//閉包設定
body: TransactionWithoutReturn.() -> Unit//執行後行為
) {
withContext(coroutineContext) {
this@transactionWithContext.transaction(noEnclosing) {
body()
}
}
}
我們昨天寫了兩個SQL,分別是插入資料的insertCafe以及讀取全部資料的getAll,在DatabaseHelper內寫上這兩個方法
//這是Kotlin喔
class DatabaseHelper(
sqlDriver: SqlDriver,
private val backgroundDispatcher: CoroutineDispatcher
) {
private val cafeDB: CafeDB = CafeDB(sqlDriver)
fun selectAllItems(): Flow<List<CAFE>> = //使用Flow,當DB變化時Flow也會提供新資料
cafeDB.cafeQueries
.getAll()
.asFlow()
.mapToList()
.flowOn(backgroundDispatcher)
suspend fun insertCafeList(cafeList: List<CafeResponseItem>) {//插入資料
cafeDB.transactionWithContext(backgroundDispatcher) {
cafeList.forEach { cafe ->
cafeDB.cafeQueries.insertCafe(cafe.id, cafe.name,cafe.address)
}
}
}
}
然後我們有了DB操作的工具,在DataRepository內加入DatabseHelper並使用
class DataRepository :KoinComponent {
companion object {
val tag = DataRepository::class.simpleName
}
private val ktorApi: CafeApi by inject()
private val dbHelper: DatabaseHelper by inject()//我們還沒寫注入所以這邊會有問題
suspend fun fetchCafesFromNetwork(cityName: String) =ktorApi.fetchCafeFromApi(cityName)
//以下兩個方法都利用DbHelper實作
fun getCafeFromDb(): Flow<List<CAFE>> = dbHelper.selectAllItems()
suspend fun insertCafeToDB(cafeResponse: List<CafeResponseItem>) {
dbHelper.insertCafeList(cafeResponse)
}
}
再來,由於我們希望DatabaseHelper由Koin幫我們提供,所以來去修改commonMain底下的Koin.kt檔案,讓Koin知道如何幫我們產生DatabaseHelper,先在coreModule內加上DatabaseHelper的實作方法
//這是Kotlin喔
private val coreModule = module{
...
single {
DatabaseHelper(
get(),//根據建構子,這個應該是SqlDriver,使用get()讓Koin去尋找實作方法
Dispatchers.Default//沒有特別挑的Coroutine
)
}
...
}
現在問題變成了,如何提供SQLDriver給Koin了,就像前面說的,雙平台的Driver不同,所以這時候就是expect/actual出馬的時候了.
在commonMain的最下面,建立一個新的module,叫platformModule.這個Koin Module專門存放各平台不同的實作.所以是expect的.
expect val platformModule: Module
有了expect就要去各平台實作actual了,同樣要注意package路徑要相同,不然KMM沒辦法對應起來.
Android的實作如下,使用AndroidSqliteDriver
actual val platformModule: Module = module {
single<SqlDriver>{
AndroidSqliteDriver(CafeDB.Schema,get(),"CafeDb")//叫做AndroidSqliteDriver
}
}
然後iOS叫做NativeSqliteDriver,因為iOS是利用Native Kotlin完成的
actual val platformModule = module {
single<SqlDriver> {NativeSqliteDriver(CafeDB.Schema,"CafeDB")}
}
大功告成!
回到androidApp內,我們來修改MainViewModel,讓Ktor網路請求的結果不直接顯示在畫面上,而是改為進入DB.
//這是Kotlin
class MainViewModel : ViewModel() , KoinComponent{
private val dataRepository: DataRepository by inject()
private val cafeList = MutableLiveData<List<CAFE>>()
val cafeListLiveData: LiveData<List<CAFE>> = Transformations.map(cafeList) { it }
fun fetchCafeList(cityName :String = "taipei"){
viewModelScope.launch {
val response = async { dataRepository.fetchCafesFromNetwork(cityName)}
val result = response.await()
//cafeList.value = result 不再直接顯示
dataRepository.insertCafeToDB(result)//改為存進DB
}
}
fun fetchCafeFromDB(){
viewModelScope.launch {
dataRepository.getCafeFromDb().collect {//讀取DB的資料
cafeList.value = it//資料更新到LiveData,也能使用Flow的AsLiveData
}
}
}
}
由於我們畫面的資料從網路請求的CafeResponseItem換成DB的物件CAFE,所以Activity和Adatper的內容也需要一並修改.
//這邊是Kotlin
class MainActivity : AppCompatActivity() {
private val viewModel: MainViewModel by viewModel()
private val adapter : CafeAdapter by lazy { CafeAdapter() }
private lateinit var cafeRecyclerView : RecyclerView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
viewModel.fetchCafeList("taipei")//進行網路請求
viewModel.fetchCafeFromDB()//撈取DB資料
viewModel.cafeListLiveData.observe(this, Observer {
adapter.cafeList = it//這邊的cafeList已經變成DB的CAFE了
adapter.notifyDataSetChanged()
})
cafeRecyclerView = findViewById(R.id.rv_cafeList)
cafeRecyclerView.adapter = adapter
cafeRecyclerView.layoutManager = LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false)
cafeRecyclerView.addItemDecoration(
DividerItemDecoration(this,
DividerItemDecoration.VERTICAL)
)
}
}
//這邊是Kotlin
class CafeAdapter : RecyclerView.Adapter<CafeViewHolder>() {
// var cafeList = listOF<CafeResponseItem>()網路請求物件改為DB物件
var cafeList = listOf<CAFE>()
...
}
然後執行可以看到由DB提供的資訊.
在模擬器執行的期間,打開Android Studio下方的 App Inspection,可以看到存在DB內的資料.
明天會來串iOS的DB