Jetpack Compose(3) — 상태 관리

목차- 1. 상태란 무엇인가

    1. Compose에서의 상태 State
  • 2.1 State
  • 2.2 remember
  • 2.3 rememberSaveable
    1. Stateless와 Stateful
  • 3.1 상태 상향 이동
    1. 상태 관리 방법
  • 4.1 stateful을 이용한 상태 관리
  • 4.2 StateHolder를 이용한 상태 관리
  • 4.3 ViewModel을 이용한 상태 관리
  • 4.4 LiveData, Rxjava, Flow를 State로 변환
    1. 요약

이전 글에서 TextField 컴포넌트 예시를 들 때 State 즉 상태에 대해 언급했습니다. 본 글에서는 State의 관련 개념에 대해 설명합니다.

  1. 상태란 무엇인가 =======

다른 선언형 UI 프레임워크와 마찬가지로 Compose의 역할은 매우 단순합니다. 데이터 상태에 반응하는 것만 담당합니다. 데이터 상태가 변경되지 않으면 UI는 스스로 변경되지 않습니다. Compose에서 각 컴포넌트는 @Composable로 수정된 함수이며, 해당 상태는 함수의 매개변수입니다. 매개변수가 변경되지 않으면 함수의 출력도 변경되지 않으며, 유일한 매개변수가 유일한 출력을 결정합니다. 반대로, 인터페이스를 변경하려면 인터페이스의 상태를 변경한 다음 Composable이 이러한 변경에 반응해야 합니다. 아래 예시를 통해 간단한 카운터를 만들어 보겠습니다. 카운터를 표시하는 컨트롤, 증가 버튼(클릭할 때마다 카운터가 1 증가), 감소 버튼(클릭할 때마다 카운터가 1 감소)이 있습니다. 이전 View 시스템으로 이 방법을 작성한다면 코드는 다음과 같을 것입니다:

class MainActivity : AppCompatActivity() {
    // ...
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // ...
        binding.incrementBtn.setOnClickListener {
            binding.tvCounter.text = "${Integer.valueOf(binding.tvCounter.text.toString()) + 1 }"
        }

        binding.decrementBtn.setOnClickListener {
            binding.tvCounter.text = "${Integer.valueOf(binding.tvCounter.text.toString()) - 1 }"
        }
    }
}

명백히 위 코드에서 카운터 로직과 UI의 결합도가 매우 높습니다. 약간 최적화해 보겠습니다:

class MainActivity : AppCompatActivity() {
    // ...
    private var counter: Int = 0

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // ...
        binding.incrementBtn.setOnClickListener {
            counter++
            updateCounter()
        }

        binding.decrementBtn.setOnClickListener {
            counter--
            updateCounter()
        }
    }

    private fun updateCounter() {
        binding.tvCounter.text = "$counter"
    }
}

이 코드의 주요 변경 사항은 counter를 카운팅에 사용했다는 점입니다. 이는 본질적으로 "상태 상향 이동"에 해당합니다. 원래 TextView 내부의 상태 "mText"를 Activity로 상향 이동시켰습니다. 이렇게 하면 카운터 UI를 변경하더라도 카운터 로직을 여전히 재사용할 수 있습니다.

하지만 현재 코드에는 여전히 몇 가지 문제점이 있습니다. 예를 들어, 카운터 로직이 Activity에 있어 다른 페이지에서 재사용할 수 없습니다. 이를 개선하기 위해 MVVM 구조를 도입하여 ViewModel로 상태를 Activity에서 상향 이동시켜 보겠습니다.

class CounterViewModel: ViewModel() {
    private var _counter: MutableStateFlow<Int> = MutableStateFlow(0)
    val counter: StateFlow<Int> get() = _counter

    fun incrementCounter() {
        _counter.value++
    }

    fun decrementCounter() {
        _counter.value--
    }
}

class MainActivity : AppCompatActivity() {
    // ...
    private val viewModel: CounterViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // ...
        binding.incrementBtn.setOnClickListener {
            viewModel.incrementCounter()
        }

        binding.decrementBtn.setOnClickListener {
            viewModel.decrementCounter()
        }

        lifecycleScope.launch {
            repeatOnLifecycle(Lifecycle.State.STARTED) {
                viewModel.counter.collect {
                    binding.tvCounter.text = $it
                }
            }
        }
    }
}

Jetpack 라이브러리 사용 경험이 있는 분들은 위 코드가 매우 익숙하실 겁니다. 상태를 ViewModel로 상향 이동시키고 StateFlow 또는 LiveData로 래핑한 다음 Activity에서 상태 변경을 감시하여 UI를 자동으로 새로 고칩니다.

이제 Compose에서 위 카운터를 구현해 보겠습니다:

@Composable
fun CounterPage() {
    Column(horizontalAlignment = Alignment.CenterHorizontally) {
        var counter = 0
        Text(text = "$counter")
        Button(onClick = { counter++ }) {
            Text(text = "증가")
        }
        Button(onClick = { counter-- }) {
            Text(text = "감소")
        }
    }
}

위 코드를 작성하고 실행해 보면 Text에 표시되는 값은 항상 0으로, 카운터 로직이 작동하지 않는 것을 알 수 있습니다. 이 문제를 설명하기 위해 로그를 추가해 보겠습니다:

@Composable
fun CounterPage() {
    Column(horizontalAlignment = Alignment.CenterHorizontally) {
        var counter = 0
        Log.d("ComposeExample", "카운터 텍스트 --> $counter")
        Text(text = "$counter")
        Button(onClick = {
            Log.d("ComposeExample", "증가 버튼 클릭")
            counter++
        }) {
            Text(text = "증가")
        }
        Button(onClick = {
            Log.d("ComposeExample", "감소 버튼 클릭")
            counter--
        }) {
            Text(text = "감소")
        }
    }
}

다시 실행하고 버튼을 클릭하면 다음과 같은 로그가 표시됩니다:

2024-03-12 21:39:27.530 21949-21949 ComposeExample             com.example.hellocompose             D  카운터 텍스트 --> 0
2024-03-12 21:39:30.859 21949-21949 ComposeExample             com.example.hellocompose             D  증가 버튼 클릭 
2024-03-12 21:39:31.309 21949-21949 ComposeExample             com.example.hellocompose             D  감소 버튼 클릭 
2024-03-12 21:39:31.468 21949-21949 ComposeExample             com.example.hellocompose             D  감소 버튼 클릭 
2024-03-12 21:39:31.762 21949-21949 ComposeExample             com.example.hellocompose             D  증가 버튼 클릭 
2024-03-12 21:39:31.927 21949-21949 ComposeExample             com.example.hellocompose             D  증가 버튼 클릭 
2024-03-12 21:39:32.661 21949-21949 ComposeExample             com.example.hellocompose             D  감소 버튼 클릭 

다시 한번 정리해 보겠습니다. Compose 컴포넌트는 실제로 함수들입니다. Compose가 UI를 새로 고치는 로직은 상태 변경이 재구성을 트리거하고 함수가 다시 호출되는 것입니다. 매개변수가 변경되어 함수 출력이 변경되면 최종적으로 렌더링되는 화면이 변경됩니다. 위 코드를 다시 보면, 우리는 Text 컴포넌트의 상태로 counter를 정의하고 Button을 클릭하여 counter를 변경하려고 했습니다. 여기까지는 문제가 없습니다. 그렇다면 문제는 어디에 있을까요? 주된 문제는 counter가 변경되어도 재구성이 트리거되지 않았다는 것입니다. 즉, 함수가 다시 호출되지 않았고, 로그도 이를 증명합니다. 이전 전통적인 View 시스템 방식을 다시 살펴보면, 우리는 상태를 변경한 후 updateCounter 메서드를 명시적으로 호출하여 UI를 새로 고쳤습니다. 나중에 ViewModel로 상태를 상향 이동시켰을 때, StateFlow나 LiveData로 래핑한 후 Activity에서 상태 변경을 감시해야 상태 변경에 반응할 수 있었습니다. 위 예시에서 우리는 이제 Compose가 counter 변경을 감지하지 못해 카운터가 작동하지 않는 이유를 명확히 알 수 있습니다. 이제 Compose의 상태에 대해 배워야 합니다.

  1. Compose에서의 상태 State ====================

2.1 State

전통적인 View에서 StateFlow나 LiveData를 사용하여 상태 변수를 관찰 가능한 객체로 래핑해야 합니다. Compose에서도 관찰 가능한 상태 유형인 MutableState와 불변 상태 유형인 State를 제공합니다. State/MutableState를 사용하여 상태 변수를 래핑하면 재구성을 트리거할 수 있습니다. 더 편리한 점은 선언형 UI 프레임워크에서 상태 변경을 명시적으로 등록할 필요가 없으며, 프레임워크가 자동으로 이러한 구독 관계를 구현한다는 것입니다. 위 코드를 다음과 같이 수정해 보겠습니다:

@Composable
fun CounterPage() {
    Column(horizontalAlignment = Alignment.CenterHorizontally) {
        val counter: MutableState<Int> = mutableStateOf(0)
        Log.d("ComposeExample", "카운터 텍스트 --> ${counter.value}")
        Text(text = "${counter.value}")
        Button(onClick = {
            Log.d("ComposeExample", "증가 버튼 클릭")
            counter.value++
        }) {
            Text(text = "증가")
        }
        Button(onClick = {
            Log.d("ComposeExample", "감소 버튼 클릭")
            counter.value--
        }) {
            Text(text = "감소")
        }
    }
}

mutableStateOf() 메서드를 사용하여 MutableState 유형의 상태 변수를 초기화하고 기본값 0을 전달했습니다. 사용할 때는 counter.value를 호출해야 합니다. 다시 실행해 보면 버튼을 클릭해도 카운터 값이 여전히 변경되지 않는 것을 알 수 있습니다. 로그는 다음과 같습니다:

2024-03-12 21:57:24.773  6791-6791  ComposeExample             com.example.hellocompose             D  카운터 텍스트 --> 0
2024-03-12 21:57:31.428  6791-6791  ComposeExample             com.example.hellocompose             D  증가 버튼 클릭 
2024-03-12 21:57:31.437  6791-6791  ComposeExample             com.example.hellocompose             D  카운터 텍스트 --> 0
2024-03-12 21:57:31.825  6791-6791  ComposeExample             com.example.hellocompose             D  감소 버튼 클릭 
2024-03-12 21:57:31.834  6791-6791  ComposeExample             com.example.hellocompose             D  카운터 텍스트 --> 0
2024-03-12 21:57:33.047  6791-6791  ComposeExample             com.example.hellocompose             D  증가 버튼 클릭 
2024-03-12 21:57:33.055  6791-6791  ComposeExample             com.example.hellocompose             D  카운터 텍스트 --> 0
2024-03-12 21:57:33.216  6791-6791  ComposeExample             com.example.hellocompose             D  증가 버튼 클릭 
2024-03-12 21:57:33.224  6791-6791  ComposeExample             com.example.hellocompose             D  카운터 텍스트 --> 0
2024-03-12 21:57:33.634  6791-6791  ComposeExample             com.example.hellocompose             D  감소 버튼 클릭 
2024-03-12 21:57:33.643  6791-6791  ComposeExample             com.example.hellocompose             D  카운터 텍스트 --> 0
2024-03-12 21:57:33.792  6791-6791  ComposeExample             com.example.hellocompose             D  감소 버튼 클릭 
2024-03-12 21:57:33.801  6791-6791  ComposeExample             com.example.hellocompose             D  카운터 텍스트 --> 0

이전과 다르게 이번에는 버튼을 클릭한 후 Text(text = "${counter.value}")가 다시 실행되었음을 알 수 있습니다. 즉, 재구성이 발생했지만 매개변수는 여전히 0으로 변경되지 않았습니다. 여기에는 재구성 범위에 대한 개념이 관련되어 있습니다. 재구성에는 범위가 있으며, 재구성 범위에 대해서는 나중에 설명하겠습니다. 여기서 알아야 할 것은 재구성이 발생하면 Text(text = "${counter.value}")가 다시 실행되고 val counter: MutableState<Int> = mutableStateOf(0)도 다시 실행된다는 것입니다. 즉, 재구성 시 counter가 다시 초기화되고 기본값 0이 할당됩니다. 따라서 버튼을 클릭하여 재구성이 발생했지만 카운터 값이 변경되지 않았습니다. 이 문제를 해결하려면 Compose의 중요한 함수 remember를 사용해야 합니다.

2.2 remember

먼저 remember 함수의 소스 코드를 살펴보겠습니다:

/**
 * [calculation]에 의해 생성된 값을 기억합니다. [calculation]은 합성 중에만 평가됩니다.
 * 재구성은 항상 합성에 의해 생성된 값을 반환합니다.
 */
@Composable
inline fun <T> remember(crossinline calculation: @DisallowComposableCalls () -> T): T =
    currentComposer.cache(false, calculation)

remember 메서드는 래핑된 변수 값을 캐싱하여 후속 재구성 과정에서 다시 초기화하지 않고 캐시에서 직접 가져옵니다. 구체적인 사용법은 다음과 같습니다:

@Composable
fun CounterPage() {
    Column(horizontalAlignment = Alignment.CenterHorizontally) {
        val counter: MutableState<Int> = remember { mutableStateOf(0) }
        Log.d("ComposeExample", "카운터 텍스트 --> ${counter.value}")
        Text(text = "${counter.value}")
        Button(onClick = {
            Log.d("ComposeExample", "증가 버튼 클릭")
            counter.value++
        }) {
            Text(text = "증가")
        }
        Button(onClick = {
            Log.d("ComposeExample", "감소 버튼 클릭")
            counter.value--
        }) {
            Text(text = "감소")
        }
    }
}

다시 실행해 보면 이번에는 정상적으로 작동합니다.

로그도 올바릅니다. 매번 클릭할 때마다 재구성이 트리거되고 counter 값도 다시 초기화되지 않았습니다.

2024-03-12 22:18:53.744 19790-19790 ComposeExample             com.example.hellocompose             D  카운터 텍스트 --> 0
2024-03-12 22:19:10.397 19790-19790 ComposeExample             com.example.hellocompose             D  증가 버튼 클릭 
2024-03-12 22:19:10.421 19790-19790 ComposeExample             com.example.hellocompose             D  카운터 텍스트 --> 1
2024-03-12 22:19:10.967 19790-19790 ComposeExample             com.example.hellocompose             D  증가 버튼 클릭 
2024-03-12 22:19:10.981 19790-19790 ComposeExample             com.example.hellocompose             D  카운터 텍스트 --> 2
2024-03-12 22:19:11.181 19790-19790 ComposeExample             com.example.hellocompose             D  증가 버튼 클릭 
2024-03-12 22:19:11.195 19790-19790 ComposeExample             com.example.hellocompose             D  카운터 텍스트 --> 3
2024-03-12 22:19:11.649 19790-19790 ComposeExample             com.example.hellocompose             D  증가 버튼 클릭 
2024-03-12 22:19:11.663 19790-19790 ComposeExample             com.example.hellocompose             D  카운터 텍스트 --> 4
2024-03-12 22:19:11.806 19790-19790 ComposeExample             com.example.hellocompose             D  증가 버튼 클릭 
2024-03-12 22:19:11.821 19790-19790 ComposeExample             com.example.hellocompose             D  카운터 텍스트 --> 5
2024-03-12 22:19:12.364 19790-19790 ComposeExample             com.example.hellocompose             D  감소 버튼 클릭 
2024-03-12 22:19:12.377 19790-19790 ComposeExample             com.example.hellocompose             D  카운터 텍스트 --> 4
2024-03-12 22:19:12.640 19790-19790 ComposeExample             com.example.hellocompose             D  감소 버튼 클릭 
2024-03-12 22:19:12.657 19790-19790 ComposeExample             com.example.hellocompose             D  카운터 텍스트 --> 3
2024-03-12 22:19:13.204 19790-19790 ComposeExample             com.example.hellocompose             D  증가 버튼 클릭 
2024-03-12 22:19:13.220 19790-19790 ComposeExample             com.example.hellocompose             D  카운터 텍스트 --> 4
2024-03-12 22:19:13.747 19790-19790 ComposeExample             com.example.hellocompose             D  감소 버튼 클릭 
2024-03-12 22:19:13.761 19790-19790 ComposeExample             com.example.hellocompose             D  카운터 텍스트 --> 3

위 코드에서 State를 생성하는 방법은 다음과 같습니다:

val counter: MutableState<Int> = remember { mutableStateOf(0) }

사용할 때 counter.value를 사용해야 하는데, 이러한 코드는 매우 번거롭습니다. 다음과 같이 더 간결하게 작성할 수 있습니다. 먼저 Kotlin은 유형 추론을 지원하므로 다음과 같이 작성할 수 있습니다:

val counter = remember { mutableStateOf(0) }

또한 Kotlin 위임 구문을 활용하여 Compose는 위임 방식으로 할당을 구현했으며, by 키워드를 사용합니다. 사용법은 다음과 같습니다:

var counter by remember { mutableStateOf(0) }

다음과 같은 메서드를 가져와야 합니다:

import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue

사용 시 counter++counter--를 직접 사용할 수 있습니다.

주의할 점은 위임 방식으로 객체를 생성하지 않은 경우 유형은 MutableState 유형이며 val로 선언합니다. 위임 방식으로 객체를 생성하면 객체 유형은 MutableState로 래핑된 객체 유형입니다. 여기서 초기값이 0으로 할당되었으므로 유형 추론에 따라 counter는 Int 유형입니다. counter의 값을 수정해야 하므로 var을 사용하여 가변 유형 객체로 선언해야 합니다.

2.3 rememberSaveable

remember를 사용하면 재구성 과정에서 상태가 다시 초기화되는 문제는 해결되지만, Activity가 파괴되고 다시 생성될 때(예: 가로/세로 회전, UiMode 전환 등) 상태 값은 여전히 다시 초기화됩니다. 전통적인 View 시스템에서도 이러한 문제가 존재하며, 이에 대한 해결책은 여러 가지가 있습니다. 예를 들어 Activity 콜백 메서드를 재정의하여 적절한 시점에 데이터를 저장하고 복원하거나 ViewModel에 데이터를 저장하는 방법 등이 있습니다. 이러한 방법들은 Compose에서도 유효하지만, Compose를 사용할 때 Activity 생명주기 개념을 약화해야 하므로 전자는 Compose에 적합하지 않습니다. ViewModel 사용은 여전히 훌륭한 선택이며, 후문에서 소개하겠습니다. 하지만 모든 데이터를 ViewModel에 저장하는 것이 최선인지는 구체적인 시나리오에 따라 판단해야 합니다. 예를 들어, 이러한 시나리오를 해결하기 위해 Compose는 rememberSaveable 메서드를 제공합니다.

var counter by rememberSaveable { mutableStateOf(0) }

사용법은 remember 메서드와 유사하며, 차이점은 rememberSaveable이 가로/세로 회전, UiMode 전환 등의 시나리오에서 래핑된 데이터를 캐싱할 수 있다는 점입니다. 그렇다면 rememberSaveable을 모든 시나리오에서 remember 대신 사용할 수 있으며 remember 메서드가 더 이상 필요하지 않은 것일까요? rememberSaveable 메서드는 remember 메서드보다 기능이 더 강력하지만 성능은 다소 떨어집니다. 실제 시나리오에 따라 선택적으로 사용해야 합니다.

여기까지 상태 관련 지식은 명확해야 합니다. 다시 이전 글의 TextField 컴포넌트를 보면 왜 그렇게 작성했는지 이해할 수 있을 것입니다.

  1. Stateless와 Stateful =======================

선언형 UI 컴포넌트는 일반적으로 Stateless 컴포넌트와 Stateful 컴포넌트로 나눌 수 있습니다. Stateless는 이 컴포넌트가 매개변수 외에 다른 상태에 의존하지 않는다는 의미입니다. 예를 들어 Text 컴포넌트,

Text("안녕하세요, Compose")

반대로, 특정 컴포넌트가 매개변수 외에 외부 상태를 가지거나 액세스하는 경우 Stateful 컴포넌트라고 합니다. 예를 들어 이전 글에서 언급된 TextField 컴포넌트,

var text by remember { mutableStateOf("텍스트 필드 초기값") }
TextField(value = text, onValueChange = {
    text = it
})

Stateless는 외부 상태에 의존하지 않고 전달된 매개변수만 의존합니다. 이것은 "순수 함수"이며, 즉 유일한 입력이 유일한 출력에 해당합니다. 매개변수가 변경되지 않으면 UI는 변경되지 않으며, 재구성은 상위 호출에서만 발생할 수 있습니다. 따라서 Compose 컴파일러는 매개변수가 변경되지 않을 때 재구성에 참여하지 않도록 최적화했습니다. 재구성 범위는 Stateless 외부로 제한됩니다. 또한 Stateless는 어떤 비즈니스와도 결합되지 않아 기능이 더 순수하고 재사용성이 더 좋으며 테스트도 더 쉽습니다. 이에 기반하여 우리는 가능한 한 Stateful 컴포넌트를 Stateless 컴포넌트로 변환해야 합니다. 이 과정을 상태 상향 이동이라고 합니다.

3.1 상태 상향 이동

상태 상향 이동의 일반적인 방법은 내부 상태를 제거하고 매개변수 형식으로 전달하는 것입니다. 그리고 호출자에게 콜백해야 하는 이벤트도 매개변수 형식으로 전달합니다. 위 카운터 코드를 예로 들어 설명하겠습니다. 간결성을 위해 앞서 추가한 로그를 제거하면 코드는 다음과 같습니다:

@Composable
fun CounterPage() {
    Column(horizontalAlignment = Alignment.CenterHorizontally) {
        var counter by remember{ mutableStateOf(0) }
        Text(text = "$counter")
        Button(onClick = {
            counter++
        }) {
            Text(text = "증가")
        }
        Button(onClick = {
            counter--
        }) {
            Text(text = "감소")
        }
    }
}

여기서 카운터는 주로 내부 상태 counter에 의존하며, 두 버튼의 클릭 이벤트는 counter를 변경합니다. 상태를 상향 이동하면 다음과 같습니다:

@Composable
fun CounterPage(count: Int, onIncrease: () -> Unit, onDecrease: () -> Unit) {
    Column(horizontalAlignment = Alignment.CenterHorizontally) {
        Text(text = "$count")
        Button(onClick = {
            onIncrease()
        }) {
            Text(text = "증가")
        }
        Button(onClick = {
            onDecrease()
        }) {
            Text(text = "감소")
        }
    }
}

이렇게 하면 Counter 컴포넌트는 더 이상 비즈니스와 결합되지 않은 Stateless 컴포넌트가 되어 책임이 더 단순해지고 재사용성과 테스트성이 향상됩니다. 또한 상태 상향 이동은 단일 데이터 소스 모델 구축에 도움이 됩니다.

  1. 상태 관리 방법 ======

Compose에서 상태를 어떻게 관리해야 하는지 살펴보겠습니다.

4.1 stateful을 이용한 상태 관리

단순한 UI 상태이며 비즈니스와 관련이 없는 상태는 Compose에서 직접 관리하는 것이 적합합니다. 예를 들어 메뉴 목록이 있고 스위치를 클릭하면 메뉴가 확장되고 다시 클릭하면 메뉴가 접힙니다. 목록 상태는 단일 이벤트(스위치 클릭)에 의해서만 결정됩니다. 또한 목록 상태는 외부 비즈니스와 아무 관련이 없습니다. 이러한 경우 Compose 내부에서 관리하는 것이 적합합니다.

4.2 StateHolder를 이용한 상태 관리

비즈니스의 복잡도가 일정 수준에 이르면 관련된 상태를 StateHolder로 통합하여 관리할 수 있습니다. UI 로직을 분리하여 Composable이 UI 레이아웃에만 집중하도록 합니다.

4.3 ViewModel을 의한 상태 관리

어떤 의미에서 ViewModel은 특수한 종류의 StateHolder입니다. 하지만 ViewModelStore에 저장되므로 다음과 같은 특징이 있습니다:

  • 수명 주기가 길어 Composition과 독립적으로 존재할 수 있으며 모든 Composable과 공유할 수 있습니다.
  • 가로/세로 회전이나 UiMode 전환으로 인해 데이터가 손실되지 않습니다.

따라서 ViewModel은 애플리케이션 전역 상태를 관리하는 데 적합하며, ViewModel은 비 UI 비즈니스 상태를 관리하는 데 더 적합합니다.

위 관리 방식은 동시에 사용할 수 있으며, 구체적인 비즈니스에 따라 유연하게 조합할 수 있습니다.

4.4 LiveData, Rxjava, Flow를 State로 변환

MVVM 아키텍처에서 ViewModel을 사용하여 상태를 관리하는 경우, 새 프로젝트라면 상태를 State 유형으로 정의하면 됩니다.

전통적인 View 프로젝트의 경우 일반적으로 LiveData, RxJava 또는 Flow와 같은 반응형 데이터 프레임워크를 사용합니다. Compose에서는 State를 사용하여 재구성을 트리거하고 UI를 새로 고쳐야 하며, 위 반응형 데이터 흐름을 Compose의 State로 변환하는 방법도 있습니다. 상위 데이터가 변경될 때 Composable이 재구성을 완료하도록 구동할 수 있습니다. 구체적인 방법은 다음과 같습니다:

확장 메서드 의존 라이브러리
LiveData.observeAsState() androidx.compose:runtime-livedata
Flow.collectAsState() 타사 라이브러리 불필요, Compose 기본 제공
Observable.subscribeAsState() androidx.compose:runtime-rxjava2 또는 androidx.compose:runtime-rxjava3
  1. 요약 ====

본 글에서는 Compose에서 상태의 개념에 대해 주로 설명했습니다. 마지막으로 요약해 보겠습니다.

  • Compose UI는 상태 변경에 의존하여 재구성을 트리거하고 인터페이스 업데이트를 구동합니다.
  • remember와 rememberSaveable를 사용하여 상태를 지속화합니다. remember는 재구성 과정에서 상태 안정성을 보장하고, rememberSaveable는 Activity가 자동으로 파괴되고 다시 생성되는 과정에서 상태 안정성을 보장합니다.
  • 상태 상향 이동을 통해 가능한 한 Stateful 컴포넌트를 Stateless 컴포넌트로 변환합니다.
  • Stateful, StateHolder, ViewModel을 사용하여 상태를 관리하는 방식을 상황에 맞게 선택합니다.
  • LiveData, RxJava, Flow 데이터 흐름을 State로 변환합니다.

태그: Jetpack Compose 상태 관리 Android UI Compose 상태 remember

5월 22일 11:28에 게시됨