안드로이드에서 단순한 데이터셋을 로컬에 저장하기 위해서 흔히 기본적으로 내장되어 있는 SharedPreferences를 사용한다.
하지만 Primitive data만 저장 가능했으며, 커스텀 데이터 타입 저장을 위해 GSON을 통해 Json String으로 변환하여 저장하는 코드들이 생겨났다.
그리고 미숙한 처리에 종종 Runtime Error, ANR이 날 수도 있다.
SharedPreferences 단점
- 실제 XML 파일 I/O 작업을 하는 것으로 UI Thread에서 작업할 경우 안전하지 않음
- Runtime Exception으로부터 안전하지 않음
- XML 파일이기에 외부에서 쉽게 파일을 읽을 수 있음 (DataStore도 읽을 수는 있음)
- 비동기 API를 제공하지만 Listener를 통해서만 값을 Read 할 수 있음
- Type-Safety 미제공
아래에서 설명할 DataStore는 SharedPreferences를 대체하기 위해 Jetpack에서 발표한 라이브러리이다.
DataStore
Kotlin coroutine과 Flow를 사용하여 비동기적으로, 일관되게 데이터를 저장이 가능
Preferences DataStore
- SharedPreferences 처럼 key-value 형태로 저장
- Type-Safety를 제공 X
Proto DataStore
- 커스텀 데이터 타입을 Protocol buffer를 통하여 저장
- 미리 정의된 schema를 통하여 Type-Safety를 보장
비교
아래 표는 SharedPreferences와 DataSore의 비교표로 왜 DataStore를 사용해야 하는지 알 수 있다.
SharedPreference vs DataStore
PreferencesDataStore 구현
build.gradle (app)
dependencies {
implementation("androidx.datastore:datastore-preferences:<latest_version>")
//Multi Module Ver.
//implementation("androidx.datastore:datastore-preferences-core:<latest_version>")
}
Data package나 쓰일 곳에 User 파일을 만들고 DataStore를 생성하는 코드를 넣는다
by 키워드를 써서 DataStore의 구현을 preferencesDataStore()에 맡기는 Context 확장 프로퍼티
import android.content.Context
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.preferencesDataStore
val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "user_prefs")
만든 dataStore를 관리할 class 생성
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.*
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
class UserManager(private val dataStore: DataStore<Preferences>) {
companion object {
val USER_AGE_KEY = intPreferencesKey("USER_AGE")
val USER_NAME_KEY = stringPreferencesKey("USER_LAST_NAME")
val USER_GENDER_KEY = booleanPreferencesKey("USER_GENDER")
}
suspend fun storeUser(age: Int, lastName: String, isMale: Boolean) {
dataStore.edit {
it[USER_AGE_KEY] = age
it[USER_NAME_KEY] = lastName
it[USER_GENDER_KEY] = isMale
}
}
val userAgeFlow: Flow<Int?> = dataStore.data.map { it[USER_AGE_KEY] }
val userNameFlow: Flow<String?> = dataStore.data.map { it[USER_NAME_KEY] }
val userGenderFlow: Flow<Boolean?> = dataStore.data.map { it[USER_GENDER_KEY] }
}
내부적으로 코루틴을 사용하기 때문에 데이터를 저장하는 storeUser()의 앞에 suspend 키워드를 붙임
intPreferencesKey(), stringPreferencesKey() 등의 함수가 보이는데 이것은 저장될 값들의 키를 정의하는 함수
타입에 맞는 함수를 쓰고 그 안에 키로 사용할 문자열을 넣어줌, 같은 이름의 키가 여러 개 있다면 ClassCastException 발생
버튼을 누르면 입력된 값들을 DataStore에 저장하고, 저장된 값이 바뀌면 이를 관찰해서 View에 사용
class PreferencesActivity : AppCompatActivity() {
private lateinit var userManager: UserManager
override fun onCreate(savedInstanceState: Bundle?) {
...
userManager = UserManager(dataStore)
btnSave.setOnClickListener{
CoroutineScope(Dispatchers.IO).launch { userManager.storeUser(임시나이, 이름, 성별) }
}
btnClear.setOnClickListener{
CoroutineScope(Dispatchers.IO).launch { userManager.clearUser() }
}
observeData()
}
private fun observeData() = lifecycleScope.launch {
launch {
userManager.userAgeFlow.collectLatest { val age = it }
}
launch {
userManager.userNameFlow.collectLatest { val name = it }
}
launch {
userManager.userGenderFlow.collectLatest { val genderBool = it }
}
}
}
ProtoDataStore 구현
ProtoDataStore를 구현할 때 Protocol Buffer도 구현해야 한다.
Protocol Buffer
Google에서 개발한 이진 직렬화 포맷
플랫폼 및 언어에 독립적이며, 다양한 언어와 시스템에서 데이터를 효율적으로 전송하기 위해 사용
주요 장점은 작은 메시지 크기, 빠른 직렬화/역직렬화 속도 및 강력한 타입 안정성을 제공
build.gradle (project) / 작성자는 Kts 환경 (build.gradle.kts)
plugins {
...
id("com.google.protobuf") version "<latest_version>" apply false
}
build.gradle (app) / Protobuf 플러그인 및 라이브러리 의존성 추가
plugins {
...
id("com.google.protobuf")
}
protobuf {
protoc {
artifact = "com.google.protobuf:protoc:3.25.1"
}
generateProtoTasks {
all().forEach { task ->
task.builtins{
id("java"){ option("lite") }
}
}
}
}
dependencies {
...
implementation("androidx.datastore:datastore:<latest_version>")
//Multi Module Ver.
//implementation("androidx.datastore:datastore-core:<latest_version>")
implementation("com.google.protobuf:protobuf-javalite:<latest_version>")
}
프로젝트 내에서 .proto 파일을 작성 ( 작성자는 src\main\proto 디렉토리 생성 후 Sample.proto 작성 )
syntax = "proto3";
option java_package = "com.ljb.datastore";
option java_multiple_files = true;
message Sample {
int32 age = 1;
string name = 2;
enum Gender {
MALE = 0;
FEMALE = 1;
}
Gender gender = 3;
bool initData = 4;
}
- syntax : 사용할 Protobuf 버전을 지정하는 부분, protocol buffer 3 버전을 사용한다는 것을 명시
- java_package : 클래스가 생성될 package 명 명시 (ex:전체 패키지 이름)
- java_multiple_files : 최상위 수준인 클래스, enum에 해당하는 자바 클래스, enum 파일 등을 별도의 파일로 분리할지를 결정하는 항목, 자세한 건 https://protobuf.dev/programming-guides/proto3/
- message : 데이터 구조 정의, 자료형을 선언, 그 안에 적절한 멤버 변수를 정의, 각 멤버 변수에 붙은 1, 2, ...에 해당하는 값은 해당 멤버 변수에 부여된 고유 값
작성 후 rebuild를 진행해주면 .proto 파일에서 정의한 자료형이 Java 클래스로 빌드되고, 이를 활용해 Android Studio 내에서 DataStore를 구현할 수 있게 된다.
Serializer 작성
Proto는 JSON처럼 직렬화된 데이터를 사용, 이를 활용하려면 직렬화/역직렬화 과정이 필요
앱 패키지 내 적절한 경로에 Serializer 생성
ViewModel을 싱글톤으로 쓰는 것과 유사한 이유로 DataStore도 단일 객체로 사용, Serializer도 object로 선언
object SampleSerializer: Serializer<Sample> {
override val defaultValue: Sample
get() = Sample.getDefaultInstance()
override suspend fun readFrom(input: InputStream): Sample {
try {
return Sample.parseFrom(input)
} catch (e: InvalidProtocolBufferException){
throw CorruptionException("Cannot read proto.", e)
}
}
override suspend fun writeTo(t: Sample, output: OutputStream) {
t.writeTo(output)
}
}
Repository
직접적인 데이터 조작은 Repository에서 진행, ViewModel이 Repository에 딸린 Flow를 읽거나 메소드를 호출하는 식으로 간접적으로 데이터에 접근
class SampleRepository(private val sampleProtoDataStore: DataStore<Sample>) {
val flow : Flow<Sample> = sampleProtoDataStore.data
suspend fun setUserData(name:String, age:Int, gender: Sample.Gender, initData: Boolean){
sampleProtoDataStore.updateData { sample ->
sample
.toBuilder()
.setName(name)
.setAge(age)
.setGender(gender)
.setInitData(initData)
.build()
}
}
suspend fun clearUserData(){
sampleProtoDataStore.updateData { sample ->
sample
.toBuilder()
.clearName()
.clearAge()
.clearGender()
.clearInitData()
.build()
}
}
}
생성자 매개 변수 DataStore<T> : .proto에서 정의한 데이터를 직접 읽고 쓸 수 있는 메소드를 제공해 주는 클래스
쓰기 및 수정 접근 : Proto는 .proto에서 지정한 멤버 변수별로 Getter와 Setter를 제공
ViewModel
susfed fun 함수 이므로 viewModelScope 사용
class ProtoViewModel(private val repository: SampleRepository) : ViewModel() {
val flow: Flow<Sample> = repository.flow
fun setUserData(name:String, age:Int, gender: Sample.Gender, initData: Boolean){
viewModelScope.launch { repository.setUserData(name, age, gender, initData) }
}
fun clearUserData(){
viewModelScope.launch { repository.clearUserData() }
}
}
ViewModelFactory에도 Repository를 매개 변수로 넣어줌
class ProtoViewModelFactory(private val repository: SampleRepository) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(ProtoViewModel::class.java)){
@Suppress("UNCHECKED_CAST")
return ProtoViewModel(repository) as T
}
throw IllegalArgumentException("Unknown ViewModel class")
}
}
Activity
최상단에 Repository의 매개 변수 DataStore<T>에 들어갈 변수 생성, 만들어둔 SampleSerializer 지정
- fileName : 로컬에 저장될 Protobuf 파일의 이름
- serializer : 말그대로 Serializer
private val Context.sampleDataStore: DataStore<Sample> by dataStore(
fileName = "sample.pb",
serializer = SampleSerializer
)
class ProtoActivity : AppCompatActivity() {
private lateinit var viewModel: ProtoViewModel
override fun onCreate(savedInstanceState: Bundle?) {
...
viewModel = ViewModelProvider(this,
ProtoViewModelFactory(SampleRepository(sampleDataStore))
)[ProtoViewModel::class.java]
setUi()
observeData()
}
private fun setUi() {
...
btnSave.setOnClickListener {
CoroutineScope(Dispatchers.IO).launch { viewModel.setUserData(이름, 나이, 성별, 초기Init) }
}
btnClear.setOnClickListener {
CoroutineScope(Dispatchers.IO).launch{ viewModel.clearUserData() }
}
}
private fun observeData() = lifecycleScope.launch {
//UI 처리
viewModel.flow.collectLatest { sampleProto ->
val age = sampleProto.age
val name = sampleProto.name
val initData = sampleProto.initData
if (sampleProto.gender == Sample.Gender.MALE){
}
}
}
}
전체코드
https://github.com/wo9374/StudyProject/tree/main/DataStore
Preference, Proto 둘 다 동일 실행 화면
PreferencesDataStore
https://developer.android.com/topic/libraries/architecture/datastore?hl=ko
https://developer.android.com/codelabs/android-proto-datastore?hl=ko#0
https://onlyfor-me-blog.tistory.com/519
ProtoDataStore
https://velog.io/@i_meant_to_be/Proto-Datastore-Jetpack-Compose
Protobuffer
https://protobuf.dev/programming-guides/proto3/
https://velog.io/@heetaeheo/Protobuf
Kts build
https://stackoverflow.com/questions/75025962/configuring-protocol-buffers-0-9-x-with-kotlin-dsl
'Android > Reference' 카테고리의 다른 글
AlarmManager (0) | 2023.12.12 |
---|---|
Notification (0) | 2023.12.09 |
BottomSheet (Persistent, Modal) (0) | 2022.05.02 |
RecyclerView Item에 Animation 주기 (0) | 2022.04.18 |
ItemDecoration / RecyclerView Item 간격 조정 (0) | 2022.04.10 |