Development record of developer who study hard everyday.

레이블이 안드로이드datastore인 게시물을 표시합니다. 모든 게시물 표시
레이블이 안드로이드datastore인 게시물을 표시합니다. 모든 게시물 표시
, , , , ,

안드로이드 ProtoDataStore 사용법 및 예제

 안드로이드 ProtoDataStore 사용법 및 예제

안드로이드 개발 블로그

지난 글에 이어서 DataStore에 대해 공부해보자.

지금 다니고있는 회사가 에이전시회사라서 옛날 프로젝트를 유지보수해야하는 일들이 생긴다.

그럴 때 보면 내가 처음 보는 라이브러리가 있는데, 그 중 하나가 objectBox다.

정확히 살펴보지는 않았지만 대략 검색해보면, 객체 자체를 앱에 저장하는 라이브러리 같았다.

이와 비슷한 구글 공식 라이브러리가 있다.

바로, Proto DataStore이다.


1. dependency 추가

app 수준의 build.gradle에 아래와 같이 종속성 추가를 해준다.

dependencies {
//prefer datastore
implementation "androidx.datastore:datastore-preferences:1.0.0"

//proto DataStore

implementation("androidx.datastore:datastore-core:1.0.0")
implementation "com.google.protobuf:protobuf-javalite:3.18.0"
    //coroutine
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.5.1")
implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.5.1")
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1")

}

app 수준의 build.gradle 맨 아래에 아래처럼 설정을 해준다.

protobuf {
protoc {
artifact = "com.google.protobuf:protoc:3.14.0"
}

// Generates the java Protobuf-lite code for the Protobufs in this project. See
// https://github.com/google/protobuf-gradle-plugin#customizing-protobuf-compilation
// for more information.
generateProtoTasks {
all().each { task ->
task.builtins {
java {
option 'lite'
}
}
}
}
}

app 수준의 build.gradle에 plugin을 추가해준다.

plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
id "com.google.protobuf" version "0.8.12" //추가
}


2. 스키마 정의

간단히 설명하자면, 내가 저장할 객체의 클래스를 정의해준다고 생각하면 쉽다.

이때, Protobuf(프로토콜 버퍼)를 사용하는데, 구글에서 만든 라이브러리라고 생각하면 된다.

다양한 언어로 객체의 타입을 정의하는데 장점이 많은 라이브러리다.

https://developers.google.com/protocol-buffers/docs/proto3?hl=ko

자세히 알고 싶은 사람들은 위 공식문서를 참고하자.


proto 파일 저장경로

먼저, 위 캡쳐화면처럼 app/src/main/proto 디렉터리에 .proto 파일을 만들어준다.

디렉토리 만드는 방법은 알거 같고 파일을 만들 때는 오른쪽 버튼 눌러서 New - File 을 누르면 된다.

이제, 스키마를 아래와 같이 정의한다.

syntax = "proto3";    //1

option java_package = "com.boo.sample.sampledatastore";    //2
option java_multiple_files = true;

message Person{    //3
string firstName = 1;
string lastName = 2;
int32 age = 3;
}

1=> proto3 문법을 따른다

2=> 패키지명 적는다.

3=> 객체 정의

proto 문법은 위에 남긴 proto 공식 문서를 참고하길 바란다.


3. Serializer 정의

class MySerializer: Serializer<Person> {    //1
override val defaultValue: Person    //2
get() = Person.getDefaultInstance()

override suspend fun readFrom(input: InputStream): Person {    //3
try {
return Person.parseFrom(input)
} catch (exception: InvalidProtocolBufferException) {
throw CorruptionException("Cannot read proto.", exception)
}
}

override suspend fun writeTo(t: Person, output: OutputStream) {    //4
t.writeTo(output)
}
}

val Context.personDataStore: DataStore<Person> by dataStore(    //5
fileName = "my_data",    //6
serializer = MySerializer()    //7
)

1=> Serializer 클래스는 데이터 유형을 읽고 쓰는 방법을 DataStore에 알린다.

2=> 아직 파일이 생성되지 않은 경우 사용할 Serializer의 기본값을 포함해야한다.

3=> DataStore로부터 데이터를 읽어온다. 역시나 suspend 함수다.

4=> DataStore에 데이터를 쓴다.

5=> dataStore로 만든 속성을 위임하여 DataStore 객체를 만든다.

6=> fileName 매개변수는 데이터를 저장하는데 사용할 파일을 DataStore에 알린다.

7=> serializer 매개변수는 정의한 serializer 클래스 이름을 DataStore에 알린다.


4. activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">

<data>

</data>

<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">

<EditText
android:id="@+id/editTextTextPersonName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:ems="10"
android:inputType="textPersonName"
android:hint="Proto 입력창"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
/>


<Button
android:id="@+id/save_btn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:text="Save"
app:layout_constraintEnd_toEndOf="@+id/editTextTextPersonName"
app:layout_constraintStart_toStartOf="@+id/editTextTextPersonName"
app:layout_constraintTop_toBottomOf="@+id/editTextTextPersonName" />

<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:textSize="20sp"
app:layout_constraintEnd_toEndOf="@+id/save_btn"
app:layout_constraintStart_toStartOf="@+id/save_btn"
app:layout_constraintTop_toBottomOf="@+id/save_btn"
tools:text="result_proto"/>

</androidx.constraintlayout.widget.ConstraintLayout>
</layout>


5. ProtoRepository 정의

class ProtoRepository(val context: Context) {
val TAG = this::class.java.simpleName

val readProto: Flow<Person> = context.personDataStore.data    //1
.catch { exception ->
if(exception is IOException) {
Log.d(TAG, exception.message.toString())
emit(Person.getDefaultInstance())
} else {
throw exception
}
}

suspend fun updateValue(firstName: String) {    //2
context.personDataStore.updateData { preference ->
preference.toBuilder().setFirstName(firstName).build()
}
}
}

1=> DataStore.data를 사용하여 저장된 객체에서 적절한 속성의 Flow를 노출합니다.

2 => Proto Datastore는 저장된 객체를 트랜잭션 방식으로 업데이트하는 updateData() 함수를 제공합니다. 


6. MainViewModel 정의

class MainViewModel(application: Application): AndroidViewModel(application) {
private val repository = ProtoRepository(application)

val firstName = repository.readProto.asLiveData()

fun updateValue(firstName : String) = viewModelScope.launch(Dispatchers.IO) {
repository.updateValue(firstName)
}
}

DataStore에서 쓰기 과정은 코루틴을 사용하여 비동기적으로 실행된다.

✋ asLiveData() 사용하기위해서 build.gradle(앱수준)에 아래와 같이 dependency를 추가한다

implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.5.1")


7. MainActivity 정의

class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private lateinit var userManager: UserManager
private var age = -1
private var frontName = ""
private var lastName = ""
private var gender = ""

private lateinit var mainViewModel: MainViewModel

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

binding = DataBindingUtil.setContentView(this, R.layout.activity_main)

mainViewModel = ViewModelProvider(this)[MainViewModel::class.java]    //1
mainViewModel.firstName.observe(this) {   //2
binding.textView.text = it.firstName
}

binding.saveBtn.setOnClickListener {    //3
val firstName = binding.editTextTextPersonName.text.toString()
mainViewModel.updateValue(firstName)
}
}

}

1=> mainViewModel 객체 가져오기

2=> DataStore에 저장된 값 관찰

3=> DataStore에 값 저장하기






Share:
Read More
, , , ,

안드로이드 Preferences DataStore 사용법과 예제

 안드로이드 Preferences DataStore 사용법과 예제


안드로이드 개발 블로그

 MVVM 패턴을 공부하면서 함께 적용할 Jetpack Architecture에 대해 공부하고 있다.

왠만한건 한 번씩 다 봤지만, DataStore란 녀석은 본 적이 없어서 이번 기회에 제대로 공부하기로 했다.

앱이 종료되어도 저장되어야할 작은 데이터는 주로 SharedPreferences를 사용해왔다.

하지만 이제는 구글에서 DataStore로 이전하기를 권장하고있다.

SharedPreferences의 한계는 다음과 같다.

1. UI스레드에서 호출할 수 있도록 API가 설계되었지만, UI스레드를 블로킹해 ANR을 발생시킬 수 있다.

2. 다중스레드 환경에서 안전하지 않다.

3. type safety가 보장되지 않는다.

이러한 문제를 해결하기위해 DataStore가 만들어졌다.

그럼, 예제를 함께 살펴보자.

1. dependency 추가

dependencies {
//prefer datastore
implementation "androidx.datastore:datastore-preferences:1.0.0"

}

app수준의 build.gradle에 위와같이 종속성 추가를 해준다.


2. Preferences DataStore 만들기

UserManager.kt 파일을 만들고 전역 프로퍼티로 아래의 코드를 추가해준다.

val Context.dataStore : DataStore<Preferences> by preferencesDataStore(name = "user_prefs")

dataStore를 싱글톤으로 만들어서 DataStore<Preferences> 객체를 만듭니다.

name 매개변수는 Preferences DataStore의 이름입니다.


3. Preferences DataStore에서 읽고 쓰기

class UserManager(
private val dataStore: DataStore<Preferences>
) {
companion object {    //1
val USER_AGE_KEY = intPreferencesKey("USER_AGE")
val USER_FIRST_NAME_KEY = stringPreferencesKey("USER_FIRST_NAME")
val USER_LAST_NAME_KEY = stringPreferencesKey("USER_LAST_NAME")
val USER_GENDER_KEY = booleanPreferencesKey("USER_GENDER")
}
    
suspend fun storeUSer(
age: Int,
frontName: String,
lastName: String,
isMale: Boolean,
) {
dataStore.edit {    //2
it[USER_AGE_KEY] = age
it[USER_FIRST_NAME_KEY] = frontName
it[USER_LAST_NAME_KEY] = lastName
it[USER_GENDER_KEY] = isMale
}
}
    //3
val userAgeFlow: kotlinx.coroutines.flow.Flow<Int?> = dataStore.data.map {
it[USER_AGE_KEY]
}

val userFirstNameFlow: kotlinx.coroutines.flow.Flow<String?> = dataStore.data.map {
it[USER_FIRST_NAME_KEY]
}

val userLastNameFlow: kotlinx.coroutines.flow.Flow<String?> = dataStore.data.map {
it[USER_LAST_NAME_KEY]
}

val userGenderFlow: kotlinx.coroutines.flow.Flow<Boolean?> = dataStore.data.map {
it[USER_GENDER_KEY]
}
}

1=> 키값 모음

예를 들어, int값의 키를 정의하려면 intPreferencesKey()를 사용한다.


2=> Preferences DataStore에 값 쓰기

SharedPreferences와 비슷하게 edit() 함수를 사용하여 값을 쓴다.

참고로 DataStore는 알아서 코루틴을 사용하여 값을 저장시킨다.

따라서 suspend 함수로 선언한다.


3=> Preferences DataStore에서 값 읽기

DataStore는 값을 저장하거나 읽어올 때 코루틴을 사용하여 Flow로 결과를 전달한다.

DataStore.data 속성을 사용하여 Flow를 받고 map을 이용하여 값을 읽어왔다.


4. activity_main.xml 구성

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">

<data>

</data>

<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">

<Button
android:id="@+id/btn_save"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:padding="18dp"
android:text="Save user"
android:textColor="@android:color/white"
android:textSize="15sp"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="@+id/switch_gender"
app:layout_constraintStart_toStartOf="@+id/switch_gender"
app:layout_constraintTop_toBottomOf="@+id/switch_gender" />

<EditText
android:id="@+id/et_lname"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:ems="10"
android:hint="이름"
app:layout_constraintEnd_toEndOf="@+id/et_fname"
app:layout_constraintStart_toStartOf="@+id/et_fname"
app:layout_constraintTop_toBottomOf="@+id/et_fname" />

<EditText
android:id="@+id/et_age"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:ems="10"
android:hint="나이"
android:inputType="number"
app:layout_constraintEnd_toEndOf="@+id/et_lname"
app:layout_constraintStart_toStartOf="@+id/et_lname"
app:layout_constraintTop_toBottomOf="@+id/et_lname"
tools:layout_editor_absoluteY="317dp" />

<EditText
android:id="@+id/et_fname"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:ems="10"
android:hint="나머지 이름"
app:layout_constraintEnd_toEndOf="@+id/tv_gender"
app:layout_constraintStart_toStartOf="@+id/tv_gender"
app:layout_constraintTop_toBottomOf="@+id/tv_gender"
tools:layout_editor_absoluteY="178dp" />

<TextView
android:id="@+id/tv_fname"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:textColor="@android:color/black"
android:textSize="20sp"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="firstName"/>

<TextView
android:id="@+id/tv_lname"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:textColor="@android:color/black"
android:textSize="20sp"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="@+id/tv_fname"
app:layout_constraintStart_toStartOf="@+id/tv_fname"
app:layout_constraintTop_toBottomOf="@+id/tv_fname"
tools:text="lastName"/>

<TextView
android:id="@+id/tv_age"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:textColor="@android:color/black"
android:textSize="20sp"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="@+id/tv_lname"
app:layout_constraintStart_toStartOf="@+id/tv_lname"
app:layout_constraintTop_toBottomOf="@+id/tv_lname"
tools:text="age"/>

<TextView
android:id="@+id/tv_gender"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:textColor="@android:color/black"
android:textSize="20sp"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="@+id/tv_age"
app:layout_constraintStart_toStartOf="@+id/tv_age"
app:layout_constraintTop_toBottomOf="@+id/tv_age" />

<androidx.appcompat.widget.SwitchCompat
android:id="@+id/switch_gender"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="성별"
android:textSize="20sp"
app:layout_constraintEnd_toEndOf="@+id/et_age"
app:layout_constraintStart_toStartOf="@+id/et_age"
app:layout_constraintTop_toBottomOf="@+id/et_age" />


</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

데이터바인딩을 사용했다.(세팅 방법은 생략한다.)

간단하게 설명하면, editText와 switch에 firstName, lastName, 나이, 성별을 입력하고 저장버튼을 누르면 DataStore에 저장한다.

그리고 TextView에는 저장된 데이터를 바로 보여준다.


5. MainActivity.kt


class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private lateinit var userManager: UserManager
private var age = -1
private var frontName = ""
private var lastName = ""
private var gender = ""

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

binding = DataBindingUtil.setContentView(this, R.layout.activity_main)

userManager = UserManager(dataStore)

binding.run {
buttonSave()
observeData()
}
}

private fun ActivityMainBinding.buttonSave() {
btnSave.setOnClickListener {
frontName = etFname.text.toString()
lastName = etLname.text.toString()
age = etAge.text.toString().toInt()
val isMale = switchGender.isChecked

CoroutineScope(Dispatchers.IO).launch {
userManager.storeUSer(age, frontName, lastName, isMale)
}
}
}

private fun observeData() {
userManager.userAgeFlow.asLiveData().observe(this) {
if(it != null) {
age = it
binding.tvAge.text = it.toString()
}
}

userManager.userFirstNameFlow.asLiveData().observe(this) {
if (it != null) {
frontName = it
binding.tvFname.text = it
}
}

userManager.userLastNameFlow.asLiveData().observe(this) {
if (it != null) {
lastName = it
binding.tvLname.text = it
}
}

userManager.userGenderFlow.asLiveData().observe(this) {
if (it != null) {
gender = if (it) "남성" else "여성"
binding.tvGender.text = gender
}
}
}
}

observeData() 함수에 보면 Flow를 LiveData로 변환시켜서 관찰하는데 

implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.5.1")

위 종속성을 추가해주어야 asLiveData() 메소드를 사용가능하다.



Share:
Read More