Day 23: 不同的环境,不同的Driver,利用Driver 驾驭SQLDelight

Keyword:SQLDelight,Driver
到23日,引入SQLDelight,到在Android上呈现DB资料
KMMDay23


在各平台上的SQLDelight的实作方法不同,因此也需要不同的Driver,好在这个Driver我们不用自己写,在一开始引入SQLDelight时的Gradle,有几行就是分别写在各个平台专用的区块,其中就有官方帮我们预先写好的Driver.我们只需要在启动SQLDelight时提供所需要的Driver就可以了.那今天我们就来学习如何使用

使用DatabaseHelper封装

首先现在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) {
        [email protected](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出马的时候了.

根据平台提供Driver

在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")}
}

大功告成!

在Android使用SQLDelight

回到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内的资料.

https://github.com/officeyuli/itHome2021/raw/main/day23/DB.png

明天会来串iOS的DB


<<:  【Day13-计数】如何快速统计资料出现频率?——使用Counter或groupby快速计算元素出现个数

>>:  DAY28 进行式--工作日志003

DAY24 - 我的网站要分析!网站分析工具的选择和态度(2)

思考了目标,我们可以开始了解网站分析工具。网站工具其实有好多种,大家最常见的GA、还有其他可能像Ho...

DAY08随机森林演算法(续6)

昨天,我们已建立完决策树,那今天,我打算带入资料去看他分类结果: 建立决策树的使用模型: #用树来预...

[ Day 4 ] - 阵列基本介绍

阵列的基本介绍 简单来说是存放一组资料集 阵列 会使用 [ ] 前後包住资料集 下面的范例意思是 d...

C# 语言

https://wolkesau.medium.com/c-语言-f42ebe8eda8c C# 入...

[Day23]Vue3 E2E Testing: Cypress 基本介绍

What's Cypress Cypress 是 Vue.js 官方推荐的一个 E2E Testin...