kmm을 이용하여 db를 사용하는 앱을 만드려면 여러방법이 있겠지만 그중에서
android/ios를 지원하는 db인 sqldelight를 소개한다.
KMM 프로젝트를 생성하고 다음 과정을 거쳐 SQLDelight를 설정하자
shared 모듈의 build.gradle.kts에 다음과 같이 추가한다.
plugins {
...
id("app.cash.sqldelight") version "2.0.0-alpha05"
}
//하단 맨 아래에 추가한다.
sqldelight {
databases {
// 두앱 android/ios에서 사용할 이름을 Database로 설정
create("Database") {
// db 파일의 위치를 com.test 위치에 생성되도록 함
packageName.set("com.test")
}
}
}
현재 1.5.4 버전이 최종버전이고 2.0.0 버전이 alpha 버전이다.
차후를 위해 2.0.0 버전으로 진행한다.
kotlin {
sourceSets {
val sqlDelightVersion = "2.0.0-alpha05"
val commonMain by getting {
dependencies {
//https://cashapp.github.io/sqldelight/2.0.0-alpha05/multiplatform_sqlite/types/ 여기서 자세한 내용 확인
implementation("app.cash.sqldelight:primitive-adapters:$sqlDelightVersion")
}
}
...
val androidMain by getting {
dependencies {
implementation("app.cash.sqldelight:android-driver:$sqlDelightVersion")
}
}
...
val iosMain by creating {
...
dependencies {
implementation("app.cash.sqldelight:native-driver:$sqlDelightVersion")
}
}
}
}
작업하기전에 여기로 이동하여 안드로이드 스튜디오에 플러그인을 추가해주자.
shared 모듈의 commonMain에 sqldelight 폴더를 만들고 아래에 동일하게 하위 폴더를 만들자.
그리고 TODO Item으로 사용될 sq 내용을 작성하자
여기서는 ToDoItem과 전체선택/입력/삭제 3개의 쿼리만 구현해보겠다(사실db쪽은 잘모른다..Room하면서 야매로 배운것..)
//ToDoItem.sq
import kotlin.Boolean;
CREATE TABLE TODOItem (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
isFinish INTEGER AS Boolean NOT NULL
);
selectAll:
SELECT *
FROM TODOItem;
insert:
INSERT INTO TODOItem(id,title,isFinish)
VALUES (?,?,?);
updateFinish:
UPDATE TODOItem
SET isFinish =:isFinish
WHERE id = ?;
deleteById:
DELETE FROM TODOItem
WHERE id = ?;
이렇게 작성을 하면 해당 모듈의 build/generated/sqldelighy/code에 설정한 db명으로 자동으로 database 관련 파일이 만들어 진다.

Android Room과 비교하면
1. ToDoItem > Entity
2. ToDoItemQueries > Dao
3. Database > AppDataBase
와 동일한 기능을 한다고 보면된다.
이제 각 앱에서 사용할 DB 모듈을 구현해 보자.
먼저 두 플랫폼에서 기본이될 구현체를 만들자
CommonMain/kotlin으로 이동하여 앱의 패키지명 아래 DriverFactory Class를 만들자
//DriverFactory.kt
import app.cash.sqldelight.db.SqlDriver
expect class DriverFactory {
fun createDriver(): SqlDriver
}
이렇게 expect class를 생성하면 빨갛게 줄이 생길텐데 이는 android/ios 쪽에 구현이 되어있지 않아서 그렇다.
밑에서 구현을 할 것이니 넘어가자
(expect /actul class는 kmm에서 사용되는 함수이다. 각 플랫폼에서 구현해야 할 사항이 있는 경우 interface처럼 제약을 걸어준다 생각하면 된다.)
androidMain폴더로 이동하여 동일한 이름을 가진 DriverFactory를 구현해 준다.
//DriverFactory.kt
import android.content.Context
import app.cash.sqldelight.db.SqlDriver
import app.cash.sqldelight.driver.android.AndroidSqliteDriver
import com.test.Database
actual class DriverFactory(private val context : Context) {
actual fun createDriver(): SqlDriver {
return AndroidSqliteDriver(Database.Schema,context,"todo.db")
}
}
IOS도 동일하게 iosMain폴더로 이동하여 동일한 이름을 가진 DriverFactory를 구현해 준다.
import app.cash.sqldelight.db.SqlDriver
import app.cash.sqldelight.driver.native.NativeSqliteDriver
import com.test.Database
actual class DriverFactory() {
actual fun createDriver(): SqlDriver {
return NativeSqliteDriver(Database.Schema,"todo.db")
}
}
이제 다시 CommonMain으로 이동하여 두 플랫폼에서 사용할 AppDatabase class를 구현해보자
import com.test.Database
import com.test.TODOItem
class AppDataBase(driverFactory: DriverFactory) {
private val driver = driverFactory.createDriver()
private val database = Database(driver)
private val queries = database.toDoItemQueries
suspend fun insertItem(title: String) {
queries.insert(null, title, false)
}
suspend fun deleteItem(id: Long) {
queries.deleteById(id)
}
suspend fun updateCheck(checked: Boolean, id: Long) {
queries.updateFinish(checked, id)
}
suspend fun getAllItems(): List<TODOItem> {
return queries.selectAll().executeAsList()
}
}
이제 두플랫폼에서 사용할 db 생성이 완료되었다.
이제 생성된 DB를 가지고
이렇게 4가지 기능을 가진 앱을 만들어 보자
전체 코드는 GIT으로 업로드 하였다.
UI 관련 코드는 생략하였고, 구현자체를 목표로 하였기때문에 단순히 입력 삭제 출력기능만 있다.
Android
class MainActivity : ComponentActivity() {
private lateinit var appDataBase: AppDataBase
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
//appDataBase 설정
appDataBase = AppDataBase(DriverFactory(this))
setContent {
MyApplicationTheme {
//처음 값 빈값
var itemList: List<TODOItem> by remember {
mutableStateOf(emptyList())
}
//시작 하면서 값 갱신
LaunchedEffect(true) {
itemList = appDataBase.getAllItems()
}
//scope 용
val scope = rememberCoroutineScope()
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colors.background
) {
ToDoView(
itemList,
addAction = { title ->
scope.launch {
appDataBase.insertItem(title)
itemList = appDataBase.getAllItems()
}
},
deleteAction = { id ->
scope.launch {
appDataBase.deleteItem(id)
itemList = appDataBase.getAllItems()
}
},
checkToggle = { id, checked ->
scope.launch {
appDataBase.updateCheck(checked, id)
itemList = appDataBase.getAllItems()
}
}
)
}
}
}
}
}
struct ContentView: View {
//appDataBase 설정
let appDataBase : AppDataBase = AppDataBase(driverFactory: DriverFactory())
// 처음값 빈값
@State var itemList : [TODOItem] = []
@State var fieldText = ""
var body: some View {
VStack{
HStack {
TextField("enter TODO Title", text: $fieldText)
Spacer()
Button("ADD") {
appDataBase.insertItem(title: fieldText) { error in
updateItem(error: error) {
fieldText = ""
}
}
}
}.padding(10)
ForEach(itemList,id:\.self) { item in
ToDoRow(item: item) {
appDataBase.deleteItem(id: item.id) { error in
updateItem(error: error)
}
} updateToggle: {
appDataBase.updateCheck(checked: !item.isFinish, id: item.id) { error in
updateItem(error: error)
}
}
}
Spacer()
}.onAppear {
//뷰가 보여질때 업데이트 시작
updateItem(error:nil)
}
}
func updateItem(error: Error?,otherAction :@escaping ()->Void = {}) {
if let error = error {
print(error)
} else {
appDataBase.getAllItems { list, error in
if let itemList = list {
self.itemList = itemList
otherAction()
}
}
}
}
}
하지만 위와같이 매번 업데이트 하기는 귀찮고 볼륨이 커지면 업데이트를 누락할수 있기때문에 Flow를 이용하여 collect한 값을 이용하는게 좋을것이다.
다음시간에는 해당 프로젝트를 업데이트 하여 보겠다.