저번에는 Ktor를 이용해 API 호출 하는 법을 배웠다.
이번에는 내부에 데이터를 저장할 수 있는 DataStore를 사용하는 방법을 알아보자
DataStore는 Android Jetpack 라이브러리 중 하나로, 앱에서 데이터를 안전하고 효율적으로 저장하기 위해 구글이 SharedPreferences의 대안으로 만든 솔루션
일반적으로 안드로이드를 사용하는 개발자라면 DataStore를 많이 사용해봤을 것이다.
예로 로그인을 한 경우 accessToken을 저장할 때 주로 DataStore를 이용해 데이터를 저장한다.
이번에는 KMP에서 안드로이드 뿐만 아니라 IOS, Desk에서 저장하는 방법을 알아보자.
참고로 아래 유튜브 영상과 공식 문서를 이용해 프로젝트를 만들었다.
[versions]
datastore = "1.1.7"
[libraries]
datastore = { module = "androidx.datastore:datastore", version.ref = "datastore" }
datastore-preferences = { module = "androidx.datastore:datastore-preferences", version.ref = "datastore" }
composeApp의 gradle파일에 아래와 같이 의존성 주입
kotlin {
//...
sourceSets {
//...
//공통 부분에서 dataStore 의존성 추가
//api를 사용하면 관련 종속된 부분도 해당 dataStore를 사용할 수 있다.
commonMain.dependencies {
implementation(compose.runtime)
implementation(compose.foundation)
implementation(compose.material3)
implementation(compose.ui)
implementation(compose.components.resources)
implementation(compose.components.uiToolingPreview)
implementation(libs.androidx.lifecycle.viewmodelCompose)
implementation(libs.androidx.lifecycle.runtimeCompose)
api(libs.datastore.preferences)
api(libs.datastore)
}
commonTest.dependencies {
implementation(libs.kotlin.test)
}
jvmMain.dependencies {
implementation(compose.desktop.currentOs)
implementation(libs.kotlinx.coroutinesSwing)
}
}
}
//..
//공통모듈에는 아래와 같이 Path의 정보를 받는 파라미터를 만들고 PreferenceDataStoreFactory.createWithPath를 이용해 DataStore 생성
fun createDataStore(producePath: () -> String): DataStore<Preferences> {
return PreferenceDataStoreFactory.createWithPath(
produceFile = { producePath().toPath() }
)
}
internal const val DATA_STORE_FILE_NAME = "prefs.preferences_pb"
//Daat
fun createDataStore(context: Context): DataStore<Preferences> {
return createDataStore {
context.filesDir.resolve(DATA_STORE_FILE_NAME).absolutePath
}
}
여기서 resolve 함수는 filesDir위치에서 DATA_STORE_FILE_NAME을 가리키며 최종적으로 절대 경로 값을 반환한다.
context.filesDir → 앱 내부 저장소의 /files 디렉토리 File 객체
.resolve("name") → 그 경로 아래의 "name" 파일/디렉토리를 가리키는 File 객체 생성
실제 파일이 생성되는 건 아니고, 단순히 경로를 나타내는 객체를 반환할 뿐임.
@OptIn(ExperimentalForeignApi::class)
fun createDataStore(): DataStore<Preferences> {
return createDataStore {
val directory = NSFileManager.defaultManager.URLForDirectory(
directory = NSDocumentationDirectory,
inDomain = NSUserDomainMask,
appropriateForURL = null,
create = false,
error = null,
)
requireNotNull(directory).path + "/$DATA_STORE_FILE_NAME"
}
}
IOS에서 경로를 가져오는 방법은 위와 같다. 해당 구현 부분은 공식 문서에서 확인이 가능하다.
//공식 문서에서는 아래와 같이 임시 파일이 만들어진다고 한다.
/*
참고: System.getProperty("java.io.tmpdir")는 시스템의 임시 폴더를 가리키며, 이 폴더는 재부팅 시 삭제될 수 있습니다.
macOS에서는 ~/Library/Application Support/[your-app] 폴더를 대신 사용할 수 있습니다.
**/
fun createDataStore(): DataStore<Preferences> = createDataStore(
producePath = {
val file = File(System.getProperty("java.io.tmpdir"), dataStoreFileName)
file.absolutePath
}
)
---
//기존 commonMain에 있는 createDataStore
//이럴 경우 기본 안드로이드 프로젝트에 DATA_STORE_FILE_NAME이 생성된다.
val prefs = createDataStore {
DATA_STORE_FILE_NAME
}
위 코드는 공식 내용이며 아래는 영상에 있는방법이다.
간단하게 사용하기 위해 프로젝트에서 파일이 생성되는 방법을 사용하자.
UI는 단순하게 버튼을 누르면 카운터가 증가하는 방식이다.
여기서 counter를 DataStore에 있는 내부 정보를 가져와서 보여주는 방식으로 구현했다.
@Composable
@Preview
fun App(
prefs: DataStore<Preferences>
) {
val counterKey = intPreferencesKey("counter")
val counter by prefs.data
.map { it[counterKey] ?: 0 }
.collectAsState(0)
val scope = rememberCoroutineScope()
MaterialTheme {
Column(
modifier = Modifier
.background(MaterialTheme.colorScheme.primaryContainer)
.safeContentPadding()
.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text(
text = counter.toString(),
textAlign = TextAlign.Center,
fontSize = 50.sp
)
Button(onClick = {
scope.launch {
prefs.edit { pref ->
pref[counterKey] = counter + 1
}
}
}) {
Text("Increment!")
}
}
}
}
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge()
super.onCreate(savedInstanceState)
setContent {
App(
prefs = remember {
createDataStore(applicationContext)
}
)
}
}
}
fun MainViewController() = ComposeUIViewController {
App(
prefs = remember {
createDataStore()
}
)
}
fun main() {
val prefs = createDataStore {
DATA_STORE_FILE_NAME
}
application {
Window(
onCloseRequest = ::exitApplication,
title = "DataStoreEx",
) {
App(
prefs = prefs
)
}
}
}
둘 다 정상적으로 데이터가 저장되는 것을 볼 수 있다.
데스크톱도 정상적으로 동작이 되며 저장된 데이터는 프로젝트 파일 위치에 생성되는 것을 볼 수 있다.
