refactor(preferences): DataStore 설정 저장 안정성을 높인다

This commit is contained in:
2026-03-11 12:32:34 +09:00
parent 8e1dabbb80
commit 418b734c3f
16 changed files with 847 additions and 285 deletions

View File

@@ -1,12 +1,10 @@
package kr.co.vividnext.sodalive.chat.talk.room
import android.annotation.SuppressLint
import android.content.Context
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.content.edit
import androidx.fragment.app.DialogFragment
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
@@ -41,7 +39,6 @@ class ChatBackgroundPickerDialogFragment : DialogFragment() {
private var characterId: Long = 0L
private var profileUrl: String = ""
private val prefsName = "chat_room_prefs"
private fun bgImageIdKey(roomId: Long) = "chat_bg_image_id_room_$roomId"
private lateinit var adapter: BgAdapter
@@ -87,8 +84,7 @@ class ChatBackgroundPickerDialogFragment : DialogFragment() {
private fun loadData() {
// 초기 선택: 저장된 이미지 ID 사용
val prefs = requireActivity().getSharedPreferences(prefsName, Context.MODE_PRIVATE)
val savedId = prefs.getLong(bgImageIdKey(roomId), -1L)
val savedId = ChatRoomPreferenceManager.getLong(bgImageIdKey(roomId), -1L)
selectedId = if (savedId > 0) savedId else null
items.clear()
@@ -124,12 +120,10 @@ class ChatBackgroundPickerDialogFragment : DialogFragment() {
}
private fun saveAndApply(item: BgItem) {
val prefs = requireActivity().getSharedPreferences(prefsName, Context.MODE_PRIVATE)
prefs.edit {
if (item.id > 0) putLong(
bgImageIdKey(roomId),
item.id
) else remove(bgImageIdKey(roomId))
if (item.id > 0) {
ChatRoomPreferenceManager.putLong(bgImageIdKey(roomId), item.id)
} else {
ChatRoomPreferenceManager.remove(bgImageIdKey(roomId))
}
(activity as? ChatRoomActivity)?.setChatBackground(item.url)
}

View File

@@ -9,7 +9,6 @@ import android.text.TextWatcher
import android.view.View
import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InputMethodManager
import androidx.core.content.edit
import androidx.core.view.isVisible
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
@@ -55,12 +54,10 @@ class ChatRoomActivity : BaseActivity<ActivityChatRoomBinding>(
// 5.3 헤더 데이터 (7.x 연동 전까지는 nullable 보관)
private var characterInfo: CharacterInfo? = null
// 5.4 SharedPreferences (안내 메시지 접힘 상태 저장)
private val prefs by lazy { getSharedPreferences("chat_room_prefs", MODE_PRIVATE) }
private fun noticePrefKey(roomId: Long) = "chat_notice_hidden_room_${roomId}"
private fun isNoticeHidden(): Boolean = prefs.getBoolean(noticePrefKey(roomId), false)
private fun isNoticeHidden(): Boolean = ChatRoomPreferenceManager.getBoolean(noticePrefKey(roomId), false)
private fun setNoticeHidden(hidden: Boolean) {
prefs.edit { putBoolean(noticePrefKey(roomId), hidden) }
ChatRoomPreferenceManager.putBoolean(noticePrefKey(roomId), hidden)
}
override fun setupView() {
@@ -866,7 +863,7 @@ class ChatRoomActivity : BaseActivity<ActivityChatRoomBinding>(
}
fun applyBackgroundVisibility() {
val visible = prefs.getBoolean("chat_bg_visible_room_$roomId", true)
val visible = ChatRoomPreferenceManager.getBoolean("chat_bg_visible_room_$roomId", true)
binding.ivBackgroundProfile.isVisible = visible
binding.viewCharacterDim.isVisible = visible
}
@@ -885,7 +882,7 @@ class ChatRoomActivity : BaseActivity<ActivityChatRoomBinding>(
private fun bgImageIdPrefKey(): String = "chat_bg_image_id_room_$roomId"
private fun getSavedBackgroundImageId(): Long? {
val id = prefs.getLong(bgImageIdPrefKey(), -1L)
val id = ChatRoomPreferenceManager.getLong(bgImageIdPrefKey(), -1L)
return if (id > 0) id else null
}
@@ -912,9 +909,7 @@ class ChatRoomActivity : BaseActivity<ActivityChatRoomBinding>(
"chat_bg_image_id_room_$roomId",
noticePrefKey(roomId)
)
prefs.edit {
keys.forEach { remove(it) }
}
ChatRoomPreferenceManager.removeAll(keys)
}
fun onResetChatRequested() {

View File

@@ -1,6 +1,5 @@
package kr.co.vividnext.sodalive.chat.talk.room
import android.content.Context
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
@@ -11,7 +10,6 @@ import android.widget.RelativeLayout
import androidx.fragment.app.DialogFragment
import com.google.android.material.switchmaterial.SwitchMaterial
import kr.co.vividnext.sodalive.R
import androidx.core.content.edit
/**
* 채팅방 우측 상단 더보기 버튼 클릭 시 표시되는 전체화면 다이얼로그.
@@ -42,13 +40,12 @@ class ChatRoomMoreDialogFragment : DialogFragment() {
// 닫기 버튼
view.findViewById<ImageView>(R.id.iv_close)?.setOnClickListener { dismiss() }
val prefs = requireActivity().getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
val bgKey = bgPrefKey(roomId)
val switch = view.findViewById<SwitchMaterial>(R.id.sw_background)
switch?.isChecked = prefs.getBoolean(bgKey, true)
switch?.isChecked = ChatRoomPreferenceManager.getBoolean(bgKey, true)
switch?.setOnCheckedChangeListener { _, isChecked ->
prefs.edit { putBoolean(bgKey, isChecked) }
ChatRoomPreferenceManager.putBoolean(bgKey, isChecked)
(activity as? ChatRoomActivity)?.applyBackgroundVisibility()
}
@@ -76,7 +73,6 @@ class ChatRoomMoreDialogFragment : DialogFragment() {
companion object {
private const val ARG_ROOM_ID = "arg_room_id"
private const val PREFS_NAME = "chat_room_prefs"
private fun bgPrefKey(roomId: Long) = "chat_bg_visible_room_$roomId"
fun newInstance(roomId: Long): ChatRoomMoreDialogFragment {

View File

@@ -0,0 +1,190 @@
package kr.co.vividnext.sodalive.chat.talk.room
import android.content.Context
import androidx.annotation.VisibleForTesting
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.SharedPreferencesMigration
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.longPreferencesKey
import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.datastore.preferences.preferencesDataStoreFile
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
object ChatRoomPreferenceManager {
private const val LEGACY_PREFERENCES_NAME = "chat_room_prefs"
private const val DATASTORE_FILE_NAME = "chat_room_preferences"
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
fun init(context: Context) {
if (initialized) {
return
}
synchronized(initLock) {
if (initialized) {
return
}
val appContext = context.applicationContext
if (!this::dataStore.isInitialized) {
dataStore = androidx.datastore.preferences.core.PreferenceDataStoreFactory.create(
migrations = listOf(SharedPreferencesMigration(appContext, LEGACY_PREFERENCES_NAME)),
produceFile = { appContext.preferencesDataStoreFile(DATASTORE_FILE_NAME) }
)
}
val initialPreferences = runBlocking {
dataStore.data.first()
}
updateState(initialPreferences)
initialized = true
observerJob?.cancel()
observerJob = appScope.launch {
dataStore.data.collect { preferences ->
updateState(preferences)
}
}
}
}
private fun updateState(preferences: Preferences) {
preferenceState.value = preferences.asMap().entries.associate { (key, value) ->
key.name to value
}
}
@VisibleForTesting(otherwise = VisibleForTesting.NONE)
fun resetForTest() {
synchronized(initLock) {
observerJob?.cancel()
observerJob = null
preferenceState.value = emptyMap()
initialized = false
}
}
fun getBoolean(key: String, defaultValue: Boolean): Boolean {
ensureInitialized()
return preferenceState.value[key] as? Boolean ?: defaultValue
}
fun putBoolean(key: String, value: Boolean) {
ensureInitialized()
preferenceState.update { state ->
val mutable = state.toMutableMap()
mutable[key] = value
mutable
}
appScope.launch {
dataStore.edit { preferences ->
preferences[booleanPreferencesKey(key)] = value
}
}
}
fun getLong(key: String, defaultValue: Long): Long {
ensureInitialized()
return preferenceState.value[key] as? Long ?: defaultValue
}
fun putLong(key: String, value: Long) {
ensureInitialized()
preferenceState.update { state ->
val mutable = state.toMutableMap()
mutable[key] = value
mutable
}
appScope.launch {
dataStore.edit { preferences ->
preferences[longPreferencesKey(key)] = value
}
}
}
fun getString(key: String, defaultValue: String): String {
ensureInitialized()
return preferenceState.value[key] as? String ?: defaultValue
}
fun putString(key: String, value: String) {
ensureInitialized()
preferenceState.update { state ->
val mutable = state.toMutableMap()
mutable[key] = value
mutable
}
appScope.launch {
dataStore.edit { preferences ->
preferences[stringPreferencesKey(key)] = value
}
}
}
fun remove(key: String) {
ensureInitialized()
preferenceState.update { state ->
val mutable = state.toMutableMap()
mutable.remove(key)
mutable
}
appScope.launch {
dataStore.edit { preferences ->
val prefKey = preferences.asMap().keys.firstOrNull { it.name == key } ?: return@edit
preferences.remove(prefKey)
}
}
}
fun removeAll(keys: Collection<String>) {
ensureInitialized()
if (keys.isEmpty()) {
return
}
preferenceState.update { state ->
val mutable = state.toMutableMap()
keys.forEach { key ->
mutable.remove(key)
}
mutable
}
appScope.launch {
dataStore.edit { preferences ->
val mapKeys = preferences.asMap().keys
keys.forEach { keyName ->
val prefKey = mapKeys.firstOrNull { it.name == keyName } ?: return@forEach
preferences.remove(prefKey)
}
}
}
}
private fun ensureInitialized() {
check(initialized) { "ChatRoomPreferenceManager is not initialized." }
}
}