와챠의 우당탕탕 코딩 일기장

[Compose] 1-3. Compose State/Stateful/Stateless/ViewModel/remember* 본문

코딩 일기장/Android(Kotlin)

[Compose] 1-3. Compose State/Stateful/Stateless/ViewModel/remember*

minWachya 2025. 4. 4. 21:21
반응형

아래와 같은 앱을 만들어보면서 State를 더 깊이 배워보려 한다.

두 가지 기능이 있는 앱이다.

  • water count: "Add one" 버튼을 눌러 하루동안 물 얼마나 마셨는지 기록
  • todo list(wellness list): 고정된 wellness 목록. 체크버튼, 삭제 버튼이 있음

 

<목차: 개념>

  1. Composable의 생명 주기
  2. Recomposition
  3. remember / rememberSaveable
  4. Stateful / Stateless
  5. ViewModel

<목차: 코드>

  1. Water Counter
  2. WellnessTask
  3. WellnessItem
  4. WellnessList
  5. WellnessViewModel
  6. WellnessScreen

코딩하기 전에 몇몇 개념부터 정리하고 갑시다.

 

1. Composable의 수명주기

 

컴포저블의 수명 주기는 다음과 같다.

  • 컴포지션 진입
  • 리컴포지션
  • 컴포지션 종료

2. Recomposition

  • 컴포지션: Composable을 실행할 때 Jetpack Compose에서 빌드한 UI에 관한 설명
  • 초기 컴포지션: 처음 Composable을 실행한 컴포지션
  • 리컴포지션: 데이터가 변경될 때 (컴포지션을 업데이트하기 위해) Composable을 다시 실행하는 것

3. remember / rememverSaveavle

  • remember: remember로 설정한값은 초기 컴포지션에서 저장되고, 저장된 값은 리컴포지션 간에 유지됨
  • rememberSaveable: remember는 구성 변경(언어 변경, 라이트모드<>다크모드 변환 등) 에서는 state를 유지하지 않지만, rememberSaveable는 유지 가능함
    • Activity가 다시 생성된 후(구성 변경 후) UI 상태를 복원할 때 rememberSaveable을 사용함
    • rememberSaveable은 Activity 재생성 및 시스템에서 시작된 프로세스 종료 전반에 걸쳐 상태를 유지함
    • 복잡한 데이터 구조나 대량의 데이터를 저장하는 데에 적합하진 않음

+ by: getter/setter 위임. remember로 설정한 MutableState 값을 (value를 사용하지 않고) 간접적으로 읽고 변경할 수 있음


4. Stateful / Stateless

  • Stateful: Composable 내에 state가 있는 상태(== remember를 사용한 객체가 있는 상태)
    • 호출자가 state를 제어, 관리할 필요가 없을 때 사용
    • 단점: 재사용성 적음. 테스트 어려움
  • Stateless: Composable 내에 state가 없는 상태. 상태 호이스팅을 통해 구현 가능

+ 상태 호이스팅: Composable을 Stateless로 만들기 위해 state를 Composable의 호출자로 옮기는 패턴: 상태 변수를 다음 두 개의 매개변수로 바꾸는 것으로 구현함

  • value: T - 표시할 값 T
  • onValueChange: (T) -> Unit - T를 변경할 수 있는 이벤트

5. ViewModel

  • ViewModel: UI에 표시되는 데이터를 보유하는 클래스
    • 구성 변경 후에도 유지됨. == 컴포지션보다 전체 기간이 길다
ViewModel 클래스는 비즈니스 로직 또는 화면 수준 상태 홀더입니다.
UI에 상태를 노출하고 관련 비즈니스 로직을 캡슐화합니다.
주요 이점은 상태를 캐시하여 구성 변경에도 이를 유지한다는 것입니다.
즉, 활동 간에 이동하거나 구성 변경(예: 화면 회전 시)을 따를 때 UI가 데이터를 다시 가져올 필요가 없습니다.

https://developer.android.com/topic/libraries/architecture/viewmodel?hl=ko

 

+ UI 로직: 화면에 상태 변경을 표시하는 방법(스낵바 표시)과 관련됨

+ 비즈니스 로직: 상태 변경 시(결제하기 또는 사용자 환경설정 저장) 실행할 작업

 

viewmodel을 사용하려면 아래 라이브러리 추가해야됨.

버전은 여기서 확인

implementation("androidx.lifecycle:lifecycle-viewmodel-compose:{latest_version}")

 


이제 코드 짜봅시더!!!!!!

 

1. Water Counter

Water Counter의 기능:

  • 초기 상태: 물 0잔. 아무 Text 없음.
  • "Add one" 버튼:
    • 버튼을 한 번 누를 때마다 물잔 갯수++
    • 물잔 갯수를 Text로 표시함
    • 물잔 최대 갯수는 10잔으로 제한

 

// Stateful: 물잔 갯수 state인 count를 관리함
@Composable
fun StatefulCounter(modifier: Modifier = Modifier) {
    var count by rememberSaveable { mutableStateOf(0) }
    // 상태 호이스팅: value T: count, onValueChange(T) -> Unit: onIncrement
    StatelessCounter(
        count = count,
        onIncrement = {count++},
        modifier = modifier
    )
}

// Stateless: 상태 호이스팅을 통해 Stateless로 만듦.
@Composable
fun StatelessCounter(
    count: Int,
    onIncrement: () -> Unit,
    modifier: Modifier = Modifier) {
    Column(modifier = modifier.padding(16.dp)) {
        if(count > 0) Text(text = "${count}잔 마심")
        Button(
            onClick = onIncrement,
            Modifier.padding(top = 8.dp),
            enabled = count < 10         // 최대 갯수 제한
        ) {
            Text("추가")
        }
    }
}

 

음 쉽죠? 주석 외의 코멘트가 필요X...


2. WellnessTask

WellnessTask는 item을 구분할 id, task 내용인 label, task 상태를 관리할 Boolean 값인checked가 있다.

이때 변할 수 있는 값인 checked는 mutableStateOf로 관리해준다.

모두 변하지 않는 값(val)였다면 data class로 써주면 되는데, state값이 있으니 class로 만들어주고, class 안에서 state를 관리하는 변수를 선언해준다.

class WellnessTask(
    val id: Int,
    val label: String,
    initialChecked: Boolean = false
) {
    var checked by mutableStateOf(initialChecked)
}

3. WellnessItem

WellnessTask 데이터가 들어갈 item compose다.

여기서 상태를 관리해주어야 하는 것은 체크박스와, X버튼을 클릭했을 때다.

최대한 Stateless로 만들기 위해(호출자에게 상태 관리를 맡기도록) 상태를 관리해주기 위한 함수(onCheckedChange, onClose)를 둔다.(상태 호이스팅)

@Composable
fun WellnessTaskItem(
    taskName: String,
    checked: Boolean,
    onCheckedChange: (Boolean) -> Unit,
    onClose: () -> Unit,
    modifier: Modifier = Modifier
) {
    Row(
        modifier = modifier,
        verticalAlignment = Alignment.CenterVertically
    ) {
        Text(
            modifier = Modifier
                .weight(1f)
                .padding(start = 16.dp),
            text = taskName
        )
        Checkbox(
            checked = checked,
            onCheckedChange = onCheckedChange
        )
        IconButton(onClick = onClose) {
            Icon(Icons.Filled.Close, contentDescription = "Close Button")
        }
    }
}

4. WellnessList

WellnessItem으로 List를 만들어봅시더

LazyColumn과 items를 사용해서 List<WellnessTask>를 하나씩 WellnessTaskItem으로 만든다.

이때 onCheckedTask, onCloseTask도 함께 WellnessItem으로 보낸다.

아이템을 구분하기 위한 id도 items의 key로 적용해준다.

@Composable
fun WellnessTaskList(
    modifier: Modifier = Modifier,
    list: List<WellnessTask>,
    onCheckedTask: (WellnessTask, Boolean) -> Unit,
    onCloseTask: (WellnessTask) -> Unit
) {
    LazyColumn(
        modifier = modifier
    ) {
        items(
            items = list,
            key = { it.id }
        ) { task ->
            WellnessTaskItem(
                taskName = task.label,
                checked = task.checked,
                onCheckedChange = { checked -> onCheckedTask(task, checked) },
                onClose = { onCloseTask(task) }
            )
        }
    }
}

5. WellnessViewModel

드디어 WellnessTask 데이터를 관리하는 ViewModel을 만들 차례!!!

ViewModel을 상속받고, 데이터와 데이터를 관리하는 함수들을 만들어주면 된다.

데이터를 관리하려면 수정 가능하게 만들어야 하는데, 외부에서 수정 불가능하게 만들기 위해 다음과 같이 만들어주면 된다.

onCheckedChange, onClose 함수도 여기서 만들어 준다!!

class WellnessViewModel: ViewModel() {
    private val _tasks = getWellnessTasks().toMutableStateList()
    val tasks: List<WellnessTask> get() = _tasks

    fun remove(item: WellnessTask) {
        _tasks.remove(item)
    }

    fun changeTaskChecked(item: WellnessTask, checked: Boolean) {
        _tasks.find{ it.id == item.id }?.let {
            it.checked = checked
        }
    }
}

private fun getWellnessTasks() = List(30) { i -> WellnessTask(i, "${i}번째 할 일") }

6. WellnessScreen

WaterCounter와 위에서 만든 ViewModel을 사용해 WellnessTaskList를 최종적으로 구현할 차례다.

viewModel()을 사용해 ViewModel을 생성하고, WellnessTaskList에 viewmodel이 관리하는 list와 함수들을 전달한다.

 

+ viewModel(): 기존 ViewModel을 반환하거나, 새 ViewModel을 생성함. ViewModel 인스턴스는 범위가 활성화되어 있는 동안 유지됨. (ex: Composable이 Activity에서 사용되면 viewModel()은 Activity가 완료되거나 프로세스가 종료될 때까지 동일한 인스턴스를 반환함)

@Composable
fun WellnessScreen(
    modifier: Modifier = Modifier,
    wellnessViewModel: WellnessViewModel = viewModel()
    ) {
    Column(modifier = modifier) {
        StatefulCounter()
        WellnessTaskList(
            list = wellnessViewModel.tasks,
            onCheckedTask = { task, checked ->
                wellnessViewModel.changeTaskChecked(task, checked)
            },
            onCloseTask = { wellnessViewModel.remove(it) }
        )
    }
}

완성!!


뭔가 새로운 걸 배우는 기분이 아니라 flutter하는 기분임

그래도... 재밋고... ㄹㅇ 잭팟 컴포즈 전에는 대체 어케 코딩했었는지 대단...ㅋ

viewmodel에서 데이터 관리할 때 변하는 데이터가 있으면 data class가 아니라 그냥 class에 mutableStateOf로 관리하는 게 쫌 기억에 남는 듯... 훨 낫다


기타

 

 

 

 

 

반응형
Comments