이번 시간에는 Plan Screen에 지난에 Create Plan을 만들면서 생성한 일정 정보를 앱에 띄워주는 작업을 해줄 것이다.
이 작업은 그냥 Firebase의 데이터를 불러와서 앱에다가 보여주면 되는 간단한 기능이고, 관련 자료도 많아서 굳이 코드를 보여주면서 자세히 설명해주지는 않겠다.(절대 귀찮아서가 아니다 ^^)
이번에 필자가 Plan Screen 기능을 적용하면서 겪은 문제는 여기 평범한 달력에 현재 날짜가 아닌 다른 날짜의 일정 정보를 추가하고자 할 때,
일정 추가를 통해 Create Plan Screen으로 갔다가 다시 이 화면으로 왔을 때에는 기본으로 설정한 날짜로 바뀌는 문제가 계속 발생했다.
그래서 이번에는 필자가 다음과 같은 문제를 어떻게 해결하였는지를 설명해보려 한다.
1. Compose에서의 상태 관리
Compose에서 상태를 관리하는 변수를 작성하려면 remember {}를 사용하는 것이 일반적이다.
필자 또한 remember를 사용해 날짜 정보를 확인할 변수를 만들어 주었다.
var yearState by remember { mutableStateOf(formatter.format(date).split("-").first()) }
var monthState by remember { mutableStateOf(formatter.format(date).split("-")[1]) }
var dayState by remember { mutableStateOf(formatter.format(date).split("-").last()) }
위의 변수들은 현재 날짜를 기본값으로 해주는 로직을 작성해 주었다.
날짜 정보를 위와 같이 설정하고, 일정 생성 기능을 적용하였을 때, 처음에 설명한 것과 같은 문제가 발생하게 된 것이다.
문제의 원인은 어렵지 않게 찾을 수 있었다.
screen별로 화면 이동(navigation)을 할 때 이동되기 전 Screen의 state가 저장되지 않고, 다른 screen을 갔다가 다시 돌아왔을 때 계속 기본값으로 설정해 주어서 위와 같은 문제가 발생한다는 것을 깨달았다.
이동 전에는 이동 전 화면의 state 데이터가 남아 있지만, 이동 후 screen을 갔다가 다시 이동 전 screen으로 왔을 때에 데이터가 없는 문제가 발생하였다.
Planner v2의 관점으로 다시 설명하자면, Plan Screen에서 특정 날짜에 일정을 추가하고 싶어서 Create Plan Screen으로 이동했었고, 다시 Plan Screen으로 돌아왔는데, 전 Plan Screen에서 설정한 일정 정보가 사라진 것이다!
문제의 영상
영상을 보면, 12일로 설정하고 create 화면에 들어갔지만, 나왔을 때에는 14일로 설정되어 있는 것을 볼 수 있다.
그래서 이번에 필자는 Compose에서 DataStore을 사용해 보기로 했다.
DataStore에 대해 간단히 소개하면, SharedPreference의 단점을 보완하기 위해서 출시한 라이브러리이다.
더 자세한 내용을 알고 싶으면 검색해서 알아보도록 하자.
필자는 DataStore을 이용해 기기 안에 날짜 정보를 저장해서 날짜 정보를 적용하기로 했다.
필자가 공부한 링크들을 남겨 놓겠다. 참고해 보면 좋을 것 같다.
https://kangmin1012.tistory.com/47
https://www.youtube.com/watch?v=yMGAbm84iIY&pp=ygUZYW5kcm9pZCBkYXRhc3RvcmUgZXhhbXBsZQ%3D%3D
필자는 DataStore class를 구현 후 Application을 상속받은 class에 DataStore instance를 초기화해 사용하는 방법을 사용했다. 이렇게 만든 이유는 DataStore가 여러 개가 존재하면 안 되기 때문(singleton)이다.
바로 코드를 통해 확인을 해보자.
2 - 1. 라이브러리 추가
dataStore을 사용하기 위한 라이브러리를 추가해 주자(2023.11.19 기준)
// data store
implementation("androidx.datastore:datastore-preferences:1.0.0")
implementation("androidx.datastore:datastore-preferences-core:1.0.0")
2 - 2. DataStore 생성
DataStore Class를 만들어 보자.
class DateDataStore(private val context: Context) {
companion object {
// 데이터 저장에 사용할 key 정의
private val Context.dateDataStore: DataStore<Preferences> by preferencesDataStore(name = "selectedDate")
val DATE_KEY = stringPreferencesKey("date")
}
suspend fun setDate(date: String = getCurrentDate()) {
context.dateDataStore.edit {
it[DATE_KEY] = date
}
}
val dateFlow: Flow<String?> = context.dateDataStore.data.catch { exception ->
if (exception is IOException) emit(emptyPreferences())
else throw exception
}.map {
it[DATE_KEY] ?: ""
}
private fun getCurrentDate(): String {
val date = Date()
val formatter = SimpleDateFormat("yyyy-MM-dd", Locale.KOREA)
return formatter.format(date)
}
}
compaion object에 자신이 사용할 DataStore와 key를 생성해 준다.
key 생성은 (자료형) PreferenceKey("key name")와 같은 형식으로 작성해 주면 된다.
데이터 저장 방법은 edit {}을 사용해 주어야 한다. 그리고 그 안에서 자신이 만든 key를 이용해서 데이터를 저장해 준다.
context.dateDataStore.edit {
it[DATE_KEY] = date
}
데이터를 불러오려면 map을 사용하면 된다 그리고 DataStore은 오류를 잡아주는 기능도 가지고 있기에 catch를 이용해 오류를 잡아내 줄 수 도 있다.
필자가 잡은 IOException은 데이터를 불러오지 못하면 발생하는 exception이기 때문에 따로 작성해서 빈 데이터를 return 해주었다.
val dateFlow: Flow<String?> = context.dateDataStore.data.catch { exception ->
if (exception is IOException) emit(emptyPreferences())
else throw exception
}.map {
it[DATE_KEY] ?: ""
}
2 - 3. Application 만들기
이전에 만든 DataStore를 초기화하기 위한 Application을 만들어 준다.
class PlannerV2Application: Application() {
private lateinit var dataStore: DateDataStore
companion object {
private lateinit var plannerV2Application: PlannerV2Application
fun getInstance() = plannerV2Application
}
override fun onCreate() {
super.onCreate()
plannerV2Application = this
dataStore = DateDataStore(this)
}
fun getDataStore(): DateDataStore = dataStore
}
getInstance()를 이용해 Application을 불러올 수 있게 하고, getDataStore()을 이용해 DataStore을 불러올 수 있도록 작성해 준다.
생성한 Application을 Manifest에서 적용해 주는 것도 잊으면 안 된다!
<application
android:name=".application.PlannerV2Application"
. . .
이제 DataStore을 사용할 준비가 끝났다.
2 - 4. 사용해 보기
필자는 앱을 실행하면 Splash가 뜨고 있을 때, DataStore의 데이터를 오늘 날짜로 변경하는 로직을 생성해 주었다.
앱을 다시 실행했을 때, 이전에 선택한 날짜가 떠 있으면 이상하기(?) 때문이다.
val initDataStoreState = remember { mutableStateOf(false) }
LaunchedEffect(Unit) {
initDataStore(initDataStoreState)
}
먼저 데이터를 초기화했는지 알려주는 remember 변수를 만들어 주었고, 1번만 수행하는 LaunchedEffect를 생성해서 DataStore의 데이터를 오늘 날짜로 초기화해 주는 작업을 수행해 주었다.
private suspend fun initDataStore(initDataStoreState: MutableState<Boolean>) {
val dataStore = PlannerV2Application.getInstance().getDataStore()
dataStore.setDate()
dataStore.dateFlow.collect {
if (it == getCurrentDate()) initDataStoreState.value = true
}
}
private fun getCurrentDate(): String {
val date = Date()
val formatter = SimpleDateFormat("yyyy-MM-dd", Locale.KOREA)
return formatter.format(date)
}
그리고 데이터가 초기화되었는지 확인하기 위해서 위에서 생성한 remember를 이용해 데이터가 초기화되어있지 않으면 loading 화면을 띄우고 그렇지 않으면 route에 연결된 화면을 띄우는 로직을 작성했다.
if (initDataStoreState.value) {
if (loginState != null) {
PlannerV2NavHost(
navHostController = navHostController,
startDestination = if (loginState!!) planRoute else loginRoute
)
}
} else CircularProgressScreen()
loading 화면 코드는 따로 올려놓지는 않겠다. 취향껏 만들어 보면 좋겠다.
Plan Screen에서는 저장한 데이터를 불러오는 로직을 작성한다.
val dataStore = PlannerV2Application.getInstance().getDataStore()
val dateFlow = dataStore.dateFlow.collectAsState(initial = "").value
dateFlow 변수를 만들고, 변수가 변함에 따라 일정 정보를 가져오는 로직을 작성한다.
LaunchedEffect(dateFlow) {
val uid = FirebaseAuth.getInstance().uid
if (!dateFlow.isNullOrEmpty() && uid != null) {
monthState = dateFlow.split("-")[1]
dayState = dateFlow.split("-").last()
planViewModel.getPlans(uid, dateFlow)
}
}
여기까지 모두 완료했으면 구현 결과를 확인해 보자.
2 - 5. 구현 결과
정리
- 이번에 날짜 정보를 저장하기 위해서 DataStore을 사용해 보았다. 처음 배우는 기술이기도 하고, Compose와 함께 사용하는 예제는 많지 않아서 구현하는데 힘들었다.
- navigate 할 때 screen이 새로 만들어져서 날짜 정보가 초기화되어서 DataStore을 사용했다. 그래서 navigate 할 때 이전에 사용한 screen stack에서 재활용할 수 있도록 코드를 짜보았지만 작동하지 않았다. 정확한 사용 방법을 아시는 분들은 댓글 부탁드립니다 :D
'⛏️ | 개발 기록 > ⏰ | Schedule Planner' 카테고리의 다른 글
[Android, Kotlin] firebase 데이터베이스 변경하기 (13) (0) | 2024.04.10 |
---|---|
[Android, Kotlin] Compose로 동적 checkBox 리스트 만들기 (12) (2) | 2023.12.04 |
[Android, Kotlin] Firebase RTDB에서 데이터 순차적으로 삽입하기 (10) (0) | 2023.11.12 |
[Android, Kotlin] Android 12 버전 이상에서 Compose로 Custom Splash Screen 생성하기 (9) (0) | 2023.11.10 |
[Android, Kotlin] Compose로 Firebase Google Login 구현하기 (8) (2) | 2023.11.09 |