카테고리 없음

Jetpack Glance를 간단하게 써 보고.

hyuckkim 2023. 6. 5. 21:25

Jetpack DataStore! (velog.io)

 

Jetpack DataStore!

프로젝트를 진행하다 보면 SharedPreferences의 간편함 덕분에 무지성으로 SP를 사용하곤 했습니다. 그런데 사실 공식문서를 살펴보면 다음과 같은 문구가 적혀있습니다.

velog.io

Jetpack DataStore에 대한 정보가 필요하면 이 글을 읽으세요.

 

https://www.youtube.com/watch?v=bhrN7yFG0D4 

Jetpack Glance에 대한 정보가 필요하면 이 영상을 보세요.

 

 

 

 

 

 

 

 

 

 

이걸로 실제로 뭘 만든 건 아니고, 정말 기능만 간단하게 써 봤다.

나는 1.0.0-alpha05 버전을 사용했고, 베타 버전으로 업그레이드 해 봤을 때 모든 코드에 빨간 줄이 쳐졌으니, 아마 미래에는 이 글이 도움이 전혀 안 될 것이다. 확실히 변명이지만, 그래서 나는 이 글을 대충 쓰기로 했다.

 

만든 건, Todo 앱이라고 우길 수 있는 앱이다.

Jetpack Compose로 만든 리스트.

대충 추가 / 완료만 구현해놓은 부끄러운 상태다.

val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "data")
object TodoList {
    private val example_counter = stringPreferencesKey("example_counter")

    suspend fun add(context: Context, str: String) {
        val data = get(context).toMutableList()
        data.add(str)
        set(context, data)
    }
    suspend fun remove(context: Context, i: Int) {
        val data = get(context).toMutableList()
        data.removeAt(i)
        set(context, data)
    }
    suspend fun getString(context: Context): String {
        return runBlocking { (context.dataStore.data.first()[example_counter] ?: "") }
    }
    suspend fun get(context: Context): List<String> {
        return getString(context).split("\n").filter { it.isNotEmpty() }
    }
    suspend fun set(context: Context, list: List<String>) {
        context.dataStore.edit { data ->
            data[example_counter] = list.joinToString(separator = "\n")
        }
    }
    suspend fun init(context: Context) {
        val sampleData = getString(context)
        if (sampleData != "") return

        set(context, listOf("밥 먹기", "세수 하기", "학교 가기"))
    }
}

데이터는 마찬가지로 보여주기 부끄러운 다른 오브젝트에 의해 관리된다.

 

목록은 jetpack datastore에 example_counter 키 하나만 사용해 단순 문자열로 저장되고, 문자열은 NSV 형식으로 저장되어 \n으로 구분된다.

NSV는 방금 내가 만든 표현인데, newline separated values라는 뜻이다. 이 앱이 조금 더 발전하면 목표 날짜나 플래그, 카테고리 등을 저장하게 되겠지만, 아마 그 때는 이것보다 나은 선택지를 선택할 것이다.

NSV는 그냥 split("\n")으로 파싱이 돼서 그냥 그걸 썼다.

또 filter { it.isNotempty()} 를 쓰면 빈 문자열에서 빈 리스트가 반환된다!

 

get / set 함수는 문자열 리스트를 주고받고, 문자열을 더하는 add, index를 받아서 없애는 remove를 만들었다.

그리고 정보를 순문장열로 받는 getString

 

기술적인 얘기를 좀 해보면, 여기서 datastore를 쓴 부분은 다음과 같다.

// 데이터 생성
val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "data")

        // 데이터 받아오기
        return runBlocking { (context.dataStore.data.first()[example_counter] ?: "") }
        
        // 데이터 수정
        context.dataStore.edit { data ->
            data[example_counter] = list.joinToString(separator = "\n")
        }

데이터 생성은 몽키 패칭 / 확장 함수 같은 느낌으로 만들어진다.

(따라서 Context.dataStore의 뒷 부분, 그러니까 dataStore는 아무 이름으로나 바꿔도 된다.)

 

내가 제일 힘들었던 게, 기본 예제에서는 (그리고 그걸 배낀 수많은 블로그에서는) 데이터를 flow로 가져오는 법만 소개한다. (아마도 Composable과 최대한 가깝게 붙여 지으라고)

Jetpack DataStore! (velog.io)

 

Jetpack DataStore!

프로젝트를 진행하다 보면 SharedPreferences의 간편함 덕분에 무지성으로 SP를 사용하곤 했습니다. 그런데 사실 공식문서를 살펴보면 다음과 같은 문구가 적혀있습니다.

velog.io

답을 여기서 찾았다. 감사합니다.

 

나는 데이터를 Compose와 Glance 두 곳에서 동시에 써야 했고,

둘 다 잘 몰랐고 Glance에서 flow가 잘 되는지도 모르겠어서 그냥 좀 기다리고 데이터를 데이터로 받길 원했다.

map() 대신 first() 쓰고, runBlocking 쓰니까 내가 원하는 대로 동작하긴 했다.

 

마지막으로 Glance.

 

초기 Glance 컴포저블 세트 Box, Row, Column, Text, Button, LazyColumn, Image, Spacer로 UI를 구성하기 때문에, Alpha 버전 동안은 여기서 데이터를 추가하게 할 수는 없다.

 

class SimpleCounterWidgetReceiver: GlanceAppWidgetReceiver() {
    override val glanceAppWidget: GlanceAppWidget
        get() = CounterWidget
}

GlanceAppWidgetReceiver를 만들어서 GlanceAppWidget을 인식시킬 수 있다.

<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
    android:description="@string/app_name"
    android:minWidth="280dp"
    android:minHeight="80dp"
    android:resizeMode="horizontal|vertical"
    android:widgetCategory="home_screen">

</appwidget-provider>

xml 파일을 만들었다. res/xml 폴더에.

 

https://www.youtube.com/watch?v=bhrN7yFG0D4 

튜토리얼 영상에서는 IntPreferencesKey를 사용해 데이터를 위젯에 저장한다.

    val jsonTodoList = stringPreferencesKey("json")

나는 순문자열에 아까 만든 NSV 데이터를 저장하기로 했다.

        val data = currentState(key = jsonTodoList) ?: ""
        val dataList = remember(data) {
            data.split("\n").filter { it.isNotEmpty() }
        }

그리고 composable 안에서 대충 잘라서 리스트로 사용하기로 했다.

 

val todoId = parameters.getOrDefault(CounterWidget.TODO_LIST_ID, 0)
        TodoList.remove(context, todoId)
        val v = TodoList.getString(context)

        updateAppWidgetState(context, glanceId) { prefs ->
            prefs[CounterWidget.jsonTodoList] = v
            CounterWidget.update(context, glanceId)
        }

버튼의 onClick Action 안에서 문자열을 새로 받아오면 된다. 와.

 

 

버튼이 한번에 작동하지 않는 버그가 있다.

Refresh 버튼 누르는 걸로 타협 보기로 했음.

내가 지금 쓰고 있는 상용 Todo 앱에도 가끔 비슷한 일이 나는 걸 보면 Widget 자체가 문제가 있긴 한 거 같음.

+ state를 두 번 만들어서 update를 두 번 하니까 대강 해결됐다. 어메이징하군.

 

그리고 Glance와 Compose간의 데이터 변화를 서로 바로 알아차리지 못해서 수동 Refresh 버튼을 만들었다.

아마 Glance 쪽에서는 actionStart 같은걸로, Compose 쪽에서는 LifecycleOwner 같은걸로 해결할 수 있을 거 같긴 한데 어떻게 써야 할 지 하나도 모르겠음

 

끝.

 

 

굳이 전체 코드를 보고 싶다면:

2023-01-Android-Study/MyTodoApp at main · hyuckkim/2023-01-Android-Study (github.com)

 

GitHub - hyuckkim/2023-01-Android-Study

Contribute to hyuckkim/2023-01-Android-Study development by creating an account on GitHub.

github.com

 

정식 버전 나오면 그 때 보는 걸로 하자.

나는 허접이라 예제 코드 자세하게 없으면 못 만들어.