본문 바로가기
⛏️ | 개발 기록/⏰ | Schedule Planner

[Android, Kotlin] Compose에서 DataStore 사용하기 (11)

by immgga 2023. 11. 19.

 

이번 시간에는 Plan Screen에 지난에 Create Plan을 만들면서 생성한 일정 정보를 앱에 띄워주는 작업을 해줄 것이다.

이 작업은 그냥 Firebase의 데이터를 불러와서 앱에다가 보여주면 되는 간단한 기능이고, 관련 자료도 많아서 굳이 코드를 보여주면서 자세히 설명해주지는 않겠다.(절대 귀찮아서가 아니다 ^^)

이번에 필자가 Plan Screen 기능을 적용하면서 겪은 문제는 여기 평범한 달력에 현재 날짜가 아닌 다른 날짜의 일정 정보를 추가하고자 할 때,

선택된 11월 12일은 당시의 날짜가 아님

 

일정 추가를 통해 Create Plan Screen으로 갔다가 다시 이 화면으로 왔을 때에는 기본으로 설정한 날짜로 바뀌는 문제가 계속 발생했다.

문제의 사진(당시 날짜 11월 14일)

 

그래서 이번에는 필자가 다음과 같은 문제를 어떻게 해결하였는지를 설명해보려 한다.

 

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
728x90

댓글