์•ˆ๋…• DataStore, ์•ˆ๋…•SharedPreferences๐Ÿ‘‹โ€Š-โ€ŠAndroid๐Ÿ“ฑโ€Š-โ€ŠPart 2: Proto DataStore

Ashton Yoonยท2021๋…„ 4์›” 23์ผ
0

์ด ๊ธ€์€ 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์ด๋ฉด ์‚ฌ์šฉ์ž๊ฐ€ ์Œ์‹์— ๋Œ€ํ•œ ํ•„ํ„ฐ ๋‚˜ ์„ ํ˜ธ๋„๋ฅผ ์„ค์ •ํ•˜์ง€ ์•Š์•˜๋‹ค๊ณ  ๊ฐ€์ • ํ•  ์ˆ˜ ์žˆ์Œ






Protobuf ์ •์˜ํ•˜๊ธฐ

๋ชจ๋ธ ํด๋ž˜์Šค๋ฅผ 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 ๋งŒ๋“ค๊ธฐ

์ž๋™์ƒ์„ฑ๋œ ํด๋ž˜์Šค๋ฅผ ์œ„ํ•ด 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)
}



Food Preference Manager ํด๋ž˜์Šค ์ƒ์„ฑ

์ด์ œ ์‚ฌ์šฉ์ž์˜ ์Œ์‹ ์„ ํ˜ธ๋„๋ฅผ ์ €์žฅํ•  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๋ฅผ ๊ตฌํ˜„ํ•ด๋ดค๋‹ค. ์•ฑ์„ ์‹คํ–‰ํ•˜๋ฉด์ด ๋ฌธ์„œ์˜ ์‹œ์ž‘ ๋ถ€๋ถ„์—์„œ ๋ณธ ๋ฐ๋ชจ์™€ ๊ฐ™์€ ๊ฒฐ๊ณผ๊ฐ€ ๋ณด์ด๊ฒŒ ๋œ๋‹ค.

0๊ฐœ์˜ ๋Œ“๊ธ€