iT邦幫忙

2021 iThome 鐵人賽

DAY 23
0

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) {
        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出馬的時候了.

根據平台提供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


上一篇
Day 22:開心SQL,SQLDelight
下一篇
Day 24:讓iOS也吃到SQL Delight
系列文
挑戰 Kotlin Multiplatform Mobile 跨平台開發,透過共同的Kotlin模組同時打造iOS與Android應用!30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言