Android Jetpack Compose Remember, State
포스트
취소

Android Jetpack Compose Remember, State

안녕하세요. Narvis2 입니다.
이번 시간에는 Jetpack Compose 에서 사용되는 rememberstate 에 대하여 알아보도록 하겠습니다.

🍎 State / Remember

🍀 1. State

  • 상태 변경에 대응하는 것은 Compose의 핵심
  • Compose앱은 Composable함수를 호출하여 데이터를 UI로 변환하는데, 데이터가 변경되면 새 데이터로 이러한 기능을 호출하여 Update 된 UI를 만든다
  • recomposing
    • Compose는 기존의 Observer 패턴과 같이 앱 데이터의 변경 사항을 관찰하기 위한 도구를 제공한다.
    • Compose데이터가 변경된 구성 요소만 재구성 하고, 영향을 받지 않은 구성을 건너 뛸 수 있도록 개별 구성에 필요한 데이터를 확인한다.

      ⚠️ 주의 ⚠️

      • @Composable 함수는 recompose되기 때문에 변수를 함수 내부에서 선언하면 안됨 (함수가 재시작될때마다 초기화되기 때문)
  • ComposeState 값이 변경되면 해당 요소가 다시 그려짐(Recomposing)
  • 하나의 Component에서 State 변화가 일어나면 해당 Componentrecomposing 을 거치고 이때 remember 키워드를 통해 state 를 기억함으로써 성공적으로 UI를 업데이트 한다.

🍀 2. Remember

  • 이 변수는 initial composition에서 메모리에 저장되어, recompose때에 값을 반환받아 사용 가능
  • recompose로 인한 함수의 재호출과 상관없이 변숫 값이 유지될 수 있다.
  • Composition 이 유지되는 동안에만 적용

    즉, Composition 이 그려지는 밑바닥의 activity/fragmentLifecycle이 변경되면 State 또한 초기화됨

  • 해당 Composable functioncomposition 에서 제걸될 때 마다 같이 제거된다.

    ⚠️ 주의 ⚠️

    • Configuration change 발생 시 값이 유지되지 않음. (기기 회전, 다크 모드적용 등등..)
  • rememberSaveable 👉 Configuration Change에도 State가 살아 있도록 해줌

🍀 3. MutableState

  • mutableStateOf(defaultValue) 를 사용하여 만듬
  • State<T> 타입의 변수를 Runtime 에 Observing 할 수 있다
  • Statevalue 가 변경되면 이 값을 읽어가는 Compose 들은 Recompose 대상이 된다.

예제 👇 MutableState 객체를 Composable function 내부에 선언하는 3가지 방법

1
2
3
val mutableState = remember { mutableStateOf(기본값) }
var value by remember { mutableStateOf(기본값) }
val (value, setValue) = remember { mutableStateOf(기본값) }

예제 👇

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Composable
fun MyScreenContent(names: List<String> = listOf("android", "compose")) {
    val counterState = remember { mutableStateOf(0) }

    Column {
        names.forEach {
            Greeting(name = it)
            Divder(color = Color.Black)
        }

        Divider(color = Color.Transparent, thickness = 32.dp)
        Counter(counterState.value) {
            counterState.value = it
        }
    }
}

@Composable
fun Counter(counte: Int, updateCount: (Int) -> Unit) {
    Button(onClick = { updateCount(count + 1) }) {
        Text(text = "$count 번 클릭하셨어요!")
    }
}

🍀 4. ViewModel 에 State 사용

  • Compose에서는 State 변화시에 해당 State와 연결된 ComposableRecomposition을 거치기 때문에 ViewModel에서 State와 이를 Update하는 함수를 직접 선언하고 Composable 함수 매개변수로 ViewModel을 받아 사용
  • LiveData, Flow -> State 변환
    • LiveData 👉 LiveData.observeAsState()
    • Flow 👉 Flow.collectAsState()

예제 👇

1
2
3
4
5
6
7
8
9
10
11
12
class TodoViewModel : ViewModel() {
    var todoItems = mutableStateListOf<TodoItem>()
        private set

    fun addItem(item: TodoItem) {
        todoItems.add(item)
    }

    fun removeItem(item: TodoItem) {
        todoItems.remove(item)
    }
}

예제 👇 위 ViewModel 사용
✅ state holder 설정

  • 만약 실제 UI를 그리는 TodoScreen 에 직접 ViewModel 을 전달하면 Preview 기능을 사용하기 어려워지고 테스트 가능성이 떨어짐
  • 따라서, UI 를 직접 그리는 Composable에는 ViewModel 을 직접 주입하지 않고 State Holder 를 담당하는 Composable 을 한 단계 거치도록 함
1
2
3
4
5
6
7
8
9
// MyScreen -> state holder 를 담당하는 @Composable
@Composable
fun MyScreen(todoViewModel: TodoViewModel) {
    TodoScreen(
        items = todoViewModel.todoItems,
        onAddItem = todoViewModel::addItem,
        onRemoveItem = todoViewModel::removeItem,
    )
}

예제 👇 Flow<T>.collectAsState() 사용

  • ViewModel 에서 StateFlow 등록 후 .collectAsState()를 사용 시 Configuration Change 발생 시에도 값을 유지할 수 있음
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class NoteViewModel : ViewModel() {
    private val _searchValue = MutableStateFlow("")
    val searchValue = _searchValue.asStateFlow()

    fun setSearchValue(query: String) {
        _searchValue.value = query
    }
}

@Composable
fun NoteNavigation(viewModel: NoteViewModel) {
    val navController = rememberNavController()

    val searchValue = viewModel.searchValue.collectAsState()

    // TODO:: something do..
}

🍎 Stateless 를 위한 State Hoisting

  • stateful - state를 갖고 있으며 이를 직접 변경할 수 없는 Composable
  • stateless - state를 갖고있지 않고, state를 선언하는 것이 아닌 주입받는 방식
  • state hoisting
    • 개발자는 최대한 stateful composable을 줄이고, 이들을 stateless로 변경하는 것이 이상적
    • 하위의 Composable에 선언된 State들을 이들의 공통 조상인 상위 Composable로 옮기는 방식으로 수행
    • 장점
      • state를 한 곳에서만 관리함으로써 버그 방지에 도움이됨
      • HoistingState를 여러 Composable과 공유할 수 있음

🍀 State 를 어느 수준의 Composable 까지 끌어올려야 할지 쉽게 파악할 수 있는 규칙 3가지

  1. 읽기 👉 State는 적어도 이를 사용하는 모든 Composable의 가장 낮은 공통 상위 요소로 끌어 올려야함
  2. 쓰기 👉 State는 최소한 이를 변경할 수 있는 Composable중 가장 상위 Composable로 끌어올려야 함
  3. 동일한 Event에 대한 응답으로 두 State가 변경되는 경우 두 State를 같이 끌어 올려야함

예제 👇 Button을 통해 Component를 숨기고 표시

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Composable
fun MyScreen() {
    val visible = remember { mutableStateOf(true) }

    Column() {
        if (visible.value) MyCard()
        MyButton(onClick = { visible.value = !visible.value }, visible = visible.value)
    }
}

@Composable
fun MyButton(onClic: () -> Unit, visible: Boolean) {
    Button(onClick = onClick) {
        Text(if(visible) "show" else "hide")
    }
}

🍬 참고 🍬

  • MyButton에 State를 선언할 필요없이, 적당한 상단 ComposableState를 선언한 뒤 StateValueState를 변경하는 함수만 전달해주면 stateless를 유지한 채 구현할 수 있음

🍎 Composable 에서 올바른 CoroutinScope

  • Composable 내부에서 Coroutine을 수행할 경우 Composable에 대한 Recomposition이 일어날 때 정리되어야 하는 Coroutine이 정리가 안된 상태로 계속해서 Coroutine이 쌓일 수 있음
  • Composable 에서 Coroutine을 생성한다면 Recomposition이 일어날 때 취소되어야 함
  • rememberCoroutineScope
    • ComposableLifecycle을 따르는 CoroutineScope 를 반환
    • Composable 이 파괴되면 자동으로 Coroutine Job 파괴
    • Composable 이 파괴될 때 파괴되는 Coroutine을 생성해야될 때에는 rememberCoroutineScope 사용
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.