Grusie 안드로이드 개발 기술 블로그

[Android] DataStore 사용하기(HashMap형태 저장) 본문

안드로이드 개발/라이브러리

[Android] DataStore 사용하기(HashMap형태 저장)

grusie 2024. 5. 23. 09:40
728x90
반응형
SMALL

이번 개발에 필요한 요구사항 중, 마지막으로 메일을 본 이후로 온 메일이 있다면 화면에 알림을 띄워주는 게 있었다.

마지막으로 메일을 본 시간을 저장하기 위해서는, 로컬DB를 사용해야 한다고 생각하였고, 선택지는 2가지가 있었다.

 

RoomDB <- 관계형 데이터베이스를 다룰 때 많은 도움이 되겠으나, 간단한 데이터를 저장하기에는 맞지 않다고 판단.

SharedPreferences <- 간단한 데이터를 Key-Value형태로 저장할 수 있어, 보통 앱 세팅과 같은 데이터들을 저장하는데 사용한다. 이런 간단한 데이터를 담는데엔 이만한 게 없다고 생각하여 SharedPreferences로 결정하였다.

 

SharedPreferences는 Key-Value 형태로 로컬 저장소에 저장하는 것을 도와주는 객체이다.

SharedPreferences는 앞으로 DataSource로 대체 될 것이며, 공식문서에도 SharedPreferences에 대한 내용이 빠졌다는 글을 봤다.

 

그래서 이번 기회에 DataSource를 활용해 보려고 하였다.

SharedPreferences와 DataStore의 비교
이미지 출처 : https://onlyfor-me-blog.tistory.com/519

 

DataStoreSharedPreferences에 비해 장점이 많다.

- Flow를 통해 읽고 쓰기가 가능하도록 비동기 API를 제공한다.

- UI 스레드를 호출해도 안전하다.

- runtime Exception으로부터 안전하다.

 

 

구현할 기본 로직

- 마지막으로 메일을 본 시간을 담을 객체를 로컬DB에 저장한다.

- 메일을 보는 것에 대한 건 내부 규정에 따라 여러 개의 Space 별로 따로 가져가야한다.

- 그렇기에 HashMap형태로, Space의 Id와 마지막으로 본 시간을 저장해야 한다.

- 마지막으로 본 시간은 계산하기 편하게 TimeInMillis로 하여 Long타입으로 저장 할 예정이다.

- 추 후 로그인 기능을 완성하면, 유저별로 보는 시간을 확인 할 수 있어야 하고, 유저 정보도 추후 저장해야 하기에, userInfo라는 이름으로 구현할 것이다.

 

사용법

Build.gradle.kts

//DataSore
implementation("androidx.datastore:datastore-preferences:1.1.1")
implementation("androidx.datastore:datastore-core:1.1.1")

DataStore를 사용하기 위한의존성을 추가해준다.

 

Data계층

DataStoreSetUp

val Context.userInfoDataStore: DataStore<Preferences> by preferencesDataStore(name = "user_info_preferences")

DataStore를 생성해주기 위한 세팅을 진행한다.

Context를 확장하여 만들어 두었다.

 

LocalUserInfoManager

class LocalUserInfoManager(private val dataStore: DataStore<Preferences>) {
	...
    suspend fun getLocalUserInfo(): LocalUserEntity {
        val preferences = dataStore.data.first()
        val lastSlowMailConfirmDate = preferences[LAST_SLOW_MAIL_CONFIRM_DATA_KEY] ?: "0"
        return LocalUserEntity(
            lastSlowMailConfirmTime = lastSlowMailConfirmDate
        )
    }

    suspend fun saveLocalUserInfo(localUserEntity: LocalUserEntity) {
        dataStore.edit { prefs ->
            localUserEntity.lastSlowMailConfirmTime.let {
                prefs[LAST_SLOW_MAIL_CONFIRM_DATA_KEY] = it
            }
        }
    }
}

dataStore를 주입받아, 실질적인 저장/출력을 담당하는 LocalUserInfoManager이다.

 

UserInfoRemoteDataSource

class UserInfoRemoteDataSource @Inject constructor(
    private val localUserInfoManager: LocalUserInfoManager,
) : UserInfoDataSource {
    override suspend fun getLocalUserInfo(): Result<LocalUserEntity> {
        return try {
            Result.success(localUserInfoManager.getLocalUserInfo())
        } catch (e: Exception) {
            Result.failure(e)
        }
    }

    override suspend fun setLocalUserInfo(localUserEntity: LocalUserEntity): Result<Unit> {
        return try {
            Result.success(localUserInfoManager.saveLocalUserInfo(localUserEntity))
        } catch (e: Exception) {
            Result.failure(e)
        }
    }
}

Repository에서 사용할 UserInfoRemoteDataSource이다. 실사용시엔, 성공/실패 여부가 필요할 것이기에, try-catch문을 활용하여 Result형태로 리턴하도록 구현하였으며, 인터페이스인 UserInfoDataSource를 상속받는다.

 

DefaultUserInfoRepository

class DefaultUserInfoRepository @Inject constructor(private val dataSource: UserInfoDataSource) :
    UserInfoRepository {
    override suspend fun getLocalUserInfo(): Result<LocalUserInfo> {
        return dataSource.getLocalUserInfo().map { it.toDomain() }
    }

    override suspend fun setLocalUserInfo(localUserInfo: LocalUserInfo): Result<Unit> {
        return dataSource.setLocalUserInfo(localUserInfo.toEntity())
    }
}

도메인 계층에 있는 UserInfoRepository 인터페이스의 구연체이며, dataSouce에서 받아온 데이터/ 전송할 데이터를 매핑하여 리턴한다.

 

도메인 계층

UseCase

class GetLocalUserInfoUseCase(private val repository: UserInfoRepository) {
    suspend operator fun invoke(): Result<LocalUserInfo> {
        return repository.getLocalUserInfo()
    }
}
class SetLocalUserInfoUseCase(private val repository: UserInfoRepository) {
    suspend operator fun invoke(localUserInfo: LocalUserInfo): Result<Unit> {
        return repository.setLocalUserInfo(localUserInfo)
    }
}

domain계층과 UI계층에 있는 모델을 따로 만들필요성을 느끼지 못 해 그대로 Ui계층에서 사용하도록 구현하였다.

 

프레젠테이션 계층

ViewModel

getLocalUserInfo()

private suspend fun getLocalUserInfo() {
    getLocalUserInfoUseCase().onSuccess {
        setLocalUserInfo(it)
    }.onFailure {
        Log.e(this::class.simpleName, it.message ?: "")
        setSlowMailBoxUiState(SlowMailBoxUiState.Error(it.message ?: ""))
    }
}

가져온 유저정보를 ViewModel내부에 있는 StateFlow 변수에 넣어두어 추 후, 유저정보를 통해 UI업데이트를 진행할 수도 있도록 구현하였다.(프로필 이미지 넣기 등)

 

getReceuvedSlowList()

private suspend fun getReceivedSlowList(lastId: Long? = null) {
    slowBoxUseCases.getReceivedSlowListUseCase(spaceId = _spaceId.value, lastId = lastId)
        .onSuccess { resultData ->
            setLastFlag(resultData.slowMails.isEmpty())

            val receivedList = resultData.slowMails.map { it.toUIModel() }

            setSlowMailBoxUiState(
                SlowMailBoxUiState.SuccessGetReceivedList(receivedList)
            )

            if (lastId == null) {
                if (receivedList.isNotEmpty()) {
                    compareNotiReceivedMail(receivedList[0])
                }
            }
        }.onFailure {
            Log.e(
                "${this::class.simpleName}",
                (it as RemoteError).toStringForLog()
            )
            setSlowMailBoxUiState(SlowMailBoxUiState.Error((it).toStringForUser()))
        }
}

메일을 가져오는 함수에서, 처음 호출하는 경우이면서, 받아온 리스트가 비어있지 않으면, 젤 최근에 온 메일을 다른 함수로 넘겨준다.

 

fetchData()

fun fetchData() {
    viewModelScope.launch {
        val fetchList = listOf(
            async {
                getReservedSlowList()
            },
            async {
                getSendSlowList()
            },
            async {
                getLocalUserInfo()
                getReceivedSlowList()
            }
        )

        fetchList.awaitAll()
        setSlowMailBoxUiState(SlowMailBoxUiState.Success)
    }
}

async - await를 사용하였으며, getLocalUserInfo()가 실행된 뒤에, getReceivedSlowLis()를 실행하여 문제 없이 동작하도록 하였다.

 

private fun compareNotiReceivedMail(lastMailInfo: MailInfo) {
    _localUserInfo.value?.lastSlowMailConfirmTime?.get(_spaceId.value)?.let {
        if (it < lastMailInfo.sendAt.millis) {
            setNotiReceivedMailInfo(lastMailInfo)
        }
    } ?: run {
        setNotiReceivedMailInfo(lastMailInfo)
    }
}

가져온 유저 정보에 있는 현재 space에 해당하는 마지막 확인 날짜와 비교해서, 그것보다 이후에 온 메일이라면, 넣어주고, 만약 유저 정보에 있는 마지막 확인 날짜가 null일 경우(처음)에는 무조건 화면에 띄워줘야 한다.

 

setLastSlowMailConfirmDate()

fun setLastSlowMailConfirmDate(timeInMillis: Long) {
    _localUserInfo.value?.let {
        val hashMap = it.lastSlowMailConfirmTime
        hashMap[_spaceId.value] = hashMap.getOrDefault(_spaceId.value, timeInMillis)
        it.copy(lastSlowMailConfirmTime = hashMap)
    }?.let {
        setLocalUserInfo(it)
        saveLocalUserInfo()
    }
}

알림이 온 메일을 클릭 했을 때, 현재 시간을 받아와 HashMap으로 전환해준 뒤, 서버에 저장한다.

 

Activity

clBtnNotification.setOnSingleClickListener {
    viewModel.setLastSlowMailConfirmDate(DateTime.now().millis)
    receiveMailInfo?.let {
        intentToDetailMailActivity(
            it,
            SlowMailListActivity.TYPE_RECEIVED
        )

        viewModel.setNotiReceivedMailInfo(null)
    }
}

메일 알림을 클릭 했을 시, 해당메일에 상세 페이지로 이동하며, 알림 정보를 초기화하여 사라지도록 한다.

 

lifecycleScope.launch {
    viewModel.notiReceivedMailInfo.collectLatest {
        receiveMailInfo = it
    }
}

뷰모델에 있는 notiReceivedMailInfo를 collect하여 데이터바인딩을 시켜준다. null일 경우, 알림이 보이지 않도록 구현되어 있다.

 

결과

데이터 스토어

 

데이터 스토어 저장소를 확인해보면, 잘 들어가 있는 것을 볼 수 있다.

 

데이터 스토어를 활용한 결과물

처음 실행해서 들어갔을 땐, 가장 최근에 온 느린 편지가 보여지도록 되어 있으며, 클릭 시 상세로 넘어가고 알림을 없애준다.

이후에 다시 들어가도, 알림이 없는 것을 볼 수 있다.

 

 

참고

https://onlyfor-me-blog.tistory.com/519

 

[Android] DataStore란? DataStore 예제

간단한 데이터를 저장하는 방법으로 지금도 자주 사용되는 건 쉐어드 프리퍼런스일 것이다. 그런데 이것을 대신해서 사용할 수 있는 것을 안드로이드에서 만든 모양이다. 그게 이 포스팅의 제

onlyfor-me-blog.tistory.com

https://kangmin1012.tistory.com/47

 

[Android/Kotlin] SharedPreferences 대신 쓰는 DataStore

지금까지 우리들은 로컬에 간단한 데이터들을 저장하기 위해서 SharedPreferences를 사용했습니다. 하지만 현재 안드로이드에서는 DataStore의 사용을 적극 권장하고 있습니다. ( 개발자 문서에서도 Sha

kangmin1012.tistory.com

 

후기

SharedPreferences를 DataStore로 변경하며 작업해 보았다.

DataStore를 권장한다고 하니 지속적으로 사용할 것을 참고하며, 마치면 될 것 같다.

현재 알림 메일을 지우는 방식을 클릭 시, 인텐트를 실행한 뒤 동작하도록 해뒀는데, Rx를 사용하면 더 좋았을 것 같다는 생각이 드나, MVP 모델을 빠르게 쳐내야 하기에, Rx는 추후에 더 필요한 상황이 오면 처리해보려고 한다.

반응형
LIST