์ด ๊ธ์ Hello DataStore, Bye SharedPreferences๐ โ Android๐ฑ โ Part 2: Preference ProtoStore์ ๋ฒ์ญํ ํฌ์คํธ.
์ด์ ์ํฐํด์์๋ Preference DataStore๋ฅผ ์ฌ์ฉํ์ฌ key-value Pair์ ํํ๋ก ๋ฐ์ดํฐ๋ฅผ ์ ์ฅํ๋ ๋ฐฉ๋ฒ์ ์ดํด๋ดค๋ค. ๊ทธ๋ฌ๋ ์ด ๋ฐฉ๋ฒ์ type-safe ํ์ง ์๋ค.
์ด ๊ธ์์๋ ์ด๋ฐ ๋จ์ ์ ์ปค๋ฒํ๊ธฐ ์ํ ๋ฐฉ๋ฒ์ ์ดํด๋ณผ ์์
๋ง์ฝ ์ด์ ๊ธ์ด ๋ณด๊ณ ์ถ๋ค๋ฉด ์ด ๋งํฌ๋ก ์ด๋ํ๋ฉด ๋๋ค.
์์ ์ฝ๋๋ ์ฌ๊ธฐ์ ํ์ธํ ์ ์๋ค.
์์์ ๋ง๊ณผ ์ข ๋ฅ์ ๊ดํ ํํฐ๋ง ๊ธฐ๋ฅ์ ์ ๊ณตํ๋ ์์ ๋ชฉ๋ก์ ๋ณด์ฌ์ฃผ๋ ์ฑ์ ๋ง๋ค์ด๋ณผ ์์ ์ด๋ค.
์ข
๋ฅ๋ ๐ขVEG ์ ๐ดNON-VEG
๋ง์ SWEET ์ SPICY
์ด๋ฐ์์ผ๋ก ๊ฐ๊ฐ ๋ ๊ฐ์ง ์ต์ ์ ์ ๊ณตํ๋ค. ์๋๋ ๋ฐ๋ชจ

์ฒซ ๋ฒ์งธ ํด์ผํ ์ผ๋ก ๋จผ์ ์ฑ ๋ชจ๋์ build.gradle์ proto Datastore, Google Protobuf ์ข
์์ฑ์ ์ถ๊ฐํ๋ค.
plugins {
...
id "com.google.protobuf" version "0.8.12"
}
dependencies {
...
// Proto DataStore
implementation "androidx.datastore:datastore-core:1.0.0-alpha01"
// Protobuf
implementation "com.google.protobuf:protobuf-javalite:3.11.0"
}
// protobuf plugin
protobuf {
protoc {
artifact = "com.google.protobuf:protoc:3.11.0"
}
generateProtoTasks {
all().each { task ->
task.builtins {
java {
option 'lite'
}
}
}
}
}
์ ๋ฐ๋ชจ์์ ๋ดค๋ ๊ฒ ์ฒ๋ผ ์์ ๋ฆฌ์คํธ๋ฅผ ๊ฐ์ง๊ณ ์์ด์ผํ๋ค. ๊ทธ๋ฌ๋ ๋จผ์ ์์์ ๋ํ ๋ชจ๋ธ์ ๋จผ์ ์์ฑํด์ค์ผํจ
data class Food(
val name: String,
val type: FoodType,
val taste: FoodTaste
)
enum class FoodTaste {
SWEET, SPICY
}
enum class FoodType {
VEG, NON_VEG
}
๋ํ DataStore์ ์ ์ฅ๋ UserFoodPreference๋ผ๋ ์๋ก์ด ํด๋์ค๋ ํ๋ ๋ง๋ค์ด์ค์ผํ๋ค. ์ด ํด๋์ค๋ type์ด๋ taste๊ฐ ๋ณ๊ฒฝ๋ ๋ ๋ง๋ค ์ ๊ณต๋๋ ์ญํ ์ ํจ
data class UserFoodPreference(
val type: FoodType?,
val taste: FoodTaste?
)
์ฐธ๊ณ : ๐ ์์ ๋ณด์ด๋ฏ์ด ํ๋๋ฅผ
nullable๋ก ์ ์งํ๋ค.null์ด๋ฉด ์ฌ์ฉ์๊ฐ ์์์ ๋ํ ํํฐ ๋ ์ ํธ๋๋ฅผ ์ค์ ํ์ง ์์๋ค๊ณ ๊ฐ์ ํ ์ ์์
๋ชจ๋ธ ํด๋์ค๋ฅผ Proto DataStore์ ์ง์ ์ ์ฅํ ์๋ ์๋ค. ๊ทธ๋์ .proto ํ์ผ ์์ ์คํค๋ง๋ฅผ ์ ์ํด์ค์ผํ๋๋ฐ
food_preference.proto๋ผ๋ ํ์ผ์ app/src/main/proto์ ์์ฑํด์ค๋ค.
syntax = "proto3";
option java_package = "dev.shreyaspatil.datastore.example.proto";
option java_multiple_files = true;
message FoodPreferences {
enum FoodType {
TYPE_UNSPECIFIED = 0;
TYPE_VEG = 1;
TYPE_NON_VEG = 2;
}
enum FoodTaste {
TASTE_UNSPECIFIED = 0;
TASTE_SWEET = 1;
TASTE_SPICY = 2;
}
FoodType type = 1;
FoodTaste taste = 2;
}
์ฐธ๊ณ :
ProtoBuffer์ ๋ํ syntax ๊ฐ์ด๋๋ ์ฌ๊ธฐ์ ํ์ธํ ์ ์๋ค
์ ๋จ๊ณ๋ฅผ ๋ง์น๋ฉด Gradle ํ๋ก์ ํธ๋ฅผ ๋ค์ ๋น๋ํ๋ค. ๊ทธ๋ฌ๋ฉด FoodPreference.java๊ฐ ์์ ์คํค๋ง์์ ์๋์ผ๋ก ์์ฑ๋๋ ๊ฒ์ ๋ณผ ์ ์์
์๋์์ฑ๋ ํด๋์ค๋ฅผ ์ํด serializer๊ฐ ํ์ํ๋ฐ ๊ทธ ์ด์ ๋ Proto DataStore๋ proto object ๋ฅผ serializes/deserializes ํ๊ธฐ์ํด serializer์ ์ธ์คํด์ค๋ฅผ ํ์๋ก ํ๊ธฐ ๋๋ฌธ์ด๋ค.
FoodPreferenceSerializer.kt๋ผ๋ ํ์ผ์ ํ๋ ์๋ก ๋ง๋ค๊ณ ์๋์ฒ๋ผ ์์ฑํจ
object FoodPreferenceSerializer : Serializer<FoodPreferences> {
override fun readFrom(input: InputStream): FoodPreferences {
try {
return FoodPreferences.parseFrom(input)
} catch (exception: InvalidProtocolBufferException) {
throw CorruptionException("Cannot read proto.", exception)
}
}
override fun writeTo(t: FoodPreferences, output: OutputStream) = t.writeTo(output)
}
์ด์ ์ฌ์ฉ์์ ์์ ์ ํธ๋๋ฅผ ์ ์ฅํ FoodPreferenceManager๋ฅผ ๋ง๋ค๊ฑด๋ฐ (์ค์ ๋ก Proto DataStore๋ฅผ ๊ตฌํํ ์์น),
์ฌ๊ธฐ์์ DataStore์ ํ์ผ์ด๋ฆ(food_prefs.pb)๊ณผ ์์์ ๋ง๋ Serializer์ ์ธ์คํด์ค๋ฅผ ์ ๊ณตํด์ค์ผ ํ๋ค.
๋จผ์ FoodPreferenceManager.kt๋ผ๋ ํ์ผ์ ์๋ก ๋ง๋ค๊ณ ์๋์ฒ๋ผ dataStore ํ๋๋ฅผ ์ด๊ธฐํ์์ผ์ฃผ๋ ์ฝ๋๋ฅผ ์์ฑ
class FoodPreferenceManager(context: Context) {
private val dataStore: DataStore<FoodPreferences> =
context.createDataStore(
fileName = "food_prefs.pb",
serializer = FoodPreferenceSerializer
)
....
์ด์ Type, Taste๋ฅผ ๋ณ๊ฒฝํด์ฃผ๋ ๋ฉ์๋๋ฅผ ๊ฐ๊ฐ ๋ง๋ค์ด์ฃผ์
Type์ ๋ณ๊ฒฝํด์ฃผ๋ ๋ฉ์๋
suspend fun updateUserFoodTypePreference(type: FoodType?) {
val foodType = when (type) {
FoodType.VEG -> FoodPreferences.FoodType.TYPE_VEG
FoodType.NON_VEG -> FoodPreferences.FoodType.TYPE_NON_VEG
null -> FoodPreferences.FoodType.TYPE_UNSPECIFIED
}
dataStore.updateData { preferences ->
preferences.toBuilder()
.setType(foodType)
.build()
}
}
Taste๋ฅผ ๋ณ๊ฒฝํด์ฃผ๋ ๋ฉ์๋
suspend fun updateUserFoodTastePreference(taste: FoodTaste?) {
val foodTaste = when (taste) {
FoodTaste.SWEET -> FoodPreferences.FoodTaste.TASTE_SWEET
FoodTaste.SPICY -> FoodPreferences.FoodTaste.TASTE_SPICY
null -> FoodPreferences.FoodTaste.TASTE_UNSPECIFIED
}
dataStore.updateData { preferences ->
preferences.toBuilder()
.setTaste(foodTaste)
.build()
}
}
์์ ๋ฉ์๋์์ ๋ณผ ์ ์๋ฏ์ด ๋ฏธ๋ฆฌ ์ ์ํด๋ FoodType, FoodTaste enum ์ ์๋์์ฑ๋ FoodPreference.javaํ์ผ ์์ ์๋ enum ์ผ๋ก ๋ณํ์์ผ์ฃผ๋ ์์
์ ์ํํ๋ค.
๋ณ๊ฒฝํด์ฃผ๋ ๋ฉ์๋๋ฅผ ์์ฑํ์ผ๋ ์ด์ ๋ ์ค์ ์ ์ ์ฅ๋ ๊ฐ์ ์ฝ์ด์ฌ ์ฐจ๋ก, Flow๋ฅผ ํ์ฉํ๋ค
val userFoodPreference = dataStore.data.catch {
if (it is IOException) {
Log.e(TAG, "Error reading sort order preferences.", it)
emit(FoodPreferences.getDefaultInstance())
} else {
throw it
}
}.map {
val type = when (it.type) {
FoodPreferences.FoodType.TYPE_VEG -> FoodType.VEG
FoodPreferences.FoodType.TYPE_NON_VEG -> FoodType.NON_VEG
else -> null
}
val taste = when (it.taste) {
FoodPreferences.FoodTaste.TASTE_SWEET -> FoodTaste.SWEET
FoodPreferences.FoodTaste.TASTE_SPICY -> FoodTaste.SPICY
else -> null
}
UserFoodPreference(type, taste)
}
์ด๊ฑธ๋ก FoodPreferenceManager ํด๋์ค์ ๊ดํ ์ค์ ์ ๋ชจ๋ ๋!
์ฌ๊ธฐ์์๋ Activity ์์ RecyclerView ๋ฅผ ์ฌ์ฉํ์ฌ์ด ์ฑ์ ๊ตฌํํ๊ณ Repository์์ ์์ ํญ๋ชฉ ๋ชฉ๋ก๊ณผ ๊ฐ์ ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ ์ค๊ธฐ ์ํด ViewModel์ ๊ตฌํํ๋ค๊ณ ๊ฐ์ , (๋ฐ๋ชจ ๋ชฉ์ ์ผ๋ก ์ฌ๊ธฐ์ ์์ ํญ๋ชฉ์ ๋๋ฏธ ๋ชฉ๋ก์ ์ ๊ณตํ๋ ์ํ DataSource๋ฅผ ๋ง๋ค์๋ค).
๋ฐ๋ผ์ DataStore์ ๊ด๋ จ๋ ๊ตฌํ์ ๋ณด์ฌ์ค๊ฑด๋ฐ ์ ์ฒด ์ฝ๋๋ ์ฌ๊ธฐ
๋จผ์ ProtoDatastoreActivity๋ผ๋ ์กํฐ๋นํฐ๋ฅผ ๋ง๋ค๊ณ FoodPreferenceManager ํ๋์ RecyclerView๋ฅผ ์ํ foodListAdapter๋ฅผ ์์ฑํด์ค๋ค.
class ProtoDatastoreActivity : AppCompatActivity() {
private lateinit var foodPreferenceManager: FoodPreferenceManager
private val foodListAdapter by lazy { FoodListAdapter() }
์ฐธ๊ณ : ์ฌ๊ธฐ์ ์๋
foodListAdapter๋RecyclerView.Adapter๋ฅผ ๊ตฌํํ ๊ฒ
์ด์ Chips ๋ฅผ ํด๋ฆญํ๋ฉด ์ค์ ์ ์ ์ฅํ๊ณ ์ ๋ฐ์ดํธํ๋ ๋ก์ง์ ๊ตฌํํ ๊ฒ์ด๋ค. ์์ธํ ์ฝ๋๋ ์๋
private fun initViews() {
foodTaste.setOnCheckedChangeListener { group, checkedId ->
val taste = when (checkedId) {
R.id.sweet -> FoodTaste.SWEET
R.id.spicy -> FoodTaste.SPICY
else -> null
}
lifecycleScope.launch { foodPreferenceManager.updateUserFoodTastePreference(taste) }
}
foodType.setOnCheckedChangeListener { group, checkedId ->
val type = when (checkedId) {
R.id.veg -> FoodType.VEG
R.id.nonVeg -> FoodType.NON_VEG
else -> null
}
lifecycleScope.launch { foodPreferenceManager.updateUserFoodTypePreference(type) }
}
}
์ ๋ก์ง์ด ๋๋ฌ์ผ๋ฉด ์ด์ ์ค์ ์ด ๋ณ๊ฒฝ๋์๋์ง Observe ํ๋ ์์ ๋ ์ฒ๋ฆฌ๋ฅผ ํด์ค์ผํจ.
private fun observePreferences() {
foodPreferenceManager.userFoodPreference.asLiveData().observe(this) {
filterFoodList(it.type, it.taste)
}
}
์ฐธ๊ณ : ์ฌ๊ธฐ์์๋
Flow์asLiveData()์ต์คํ ์ ์ ์ฌ์ฉํ๋ค. ๊ทธ๋ ์ง ์์ผ๋ฉดlifecycleScope๋ฅผ ์ฌ์ฉํ ์๋ ์์. ์ด๋ ์ผ๋ฐ์ ์ผ๋ก ViewModel ๋ก ์ค์ ๋ก ๊ตฌํํ ๋ ์ ์ฉํ๋ค
์ด์ ์ค์ ๋ฐ์ดํฐ๋ฅผ ํํฐ๋งํ๋ ๋ก์ง์ ๊ตฌํํด์ผํจfilterFoodList()๋ผ๋ ๋ฉ์๋๋ฅผ ๋ง๋ค๊ณ ์๋์ฒ๋ผ ์์ฑํ๋ค.
private fun filterFoodList(type: FoodType?, taste: FoodTaste?) {
var filteredList = getFoodList()
type?.let { foodType ->
filteredList = filteredList.filter { it.type == foodType }
}
taste?.let { foodTaste ->
filteredList = filteredList.filter { it.taste == foodTaste }
}
foodListAdapter.submitList(filteredList)
if (filteredList.isEmpty()) {
Toast.makeText(this, "No results!", Toast.LENGTH_SHORT).show()
}
updateViews(type, taste)
}
์ง๊ธ๊น์ง Proto DataStore๋ฅผ ๊ตฌํํด๋ดค๋ค. ์ฑ์ ์คํํ๋ฉด์ด ๋ฌธ์์ ์์ ๋ถ๋ถ์์ ๋ณธ ๋ฐ๋ชจ์ ๊ฐ์ ๊ฒฐ๊ณผ๊ฐ ๋ณด์ด๊ฒ ๋๋ค.