일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 | 31 |
- 플레이스토어
- 로그인
- 코딩테스트
- 알고리즘
- 리사이클러뷰
- ListAdapter
- Jetpack
- 코틀린
- cleanarchitecture
- 파이어베이스
- Android
- 안드로이드
- Compose
- Build variants
- UiState
- coroutine
- Flow
- 컴포즈
- NavHost
- MVVM
- 커스텀뷰
- Authentication
- 회원가입
- XML
- DiffUtil
- 뷰
- Kotlin
- NavController
- 클린아키텍처
- sharedFlow
- Today
- Total
Grusie 안드로이드 개발 기술 블로그
[Android] DataStore 사용하기(HashMap형태 저장) 본문
이번 개발에 필요한 요구사항 중, 마지막으로 메일을 본 이후로 온 메일이 있다면 화면에 알림을 띄워주는 게 있었다.
마지막으로 메일을 본 시간을 저장하기 위해서는, 로컬DB를 사용해야 한다고 생각하였고, 선택지는 2가지가 있었다.
RoomDB <- 관계형 데이터베이스를 다룰 때 많은 도움이 되겠으나, 간단한 데이터를 저장하기에는 맞지 않다고 판단.
SharedPreferences <- 간단한 데이터를 Key-Value형태로 저장할 수 있어, 보통 앱 세팅과 같은 데이터들을 저장하는데 사용한다. 이런 간단한 데이터를 담는데엔 이만한 게 없다고 생각하여 SharedPreferences로 결정하였다.
SharedPreferences는 Key-Value 형태로 로컬 저장소에 저장하는 것을 도와주는 객체이다.
SharedPreferences는 앞으로 DataSource로 대체 될 것이며, 공식문서에도 SharedPreferences에 대한 내용이 빠졌다는 글을 봤다.
그래서 이번 기회에 DataSource를 활용해 보려고 하였다.
DataStore는 SharedPreferences에 비해 장점이 많다.
- 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
https://kangmin1012.tistory.com/47
후기
SharedPreferences를 DataStore로 변경하며 작업해 보았다.
DataStore를 권장한다고 하니 지속적으로 사용할 것을 참고하며, 마치면 될 것 같다.
현재 알림 메일을 지우는 방식을 클릭 시, 인텐트를 실행한 뒤 동작하도록 해뒀는데, Rx를 사용하면 더 좋았을 것 같다는 생각이 드나, MVP 모델을 빠르게 쳐내야 하기에, Rx는 추후에 더 필요한 상황이 오면 처리해보려고 한다.
'안드로이드 개발 > 라이브러리' 카테고리의 다른 글
[Android] Glide Permission Denied Exception (0) | 2024.07.30 |
---|---|
[Android] 네이버 맵 공식 클러스터링 적용법 (0) | 2024.06.12 |
[Android] Data binding 기본 사용법 및 BindingAdapter (0) | 2024.04.26 |
[Android] 이미지 로딩 라이브러리 Glide, Fresco (Glide를 사용한 이유) (1) | 2024.04.19 |
[Android]RxJava 사용하기 (0) | 2024.04.15 |