KMM- TODO 앱 만들기(1)(with sqlDelight)

WonDDak·2023년 4월 17일

KMM-TODO

목록 보기
1/2

kmm을 이용하여 db를 사용하는 앱을 만드려면 여러방법이 있겠지만 그중에서
android/ios를 지원하는 db인 sqldelight를 소개한다.

설정

KMM 프로젝트를 생성하고 다음 과정을 거쳐 SQLDelight를 설정하자

SQLDelight 추가

shared 모듈의 build.gradle.kts에 다음과 같이 추가한다.

plugin 추가

plugins {
    ...
    id("app.cash.sqldelight") version "2.0.0-alpha05"
}

SQLDelight 설정

//하단 맨 아래에 추가한다.
sqldelight {
  databases {
    // 두앱 android/ios에서 사용할 이름을 Database로 설정
    create("Database") {
        // db 파일의 위치를 com.test 위치에 생성되도록 함
      packageName.set("com.test")
    }
  }
}

dependencies 추가

현재 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")
            }
        }
    }
}

DB 정보 생성

작업하기전에 여기로 이동하여 안드로이드 스튜디오에 플러그인을 추가해주자.

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 관련 파일이 만들어 진다.

db생성

Android Room과 비교하면
1. ToDoItem > Entity
2. ToDoItemQueries > Dao
3. Database > AppDataBase
와 동일한 기능을 한다고 보면된다.


DB 구현

이제 각 앱에서 사용할 DB 모듈을 구현해 보자.

Common

먼저 두 플랫폼에서 기본이될 구현체를 만들자
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처럼 제약을 걸어준다 생각하면 된다.)

Android

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

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

공통 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()
                            }
                        }
                    )
                }
            }
        }
    }
}

ios

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한 값을 이용하는게 좋을것이다.
다음시간에는 해당 프로젝트를 업데이트 하여 보겠다.

profile
안녕하세요. 원딱입니다.

0개의 댓글