refactor(preferences): DataStore 설정 저장 안정성을 높인다
This commit is contained in:
@@ -0,0 +1,46 @@
|
||||
package kr.co.vividnext.sodalive.common
|
||||
|
||||
import android.content.Context
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import androidx.datastore.core.DataStore
|
||||
import androidx.datastore.preferences.SharedPreferencesMigration
|
||||
import androidx.datastore.preferences.core.PreferenceDataStoreFactory
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import androidx.datastore.preferences.preferencesDataStoreFile
|
||||
|
||||
object AppPreferencesDataStoreProvider {
|
||||
private const val DATASTORE_FILE_NAME = "sodalive_default_preferences"
|
||||
private const val DEFAULT_SHARED_PREFERENCES_SUFFIX = "_preferences"
|
||||
|
||||
@Volatile
|
||||
private var dataStore: DataStore<Preferences>? = null
|
||||
|
||||
fun get(context: Context): DataStore<Preferences> {
|
||||
val existing = dataStore
|
||||
if (existing != null) {
|
||||
return existing
|
||||
}
|
||||
|
||||
return synchronized(this) {
|
||||
dataStore ?: createDataStore(context.applicationContext).also { dataStore = it }
|
||||
}
|
||||
}
|
||||
|
||||
private fun createDataStore(context: Context): DataStore<Preferences> {
|
||||
val legacyPreferencesName = "${context.packageName}$DEFAULT_SHARED_PREFERENCES_SUFFIX"
|
||||
return PreferenceDataStoreFactory.create(
|
||||
migrations = listOf(
|
||||
// 기존 기본 SharedPreferences 값은 DataStore 첫 접근 시 자동 이관된다.
|
||||
SharedPreferencesMigration(context, legacyPreferencesName)
|
||||
),
|
||||
produceFile = { context.preferencesDataStoreFile(DATASTORE_FILE_NAME) }
|
||||
)
|
||||
}
|
||||
|
||||
@VisibleForTesting(otherwise = VisibleForTesting.NONE)
|
||||
fun resetForTest() {
|
||||
synchronized(this) {
|
||||
dataStore = null
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,167 +1,270 @@
|
||||
package kr.co.vividnext.sodalive.common
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import androidx.preference.PreferenceManager
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import androidx.datastore.core.DataStore
|
||||
import androidx.datastore.preferences.core.MutablePreferences
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import androidx.datastore.preferences.core.booleanPreferencesKey
|
||||
import androidx.datastore.preferences.core.edit
|
||||
import androidx.datastore.preferences.core.floatPreferencesKey
|
||||
import androidx.datastore.preferences.core.intPreferencesKey
|
||||
import androidx.datastore.preferences.core.longPreferencesKey
|
||||
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.reflect.TypeToken
|
||||
import kr.co.vividnext.sodalive.settings.notification.MemberRole
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
|
||||
object SharedPreferenceManager {
|
||||
private lateinit var sharedPreferences: SharedPreferences
|
||||
private const val PREF_APP_LANGUAGE_CODE = "pref_app_language_code"
|
||||
|
||||
private lateinit var dataStore: DataStore<Preferences>
|
||||
private val appScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||
private val preferenceState = MutableStateFlow<Map<String, Any?>>(emptyMap())
|
||||
private val initLock = Any()
|
||||
private var observerJob: Job? = null
|
||||
|
||||
@Volatile
|
||||
private var initialized = false
|
||||
|
||||
val roleFlow: Flow<String> =
|
||||
preferenceState.map { state ->
|
||||
state[Constants.PREF_USER_ROLE] as? String ?: MemberRole.USER.name
|
||||
}.distinctUntilChanged()
|
||||
|
||||
val isPlayerServiceRunningFlow: Flow<Boolean> =
|
||||
preferenceState.map { state ->
|
||||
state[Constants.PREF_IS_PLAYER_SERVICE_RUNNING] as? Boolean ?: false
|
||||
}.distinctUntilChanged()
|
||||
|
||||
fun init(context: Context) {
|
||||
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
}
|
||||
if (initialized) {
|
||||
return
|
||||
}
|
||||
|
||||
fun registerOnSharedPreferenceChangeListener(
|
||||
listener: SharedPreferences.OnSharedPreferenceChangeListener
|
||||
) {
|
||||
sharedPreferences.registerOnSharedPreferenceChangeListener(listener)
|
||||
}
|
||||
synchronized(initLock) {
|
||||
if (initialized) {
|
||||
return
|
||||
}
|
||||
|
||||
fun unregisterOnSharedPreferenceChangeListener(
|
||||
listener: SharedPreferences.OnSharedPreferenceChangeListener
|
||||
) {
|
||||
sharedPreferences.unregisterOnSharedPreferenceChangeListener(listener)
|
||||
}
|
||||
dataStore = AppPreferencesDataStoreProvider.get(context.applicationContext)
|
||||
val initialPreferences = runBlocking {
|
||||
dataStore.data.first()
|
||||
}
|
||||
updateState(initialPreferences)
|
||||
initialized = true
|
||||
|
||||
fun clear() {
|
||||
sharedPreferences.edit { editor ->
|
||||
sharedPreferences.all.keys
|
||||
.filterNot { it == Constants.PREF_PUSH_TOKEN }
|
||||
.forEach { editor.remove(it) }
|
||||
observerJob?.cancel()
|
||||
observerJob = appScope.launch {
|
||||
dataStore.data.collect { preferences ->
|
||||
updateState(preferences)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private inline fun SharedPreferences.edit(operation: (SharedPreferences.Editor) -> Unit) {
|
||||
val editor = this.edit()
|
||||
operation(editor)
|
||||
editor.apply()
|
||||
private fun updateState(preferences: Preferences) {
|
||||
preferenceState.value = preferences.asMap().entries.associate { (key, value) ->
|
||||
key.name to value
|
||||
}
|
||||
}
|
||||
|
||||
private operator fun SharedPreferences.set(key: String, value: Any?) {
|
||||
when (value) {
|
||||
is String? -> edit { it.putString(key, value) }
|
||||
is Int -> edit { it.putInt(key, value) }
|
||||
is Boolean -> edit { it.putBoolean(key, value) }
|
||||
is Float -> edit { it.putFloat(key, value) }
|
||||
is Long -> edit { it.putLong(key, value) }
|
||||
else -> throw UnsupportedOperationException("Error")
|
||||
@VisibleForTesting(otherwise = VisibleForTesting.NONE)
|
||||
fun resetForTest() {
|
||||
synchronized(initLock) {
|
||||
observerJob?.cancel()
|
||||
observerJob = null
|
||||
preferenceState.value = emptyMap()
|
||||
initialized = false
|
||||
}
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
ensureInitialized()
|
||||
|
||||
preferenceState.update { state ->
|
||||
val pushToken = state[Constants.PREF_PUSH_TOKEN]
|
||||
if (pushToken != null) {
|
||||
mapOf(Constants.PREF_PUSH_TOKEN to pushToken)
|
||||
} else {
|
||||
emptyMap()
|
||||
}
|
||||
}
|
||||
|
||||
appScope.launch {
|
||||
dataStore.edit { preferences ->
|
||||
val keysToRemove = preferences.asMap().keys.filter { it.name != Constants.PREF_PUSH_TOKEN }
|
||||
keysToRemove.forEach { preferenceKey ->
|
||||
preferences.remove(preferenceKey)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun ensureInitialized() {
|
||||
check(initialized) { "SharedPreferenceManager is not initialized." }
|
||||
}
|
||||
|
||||
private fun setPreference(key: String, value: Any?) {
|
||||
ensureInitialized()
|
||||
|
||||
preferenceState.update { state ->
|
||||
val mutable = state.toMutableMap()
|
||||
if (value == null) {
|
||||
mutable.remove(key)
|
||||
} else {
|
||||
mutable[key] = value
|
||||
}
|
||||
mutable
|
||||
}
|
||||
|
||||
appScope.launch {
|
||||
dataStore.edit { preferences ->
|
||||
when (value) {
|
||||
null -> removeByName(preferences, key)
|
||||
is String -> preferences[stringPreferencesKey(key)] = value
|
||||
is Int -> preferences[intPreferencesKey(key)] = value
|
||||
is Boolean -> preferences[booleanPreferencesKey(key)] = value
|
||||
is Float -> preferences[floatPreferencesKey(key)] = value
|
||||
is Long -> preferences[longPreferencesKey(key)] = value
|
||||
else -> throw UnsupportedOperationException("Error")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
private operator fun <T> SharedPreferences.get(key: String, defaultValue: T? = null): T {
|
||||
private fun <T> getPreference(key: String, defaultValue: T? = null): T {
|
||||
ensureInitialized()
|
||||
val value = preferenceState.value[key]
|
||||
|
||||
return when (defaultValue) {
|
||||
is String, null -> getString(key, defaultValue as? String) as T
|
||||
is Int -> getInt(key, defaultValue as? Int ?: -1) as T
|
||||
is Boolean -> getBoolean(key, defaultValue as? Boolean ?: false) as T
|
||||
is Float -> getFloat(key, defaultValue as? Float ?: -1f) as T
|
||||
is Long -> getLong(key, defaultValue as? Long ?: -1) as T
|
||||
is String, null -> (value as? String ?: defaultValue as? String) as T
|
||||
is Int -> (value as? Int ?: defaultValue) as T
|
||||
is Boolean -> (value as? Boolean ?: defaultValue) as T
|
||||
is Float -> (value as? Float ?: defaultValue) as T
|
||||
is Long -> (value as? Long ?: defaultValue) as T
|
||||
else -> throw UnsupportedOperationException("Error")
|
||||
}
|
||||
}
|
||||
|
||||
private fun removeByName(preferences: MutablePreferences, key: String) {
|
||||
val targetKey = preferences.asMap().keys.firstOrNull { it.name == key } ?: return
|
||||
preferences.remove(targetKey)
|
||||
}
|
||||
|
||||
var token: String
|
||||
get() = sharedPreferences[Constants.PREF_TOKEN, ""]
|
||||
get() = getPreference(Constants.PREF_TOKEN, "")
|
||||
set(value) {
|
||||
sharedPreferences[Constants.PREF_TOKEN] = value
|
||||
setPreference(Constants.PREF_TOKEN, value)
|
||||
}
|
||||
|
||||
var userId: Long
|
||||
get() = sharedPreferences[Constants.PREF_USER_ID, 0]
|
||||
get() = getPreference(Constants.PREF_USER_ID, 0L)
|
||||
set(value) {
|
||||
sharedPreferences[Constants.PREF_USER_ID] = value
|
||||
setPreference(Constants.PREF_USER_ID, value)
|
||||
}
|
||||
|
||||
var nickname: String
|
||||
get() = sharedPreferences[Constants.PREF_NICKNAME, ""]
|
||||
get() = getPreference(Constants.PREF_NICKNAME, "")
|
||||
set(value) {
|
||||
sharedPreferences[Constants.PREF_NICKNAME] = value
|
||||
setPreference(Constants.PREF_NICKNAME, value)
|
||||
}
|
||||
|
||||
var email: String
|
||||
get() = sharedPreferences[Constants.PREF_EMAIL, ""]
|
||||
get() = getPreference(Constants.PREF_EMAIL, "")
|
||||
set(value) {
|
||||
sharedPreferences[Constants.PREF_EMAIL] = value
|
||||
setPreference(Constants.PREF_EMAIL, value)
|
||||
}
|
||||
|
||||
var profileImage: String
|
||||
get() = sharedPreferences[Constants.PREF_PROFILE_IMAGE, ""]
|
||||
get() = getPreference(Constants.PREF_PROFILE_IMAGE, "")
|
||||
set(value) {
|
||||
sharedPreferences[Constants.PREF_PROFILE_IMAGE] = value
|
||||
setPreference(Constants.PREF_PROFILE_IMAGE, value)
|
||||
}
|
||||
|
||||
var can: Int
|
||||
get() = sharedPreferences[Constants.PREF_CAN, 0]
|
||||
get() = getPreference(Constants.PREF_CAN, 0)
|
||||
set(value) {
|
||||
sharedPreferences[Constants.PREF_CAN] = value
|
||||
setPreference(Constants.PREF_CAN, value)
|
||||
}
|
||||
|
||||
var point: Int
|
||||
get() = sharedPreferences[Constants.PREF_POINT, 0]
|
||||
get() = getPreference(Constants.PREF_POINT, 0)
|
||||
set(value) {
|
||||
sharedPreferences[Constants.PREF_POINT] = value
|
||||
setPreference(Constants.PREF_POINT, value)
|
||||
}
|
||||
|
||||
var role: String
|
||||
get() = sharedPreferences[Constants.PREF_USER_ROLE, MemberRole.USER.name]
|
||||
get() = getPreference(Constants.PREF_USER_ROLE, MemberRole.USER.name)
|
||||
set(value) {
|
||||
sharedPreferences[Constants.PREF_USER_ROLE] = value
|
||||
setPreference(Constants.PREF_USER_ROLE, value)
|
||||
}
|
||||
|
||||
var isAuth: Boolean
|
||||
get() = sharedPreferences[Constants.PREF_IS_ADULT, false]
|
||||
get() = getPreference(Constants.PREF_IS_ADULT, false)
|
||||
set(value) {
|
||||
sharedPreferences[Constants.PREF_IS_ADULT] = value
|
||||
setPreference(Constants.PREF_IS_ADULT, value)
|
||||
}
|
||||
|
||||
var isAuditionNotification: Boolean
|
||||
get() = sharedPreferences[Constants.PREF_IS_AUDITION_NOTIFICATION, false]
|
||||
get() = getPreference(Constants.PREF_IS_AUDITION_NOTIFICATION, false)
|
||||
set(value) {
|
||||
sharedPreferences[Constants.PREF_IS_AUDITION_NOTIFICATION] = value
|
||||
setPreference(Constants.PREF_IS_AUDITION_NOTIFICATION, value)
|
||||
}
|
||||
|
||||
var isAdultContentVisible: Boolean
|
||||
get() = sharedPreferences[Constants.PREF_IS_ADULT_CONTENT_VISIBLE, true]
|
||||
get() = getPreference(Constants.PREF_IS_ADULT_CONTENT_VISIBLE, true)
|
||||
set(value) {
|
||||
sharedPreferences[Constants.PREF_IS_ADULT_CONTENT_VISIBLE] = value
|
||||
setPreference(Constants.PREF_IS_ADULT_CONTENT_VISIBLE, value)
|
||||
}
|
||||
|
||||
var contentPreference: Int
|
||||
get() = sharedPreferences[Constants.PREF_CONTENT_PREFERENCE, 0]
|
||||
get() = getPreference(Constants.PREF_CONTENT_PREFERENCE, 0)
|
||||
set(value) {
|
||||
sharedPreferences[Constants.PREF_CONTENT_PREFERENCE] = value
|
||||
setPreference(Constants.PREF_CONTENT_PREFERENCE, value)
|
||||
}
|
||||
|
||||
var pushToken: String
|
||||
get() = sharedPreferences[Constants.PREF_PUSH_TOKEN, ""]
|
||||
get() = getPreference(Constants.PREF_PUSH_TOKEN, "")
|
||||
set(value) {
|
||||
sharedPreferences[Constants.PREF_PUSH_TOKEN] = value
|
||||
setPreference(Constants.PREF_PUSH_TOKEN, value)
|
||||
}
|
||||
|
||||
var isContentPlayLoop: Boolean
|
||||
get() = sharedPreferences[Constants.PREF_IS_CONTENT_PLAY_LOOP, false]
|
||||
get() = getPreference(Constants.PREF_IS_CONTENT_PLAY_LOOP, false)
|
||||
set(value) {
|
||||
sharedPreferences[Constants.PREF_IS_CONTENT_PLAY_LOOP] = value
|
||||
setPreference(Constants.PREF_IS_CONTENT_PLAY_LOOP, value)
|
||||
}
|
||||
|
||||
var notShowingEventPopupId: Long
|
||||
get() = sharedPreferences[Constants.PREF_NOT_SHOWING_EVENT_POPUP_ID, 0]
|
||||
get() = getPreference(Constants.PREF_NOT_SHOWING_EVENT_POPUP_ID, 0L)
|
||||
set(value) {
|
||||
sharedPreferences[Constants.PREF_NOT_SHOWING_EVENT_POPUP_ID] = value
|
||||
setPreference(Constants.PREF_NOT_SHOWING_EVENT_POPUP_ID, value)
|
||||
}
|
||||
|
||||
var isViewedOnboardingTutorial: Boolean
|
||||
get() = sharedPreferences[Constants.PREF_IS_VIEWED_ON_BOARDING_TUTORIAL, false]
|
||||
get() = getPreference(Constants.PREF_IS_VIEWED_ON_BOARDING_TUTORIAL, false)
|
||||
set(value) {
|
||||
sharedPreferences[Constants.PREF_IS_VIEWED_ON_BOARDING_TUTORIAL] = value
|
||||
setPreference(Constants.PREF_IS_VIEWED_ON_BOARDING_TUTORIAL, value)
|
||||
}
|
||||
|
||||
var noChatRoomList: List<Long>
|
||||
get() {
|
||||
val list = sharedPreferences[Constants.PREF_NO_CHAT_ROOM, ""]
|
||||
val list = getPreference(Constants.PREF_NO_CHAT_ROOM, "")
|
||||
val gson = Gson()
|
||||
val listType = object : TypeToken<List<Long>>() {}.type
|
||||
val myList = gson.fromJson<List<Long>>(list, listType)
|
||||
@@ -170,54 +273,60 @@ object SharedPreferenceManager {
|
||||
set(value) {
|
||||
val gson = Gson()
|
||||
val listJson = gson.toJson(value)
|
||||
sharedPreferences[Constants.PREF_NO_CHAT_ROOM] = listJson
|
||||
setPreference(Constants.PREF_NO_CHAT_ROOM, listJson)
|
||||
}
|
||||
|
||||
var isPlayerServiceRunning: Boolean
|
||||
get() = sharedPreferences[Constants.PREF_IS_PLAYER_SERVICE_RUNNING, false]
|
||||
get() = getPreference(Constants.PREF_IS_PLAYER_SERVICE_RUNNING, false)
|
||||
set(value) {
|
||||
sharedPreferences[Constants.PREF_IS_PLAYER_SERVICE_RUNNING] = value
|
||||
setPreference(Constants.PREF_IS_PLAYER_SERVICE_RUNNING, value)
|
||||
}
|
||||
|
||||
var marketingPid: String
|
||||
get() = sharedPreferences[Constants.PREF_MARKETING_PID, ""]
|
||||
get() = getPreference(Constants.PREF_MARKETING_PID, "")
|
||||
set(value) {
|
||||
sharedPreferences[Constants.PREF_MARKETING_PID] = value
|
||||
setPreference(Constants.PREF_MARKETING_PID, value)
|
||||
}
|
||||
|
||||
var marketingUtmSource: String
|
||||
get() = sharedPreferences[Constants.PREF_MARKETING_UTM_SOURCE, ""]
|
||||
get() = getPreference(Constants.PREF_MARKETING_UTM_SOURCE, "")
|
||||
set(value) {
|
||||
sharedPreferences[Constants.PREF_MARKETING_UTM_SOURCE] = value
|
||||
setPreference(Constants.PREF_MARKETING_UTM_SOURCE, value)
|
||||
}
|
||||
|
||||
var marketingUtmMedium: String
|
||||
get() = sharedPreferences[Constants.PREF_MARKETING_UTM_MEDIUM, ""]
|
||||
get() = getPreference(Constants.PREF_MARKETING_UTM_MEDIUM, "")
|
||||
set(value) {
|
||||
sharedPreferences[Constants.PREF_MARKETING_UTM_MEDIUM] = value
|
||||
setPreference(Constants.PREF_MARKETING_UTM_MEDIUM, value)
|
||||
}
|
||||
|
||||
var marketingUtmCampaign: String
|
||||
get() = sharedPreferences[Constants.PREF_MARKETING_UTM_CAMPAIGN, ""]
|
||||
get() = getPreference(Constants.PREF_MARKETING_UTM_CAMPAIGN, "")
|
||||
set(value) {
|
||||
sharedPreferences[Constants.PREF_MARKETING_UTM_CAMPAIGN] = value
|
||||
setPreference(Constants.PREF_MARKETING_UTM_CAMPAIGN, value)
|
||||
}
|
||||
|
||||
var marketingLinkValue: String
|
||||
get() = sharedPreferences[Constants.PREF_MARKETING_LINK_VALUE, ""]
|
||||
get() = getPreference(Constants.PREF_MARKETING_LINK_VALUE, "")
|
||||
set(value) {
|
||||
sharedPreferences[Constants.PREF_MARKETING_LINK_VALUE] = value
|
||||
setPreference(Constants.PREF_MARKETING_LINK_VALUE, value)
|
||||
}
|
||||
|
||||
var marketingLinkValueId: Long
|
||||
get() = sharedPreferences[Constants.PREF_MARKETING_LINK_VALUE_ID, 0L]
|
||||
get() = getPreference(Constants.PREF_MARKETING_LINK_VALUE_ID, 0L)
|
||||
set(value) {
|
||||
sharedPreferences[Constants.PREF_MARKETING_LINK_VALUE_ID] = value
|
||||
setPreference(Constants.PREF_MARKETING_LINK_VALUE_ID, value)
|
||||
}
|
||||
|
||||
var alreadyTrackingAppLaunch: Boolean
|
||||
get() = sharedPreferences[Constants.PREF_ALREADY_TRACKING_APP_LAUNCH, true]
|
||||
get() = getPreference(Constants.PREF_ALREADY_TRACKING_APP_LAUNCH, true)
|
||||
set(value) {
|
||||
sharedPreferences[Constants.PREF_ALREADY_TRACKING_APP_LAUNCH] = value
|
||||
setPreference(Constants.PREF_ALREADY_TRACKING_APP_LAUNCH, value)
|
||||
}
|
||||
|
||||
var appLanguageCode: String
|
||||
get() = getPreference(PREF_APP_LANGUAGE_CODE, "")
|
||||
set(value) {
|
||||
setPreference(PREF_APP_LANGUAGE_CODE, value)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user