Compose UI 설계

Compose의 UI는 변경할 수 없습니다. UI를 설계한 후 업데이트할 수 없습니다. UI 상태는 제어할 수 있습니다. UI 상태가 변경될 때마다 Compose는 변경된 UI 트리 부분을 다시 만듭니다. 컴포저블은 상태를 수락하고 이벤트를 노출할 수 있습니다. 예를 들어 TextField는 값을 수락하고 콜백 onValueChange를 노출합니다. 이 콜백은 값을 변경하기 위해 콜백 핸들러를 요청합니다.

var name by remember { mutableStateOf("") }
OutlinedTextField(
    value = name,
    onValueChange = { name = it },
    label = { Text("Name") }
)

컴포저블이 상태를 수락하고 이벤트를 노출하기 때문에 단방향 데이터 흐름 패턴은 Jetpack Compose에 적합합니다. 이 가이드에서는 Compose에서 단방향 데이터 흐름 패턴을 구현하는 방법, 이벤트 및 상태 홀더를 구현하는 방법, Compose에서 ViewModel을 사용하는 방법을 중점적으로 설명합니다.

단방향 데이터 흐름

단방향 데이터 흐름(UDF)은 상태는 아래로 이동하고 이벤트는 위로 이동하는 디자인 패턴입니다. 단방향 데이터 흐름을 따라 UI에 상태를 표시하는 컴포저블과 상태를 저장하고 변경하는 앱 부분을 서로 분리할 수 있습니다.

단방향 데이터 흐름을 사용하는 앱의 UI 업데이트 루프는 다음과 같습니다.

  • 이벤트: UI의 일부가 이벤트를 생성하여 위쪽으로 전달하거나(예: 처리하기 위해 ViewModel에 전달되는 버튼 클릭) 앱의 다른 레이어에서 이벤트가 전달됩니다(예: 사용자 세션이 종료되었음을 표시).
  • 상태 업데이트: 이벤트 핸들러가 상태를 변경할 수도 있습니다.
  • 상태 표시: 상태 홀더가 상태를 아래로 전달하고 UI가 상태를 표시합니다.

단방향 데이터 흐름

Jetpack Compose를 사용할 때 이 패턴을 따르면 몇 가지 이점이 있습니다.

  • 테스트 가능성: 상태와 상태를 표시하는 UI를 분리하여 격리 상태에서 더 쉽게 테스트할 수 있습니다.
  • 상태 캡슐화: 상태는 한 곳에서만 업데이트할 수 있고 컴포저블의 상태에 관한 정보 소스가 하나뿐이므로 일관되지 않은 상태로 인해 버그를 만들 가능성이 작습니다.
  • UI 일관성: 식별 가능한 상태 홀더(LiveData 또는 StateFlow)를 사용함으로써 모든 상태 업데이트가 UI에 즉시 반영됩니다.

Jetpack Compose의 단방향 데이터 흐름

컴포저블은 상태 및 이벤트를 기반으로 작동합니다. 예를 들어 TextFieldvalue 매개변수가 업데이트되고 onValueChange 콜백(값을 새 값으로 변경하도록 요청하는 이벤트)을 노출할 때만 업데이트됩니다. Compose는 State 객체를 값 홀더로 정의하며 상태 값이 변경되면 리컴포지션이 트리거됩니다. 값을 저장해야 하는 기간에 따라 remember { mutableStateOf(value) } 또는 rememberSaveable { mutableStateOf(value)에 상태를 유지할 수 있습니다.

TextField 컴포저블 값의 유형은 String이므로 하드코딩된 값, ViewModel, 상위 컴포저블에서 전달된 값 등 어디서나 가져올 수 있습니다. State 객체에 값을 유지할 필요는 없지만 onValueChange가 호출될 때 값을 업데이트해야 합니다.

컴포저블 매개변수 정의

컴포저블의 상태 매개변수를 정의할 때는 다음 사항을 고려해야 합니다.

  • 컴포저블의 재사용 가능성 또는 유연성
  • 상태 매개변수가 컴포저블의 성능에 미치는 영향

분리 및 재사용을 유도하기 위해 각 컴포저블에는 가능한 한 최소한의 정보를 포함해야 합니다. 예를 들어 뉴스 기사의 헤더를 저장하는 컴포저블을 빌드하는 경우 전체 뉴스 기사가 아니라 표시해야 하는 정보만 전달합니다.

@Composable
fun Header(title: String, subtitle: String) {
    // Recomposes when title or subtitle have changed.
}

@Composable
fun Header(news: News) {
    // Recomposes when a new instance of News is passed in.
}

개별 매개변수를 사용하면 성능이 향상되는 경우도 있습니다. 예를 들어 Newstitlesubtitle보다 더 많은 정보가 포함된 경우 titlesubtitle이 변경되지 않았더라도 News의 새 인스턴스가 Header(news)에 전달될 때마다 컴포저블이 재구성됩니다.

전달하는 매개변수의 수를 신중하게 고려하세요. 함수에 매개변수가 너무 많으면 함수의 인체공학적 특성이 감소하므로 이 경우 매개변수를 클래스로 그룹화하는 것이 좋습니다.

Compose의 이벤트

앱의 모든 입력은 탭, 텍스트 변경, 타이머 또는 기타 업데이트와 같은 이벤트로 표시해야 합니다. 이러한 이벤트에 따라 UI 상태가 변경되므로 ViewModel이 이벤트를 처리하고 UI 상태를 업데이트해야 합니다.

UI 레이어가 이벤트 핸들러 외부에서 상태를 변경하면 안 됩니다. 이렇게 하면 애플리케이션에 불일치 및 버그가 발생할 수 있습니다.

상태 및 이벤트 핸들러 람다에 변경할 수 없는 값을 전달하는 것이 좋습니다. 이 접근 방식에는 다음과 같은 이점이 있습니다.

  • 재사용 가능성이 개선됩니다.
  • UI가 상태의 값을 직접 변경하지 않습니다.
  • 상태가 다른 스레드에서 변경되지 않기 때문에 동시 실행 문제가 발생하지 않습니다.
  • 보통 코드 복잡성이 감소합니다.

예를 들어 String 및 람다를 매개변수로 사용할 수 있는 컴포저블은 많은 컨텍스트에서 호출할 수 있으며 재사용 가능성이 높습니다. 앱의 상단 앱 바에 항상 텍스트가 표시되고 뒤로 버튼이 있다고 가정해 보겠습니다. 텍스트 및 뒤로 버튼 핸들을 매개변수로 수신하는 더 일반적인 MyAppTopAppBar 컴포저블을 정의할 수 있습니다.

@Composable
fun MyAppTopAppBar(topAppBarText: String, onBackPressed: () -> Unit) {
    TopAppBar(
        title = {
            Text(
                text = topAppBarText,
                textAlign = TextAlign.Center,
                modifier = Modifier
                    .fillMaxSize()
                    .wrapContentSize(Alignment.Center)
            )
        },
        navigationIcon = {
            IconButton(onClick = onBackPressed) {
                Icon(
                        Icons.Filled.ArrowBack,
                        contentDescription = localizedString
                    )
            }
        },
        // ...
    )
}

ViewModel, 상태 및 이벤트: 예

다음 중 하나가 참인 경우 ViewModelmutableStateOf를 사용하여 앱에 단방향 데이터 흐름을 도입할 수도 있습니다.

  • UI의 상태가 LiveData를 통해 식별 가능한 상태 홀더 구현으로 노출됩니다.
  • ViewModel은 앱의 UI 또는 다른 레이어에서 발생하는 이벤트를 처리하고 이벤트를 기반으로 상태 홀더를 업데이트합니다.

예를 들어 로그인 화면을 구현할 때 로그인 버튼을 탭하면 앱에 진행률 스피너 및 네트워크 호출이 표시됩니다. 로그인에 성공하면 앱이 다른 화면으로 이동합니다. 오류가 발생한 경우 앱에 스낵바가 표시됩니다. 다음은 화면 상태와 이벤트를 모델링하는 방법입니다.

화면에는 네 가지 상태가 있습니다.

  • 로그아웃됨: 사용자가 아직 로그인하지 않음
  • 진행 중: 앱에서 현재 네트워크를 호출하여 사용자를 로그인하려고 하는 중임
  • 오류: 로그인하는 중에 오류가 발생함
  • 로그인됨: 사용자가 로그인함

이러한 상태를 봉인 클래스로 모델링할 수 있습니다. ViewModel은 상태를 State로 노출하고 초기 상태를 설정하고 필요에 따라 상태를 업데이트합니다. ViewModel은 또한 onSignIn() 메서드를 노출하여 로그인 이벤트를 처리합니다.

sealed class UiState {
    object SignedOut : UiState()
    object InProgress : UiState()
    object Error : UiState()
    object SignIn : UiState()
}

class MyViewModel : ViewModel() {
    private val _uiState = mutableStateOf<UiState>(SignedOut)
    val uiState: State<UiState>
        get() = _uiState

    // ...
}

mutableStateOf API 외에 Compose는 LiveData, Flow, Observable이 리스너로 등록하고 값을 상태로 표시할 수 있는 확장 프로그램을 제공합니다.

class MyViewModel : ViewModel() {
    private val _uiState = MutableLiveData<UiState>(SignedOut)
    val uiState: LiveData<UiState>
        get() = _uiState

    // ...
}

@Composable
fun MyComposable(viewModel: MyViewModel) {
    val uiState = viewModel.uiState.observeAsState()
    // ...
}