[Android] DataStore

민채·2024년 2월 18일
0

Android

목록 보기
8/16

지금까지 토큰이나 간단한 데이터를 저장할 때 SharedPreferences를 이용해 저장했다.
그런데 이제 SharedPreferences말고 DataStore를 이용해 데이터를 저장하는 걸 권장하고 있어서 이에 대해 공부하고 남기는 글!

처음 써봐서 제대로 적용한 건지 헷갈리지만 차차 더 깊게 공부해 봐야겠다

DataStore

  • 프로토콜 버퍼(구조화된 데이터를 직렬화 하는 메커니즘)를 사용하여 Key-value타입 혹은 유형이 지정된 객체를 로컬에 저장할 수 있는 기능
  • Preferences Datastore 와 Proto Datastore두가지 방법의 데이터 저장 기법을 제공
  • Corouine + Flow를 사용할 수 있어 비동기적이고 일관적인 트랜잭션 방식으로 데이터를 저장

Preferences DataStore

  • 기존의 SharedPreference 와 비슷한 저장 방법
  • 키-값 형태로 데이터가 저장되며 값은 숫자, 문자열등의 기초 데이터
  • 타입 안전성을 제공하지 않으며 사전 정의된 스키마가 필요하지 않음

Proto DataStore

  • 구조화된 데이터를 저장할 수 있으며 타입 안정성을 제공
  • 프로토콜 버퍼를 사용하여 스키마를 정의해야 함

Preferences DataStore 사용법

build.gradle에 추가

dependencies {
	implementation "androidx.datastore:datastore-preferences:1.0.0"
}

DataStore 클래스 생성

일단 나는 DataStore 클래스를 따로 만들었다.

class EdiyaDataStore(private val context : Context) {

    private val Context.dataStore  by preferencesDataStore(name = "dataStore")

    private val STAMP_MAIN_FIRST = booleanPreferencesKey(PREF_STAMP_MAIN_FIRST)
}
  • DataStore에서 사용하는 키 값은 사용할 타입PreferencesKey("keyName")과 같은 형태로 선언
  • 위의 예시는 boolean 값을 저장
  • String, int 타입을 저장하려면 stringPreferencesKey, intPreferencesKey 사용

데이터 저장

  • 데이터를 쓰기 위해선 edit()를 사용
  • 해당 작업은 비동기적으로 동작하므로 suspend 키워드를 통해 Coroutine 영역에서 동작할 수 있도록 해야 함
suspend fun setStampMainFirst(value: Boolean) {
	context.dataStore.edit { preferences ->
		preferences[STAMP_MAIN_FIRST] = value

	}
}
  • 다음과 같이 호출하여 데이터를 저장
EdiyaApplication.appApplication.getDataStore().setStampMainFirst(true)

데이터를 읽는 Flow 생성

val stampMainFirst: Flow<Boolean> = context.dataStore.data
    	// catch()를 사용하여 데이터 읽는 과정에서 문제가 생겼을 때 예외처리
        .catch { exception ->
            if (exception is IOException) {
                emit(emptyPreferences())
            } else {
                throw exception
            }
        }
        .map { preferences ->
            preferences[STAMP_MAIN_FIRST] ?: false
        }
  • 코루틴의 Flow를 사용하여 DataStore에서 데이터를 읽어올 때 해당 데이터를 Flow객체로 전달
  • map()을 활용하여 STAMP_MAIN_FIRST에 대응하는 Value를 Flow 형태로 가져옴
  • catch()를 사용하여 데이터 읽는 과정에서 문제가 생겼을 때 예외처리

전체 코드

DataStore.kt

import android.content.Context
import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.emptyPreferences
import androidx.datastore.preferences.preferencesDataStore
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.map
import java.io.IOException

class EdiyaDataStore(private val context : Context) {

    private val Context.dataStore  by preferencesDataStore(name = "dataStore")

    private val STAMP_MAIN_FIRST = booleanPreferencesKey(PREF_STAMP_MAIN_FIRST) // 스탬프 메인 진입 여부
    
    // 스탬프 진입 값 갱신
    suspend fun setStampMainFirst(value: Boolean) {
        context.dataStore.edit { preferences ->
            preferences[STAMP_MAIN_FIRST] = value

        }
    }

    val stampMainFirst: Flow<Boolean> = context.dataStore.data
        .catch { exception ->
            if (exception is IOException) {
                emit(emptyPreferences())
            } else {
                throw exception
            }
        }
        .map { preferences ->
            preferences[STAMP_MAIN_FIRST] ?: false
        }
}

DataStore는 싱글톤으로 관리되어야 함 -> Application에서 생성

Application.kt

private lateinit var dataStore : EdiyaDataStore

fun getDataStore() : EdiyaDataStore = dataStore

Fragment 코드

// Flow형태로 값을 받아오기 때문에 값 변경이 있어야만 동작함
// 동작의 변경이 아닌 원하는 타이밍에 값을 가져오고 싶다면 first()함수를 이용해 값을 호출
    
val dataStore = EdiyaApplication.appApplication.getDataStore()

// 비동기 방식
lifecycleScope.launch {
    // Flow형태로 값을 받아오기 때문에 값 변경이 있어야만 동작함
    // 동작의 변경이 아닌 원하는 타이밍에 값을 가져오고 싶다면 first()함수를 이용해 값을 호출
   if (dataStore.stampMainFirst.first() && userInfo.dueToExpireStampCnt > 0) {
       dataStore.setStampMainFirst(false)
       ExpireStampDialog(mActivity, userInfo.dueToExpireStampCnt) // 소멸예정 스탬프 바텀시트 -> 최초 진입(오늘 + 앱실행 + 스탬프 서브메인 처음 진입), 소멸예정 스탬프 있을 경우
   }
}

// 동기 방식
// 다만, runBlocking 안에서 무거운 작업은 피해야 함 -> 블로킹 되는 시간이 길어짐에 따라 프레임 드랍으로 인한 화면 버벅임(16ms), ANR(5s)로 이어질 수 있음
runBlocking {
    if (dataStore.stampMainFirst.first() && userInfo.dueToExpireStampCnt > 0) {
        dataStore.setStampMainFirst(false)
        ExpireStampDialog(mActivity, userInfo.dueToExpireStampCnt)
    }
}

// flow를 liveData로 바꿔서 observe하고 싶은 경우
dataStore.stampMainFirst.asLiveData().observe(this) {
	...
}

참조

profile
코딩계의 떠오르는 태양☀️

0개의 댓글