Compose의 상태 소개

1. 시작하기 전에

이 Codelab에서는 상태 및 Jetpack Compose에서 상태를 사용하고 조작하는 방법에 관해 알아봅니다.

기본적으로 앱의 상태는 시간이 지남에 따라 변할 수 있는 값입니다. 이 정의는 데이터베이스부터 앱의 변수까지 모든 것이 포함될 정도로 매우 광범위합니다. 이후 단원에서 데이터베이스에 관해 자세히 살펴보겠지만, 지금은 데이터베이스가 컴퓨터에 있는 파일과 같이 구조화된 정보의 정리된 모음임을 아는 것만으로 충분합니다.

모든 Android 앱에서는 사용자에게 상태가 표시됩니다. 다음은 Android 앱 상태의 몇 가지 예시입니다.

  • 네트워크 연결을 설정할 수 없을 때 표시되는 메시지
  • 양식(예: 등록 양식): 상태를 작성하고 제출할 수 있습니다.
  • 버튼과 같이 탭할 수 있는 컨트롤: 상태는 탭하지 않음, 탭하는 중(디스플레이 애니메이션) 또는 탭함(onClick 동작) 중에 선택할 수 있습니다.

이 Codelab에서는 Compose를 사용할 때 상태를 사용하고 고려하는 방법을 살펴봅니다. 이를 위해 다음과 같이 기본으로 제공되는 Compose UI 요소를 사용하여 Tip Time이라는 팁 계산기 앱을 빌드합니다.

  • 텍스트를 입력하고 수정하는 TextField 컴포저블
  • 텍스트를 표시하는 Text 컴포저블
  • UI 요소 사이에 빈 공간을 표시하는 Spacer 컴포저블

이 Codelab을 마칠 무렵에는 서비스 금액을 입력하면 팁 금액을 자동으로 계산하는 대화형 팁 계산기가 빌드됩니다. 최종 앱의 모습은 다음 이미지와 같이 표시됩니다.

e82cbb534872abcf.png

기본 요건

  • @Composable 주석과 같은 Compose에 관한 기본 이해
  • RowColumn 레이아웃 컴포저블과 같은 Compose 레이아웃에 관한 기본 지식
  • Modifier.padding() 함수와 같은 수정자에 관한 기본 지식
  • Text 컴포저블에 관한 지식

학습할 내용

  • UI에서 상태를 고려하는 방법
  • Compose에서 상태를 사용하여 데이터를 표시하는 방법
  • 앱에 텍스트 상자를 추가하는 방법
  • 상태를 끌어올리는 방법

빌드할 항목

  • 서비스 금액을 기반으로 팁 금액을 계산하는 Tip Time이라는 팁 계산기 앱

필요한 항목

  • 인터넷 액세스가 가능하고 웹브라우저가 있는 컴퓨터
  • Kotlin 지식
  • 최신 버전의 Android 스튜디오

2. 시작하기

  1. Google의 온라인 팁 계산기를 사용해 봅니다. 이는 예시일 뿐이며 이 과정에서 만들게 될 Android 앱은 아닙니다.

46bf4366edc1055f.png 18da3c120daa0759.png

  1. BillTip % 입력란에 다른 값을 입력합니다. 팁과 총금액이 변경됩니다.

C0980ba3e9ebba02.png

값을 입력하는 즉시 TipTotal이 업데이트됩니다. 다음 Codelab을 마치면 Android에서 비슷한 팁 계산기 앱을 개발하게 됩니다.

이 과정에서는 간단한 팁 계산기 Android 앱을 빌드합니다.

개발자는 깔끔하게 보이지 않더라도 사용 가능하고 작동하는 간단한 버전의 앱을 준비한 후 기능을 추가하여 나중에 시각적으로 세련된 앱을 만드는 방식으로 작업하는 때가 많습니다.

이 Codelab을 마치고 나면 팁 계산기 앱은 다음 스크린샷과 같이 표시됩니다. 사용자가 청구 금액을 입력하면 앱에 추천 팁 금액이 표시됩니다. 현재 팁 비율은 15%로 하드코딩되어 있습니다. 다음 Codelab에서는 계속 앱 작업을 진행하면서 맞춤 팁 비율을 설정하는 등의 기능을 추가합니다.

3. 시작 코드 가져오기

시작 코드는 새 프로젝트의 시작점으로 사용할 수 있는 미리 작성된 코드입니다. 또한 이 Codelab에서 학습한 새로운 개념에 집중하는 데 도움이 될 수 있습니다.

다음에서 시작 코드를 다운로드하여 시작하세요.

GitHub 저장소를 클론하여 코드를 가져와도 됩니다.

$ git clone https://github.com/google-developer-training/basic-android-kotlin-compose-training-tip-calculator.git
$ cd basic-android-kotlin-compose-training-tip-calculator
$ git checkout starter

TipTime GitHub 저장소에서 시작 코드를 찾아볼 수 있습니다.

시작 앱 개요

시작 코드에 익숙해지려면 다음 단계를 완료하세요.

  1. Android 스튜디오에서 시작 코드가 있는 프로젝트를 엽니다.
  2. Android 기기나 에뮬레이터에서 앱을 실행합니다.
  3. 텍스트 구성요소가 두 개 표시됩니다. 하나는 라벨용이고 다른 하나는 팁 금액 표시용입니다.

e85b767a43c69a97.png

시작 코드 둘러보기

시작 코드에는 텍스트 컴포저블이 있습니다. 이 개발자 과정에서는 사용자가 입력할 수 있는 텍스트 필드를 추가합니다. 다음은 시작하는 데 도움이 되는 몇 가지 파일입니다.

res > values > strings.xml

<resources>
   <string name="app_name">Tip Time</string>
   <string name="calculate_tip">Calculate Tip</string>
   <string name="bill_amount">Bill Amount</string>
   <string name="tip_amount">Tip Amount: %s</string>
</resources>

이 파일은 앱에서 사용할 모든 문자열이 포함된 리소스의 string.xml 파일입니다.

MainActivity

이 파일에는 대부분 템플릿에서 생성된 코드와 다음 함수가 포함되어 있습니다.

  • TipTimeLayout() 함수에는 스크린샷에 표시된 두 개의 텍스트 컴포저블이 있는 Column 요소가 포함되어 있습니다. 또한 미적인 이유로 공백을 추가하는 spacer 컴포저블도 있습니다.
  • calculateTip() 함수는 청구 금액을 수락하고 15% 팁 금액을 계산합니다. tipPercent 매개변수는 15.0 기본 인수 값으로 설정됩니다. 그러면 이제 기본 팁 값이 15%로 설정됩니다. 다음 Codelab에서 사용자의 팁 금액을 가져올 것입니다.
@Composable
fun TipTimeLayout() {
    Column(
        modifier = Modifier
            .statusBarsPadding()
            .padding(horizontal = 40.dp)
            .verticalScroll(rememberScrollState())
            .safeDrawingPadding(),
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.Center
    ) {
        Text(
            text = stringResource(R.string.calculate_tip),
            modifier = Modifier
                .padding(bottom = 16.dp, top = 40.dp)
                .align(alignment = Alignment.Start)
        )
        Text(
            text = stringResource(R.string.tip_amount, "$0.00"),
            style = MaterialTheme.typography.displaySmall
        )
        Spacer(modifier = Modifier.height(150.dp))
    }
}
private fun calculateTip(amount: Double, tipPercent: Double = 15.0): String {
   val tip = tipPercent / 100 * amount
   return NumberFormat.getCurrencyInstance().format(tip)
}

onCreate() 함수의 Surface() 블록에서 TipTimeLayout() 함수가 호출됩니다. 이렇게 하면 기기나 에뮬레이터에 앱의 레이아웃이 표시됩니다.

override fun onCreate(savedInstanceState: Bundle?) {
   //...
   setContent {
       TipTimeTheme {
           Surface(
           //...
           ) {
               TipTimeLayout()
           }
       }
   }
}

TipTimeLayoutPreview() 함수의 TipTimeTheme 블록에서 TipTimeLayout() 함수가 호출됩니다. 그러면 DesignSplit 창에 앱의 레이아웃이 표시됩니다.

@Preview(showBackground = true)
@Composable
fun TipTimeLayoutPreview() {
   TipTimeTheme {
       TipTimeLayout()
   }
}

ae11354e61d2a2b9.png

사용자의 입력 받기

이 섹션에서는 사용자가 앱에 청구 금액을 입력할 수 있는 UI 요소를 추가합니다. 다음 이미지처럼 표시됩니다.

58671affa01fb9e1.png

앱에서 맞춤 스타일과 테마를 사용합니다.

스타일과 테마는 단일 UI 요소의 모양을 지정하는 속성의 모음입니다. 스타일은 글꼴 색상, 글꼴 크기, 배경 색상 등 앱 전체에 적용할 수 있는 속성을 지정할 수 있습니다. 이후 Codelab에서는 앱에서 이를 구현하는 방법을 알아봅니다. 지금은 앱을 더 멋지게 만들기 위해 이미 구현되어 있습니다.

이해를 돕기 위해 맞춤 테마가 적용된 앱과 적용되지 않은 앱의 솔루션 버전을 나란히 비교해 봤습니다.

맞춤 테마가 적용되지 않음

맞춤 테마가 적용됨

사용자는 구성 가능한 TextField 함수를 사용하여 앱에 텍스트를 입력할 수 있습니다. 예를 들어 다음 이미지처럼 Gmail 앱 로그인 화면에 텍스트 상자가 표시됩니다.

이메일 텍스트 입력란이 있는 Gmail 앱이 있는 휴대전화 화면

TextField 컴포저블을 앱에 추가합니다.

  1. MainActivity.kt 파일에서 Modifier 매개변수를 사용하는 구성 가능한 EditNumberField() 함수를 추가합니다.
  2. TipTimeLayout() 아래 EditNumberField() 함수의 본문에서 빈 문자열로 설정된 value라는 이름의 매개변수와 빈 람다 표현식으로 설정된 onValueChange라는 이름의 매개변수를 취하는 TextField를 추가합니다.
@Composable
fun EditNumberField(modifier: Modifier = Modifier) {
   TextField(
      value = "",
      onValueChange = {},
      modifier = modifier
   )
}
  1. 전달한 매개변수를 확인합니다.
  • value 매개변수는 여기에서 전달하는 문자열 값을 표시하는 텍스트 상자입니다.
  • onValueChange 매개변수는 사용자가 텍스트 상자에 텍스트를 입력할 때 트리거되는 람다 콜백입니다.
  1. 이 함수를 가져옵니다.
import androidx.compose.material3.TextField
  1. TipTimeLayout() 컴포저블의 첫 번째 텍스트 구성 가능한 함수 다음 줄에서 EditNumberField() 함수를 호출하여 다음 수정자를 전달합니다.
import androidx.compose.foundation.layout.fillMaxWidth

@Composable
fun TipTimeLayout() {
   Column(
        modifier = Modifier
            .statusBarsPadding()
            .padding(horizontal = 40.dp)
            .verticalScroll(rememberScrollState())
            .safeDrawingPadding(),
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.Center
   ) {
       Text(
           ...
       )
       EditNumberField(modifier = Modifier.padding(bottom = 32.dp).fillMaxWidth())
       Text(
           ...
       )
       ...
   }
}

다음과 같이 화면에 텍스트 상자가 표시됩니다.

  1. Design 창에 Calculate Tip 텍스트와 빈 텍스트 상자, Tip Amount 텍스트 컴포저블이 표시됩니다.

2c208378cd4b8d41.png

4. Compose에서 상태 사용

앱의 상태는 시간이 지남에 따라 변할 수 있는 값입니다. 이 앱에서 상태는 청구 금액입니다.

상태를 저장할 변수를 추가합니다.

  1. EditNumberField() 함수 시작 부분에서 val 키워드를 사용하여 amountInput 변수를 추가하고 "0" 값으로 설정합니다.
val amountInput = "0"

이는 청구 금액에 관한 앱의 상태입니다.

  1. value라는 이름의 매개변수를 amountInput 값으로 설정합니다.
TextField(
   value = amountInput,
   onValueChange = {},
)
  1. 미리보기를 확인합니다. 다음 이미지와 같이 상태 변수에 설정된 값이 텍스트 상자에 표시됩니다.

e8e24821adfd9d8c.png

  1. 에뮬레이터에서 앱을 실행하고 다른 값을 입력해 봅니다. TextField 컴포저블이 자체적으로 업데이트되지 않으므로 하드코딩된 상태는 변경되지 않습니다. 상태는 amountInput 속성으로 설정된 value 매개변수가 변경되면 업데이트됩니다.

amountInput 변수는 텍스트 상자의 상태를 나타냅니다. 하드코딩된 상태는 수정이 불가능해서 사용자 입력을 반영하지 못하므로 유용하지 않습니다. 사용자가 청구 금액을 업데이트할 때 앱 상태를 업데이트해야 합니다.

5. 컴포지션

앱의 컴포저블은 일부 텍스트, 공백, 텍스트 상자와 함께 열이 표시된 UI를 설명해 줍니다. 텍스트에는 Calculate Tip 텍스트가 표시되고 텍스트 상자에는 0 값 또는 기본값이 표시됩니다.

Compose는 선언형 UI 프레임워크로, UI의 모습을 코드로 선언하는 것입니다. 처음에 텍스트 상자에 100 값을 표시하려면 컴포저블 코드에서 초깃값을 100 값으로 설정합니다.

앱이 실행되는 동안 또는 사용자가 앱과 상호작용할 때 UI를 변경하고자 하면 어떻게 될까요? 예를 들어 사용자가 입력한 값으로 amountInput 변수를 업데이트하고 그것을 텍스트 상자에 표시하려고 하면 어떻게 될까요? 이때 리컴포지션이라는 프로세스를 사용하여 앱의 컴포지션을 업데이트할 것입니다.

컴포지션은 Compose가 컴포저블을 실행할 때 빌드한 UI에 관한 설명입니다. Compose 앱은 구성 가능한 함수를 호출하여 데이터를 UI로 변환합니다. 상태가 변경되면 Compose는 영향을 받는 구성 가능한 함수를 새 상태로 다시 실행합니다. 그러면 리컴포지션이라는 업데이트된 UI가 만들어집니다. Compose는 자동으로 리컴포지션을 예약합니다.

Compose는 초기 컴포지션 시 처음으로 컴포저블을 실행할 때 컴포지션에서 UI를 기술하기 위해 호출하는 컴포저블을 추적합니다. 리컴포지션은 Compose가 데이터 변경사항에 따라 변경될 수 있는 컴포저블을 다시 실행한 다음 변경사항을 반영하도록 컴포지션을 업데이트하는 것입니다.

컴포지션은 초기 컴포지션을 통해서만 생성되고 리컴포지션을 통해서만 업데이트될 수 있습니다. 컴포지션을 수정하는 유일한 방법은 리컴포지션을 통하는 것입니다. 이렇게 하려면 Compose가 추적할 상태를 알아야 합니다. 그래야 Compose가 업데이트를 받을 때 리컴포지션을 예약할 수 있습니다. 여기서는 추적할 상태가 amountInput 변수이므로 값이 변경될 때마다 Compose는 리컴포지션을 예약합니다.

Compose에서 StateMutableState 유형을 사용하여 앱의 상태를 Compose에서 관찰 가능하거나 추적 가능한 상태로 설정할 수 있습니다. State 유형은 변경할 수 없어 그 유형의 값만 읽을 수 있는 반면, MutableState 유형은 변경할 수 있습니다. mutableStateOf() 함수를 사용하여 관찰 가능한 MutableState를 만들 수 있습니다. 이 함수는 초깃값을 State 객체에 래핑된 매개변수로 수신한 다음, value의 값을 관찰 가능한 상태로 만듭니다.

mutableStateOf() 함수에서 반환하는 값은 다음과 같은 특성을 지닙니다.

  • 상태, 즉 청구 금액을 보유합니다.
  • 변경 가능하므로 값을 변경할 수 있습니다.
  • 관찰 가능하므로, Compose는 값의 변경을 관찰하고 리컴포지션을 트리거하여 UI를 업데이트합니다.

서비스 비용 상태를 추가합니다.

  1. EditNumberField() 함수에서 amountInput 상태 변수 앞에 있는 val 키워드를 var 키워드로 변경합니다.
var amountInput = "0"

이렇게 하면 amountInput이 변경 가능해집니다.

  1. Compose가 amountInput 상태 추적을 인식하도록 하드코딩된 String 변수 대신에 MutableState<String> 유형을 사용한 다음, amountInput 상태 변수의 초기 기본값인 "0" 문자열을 전달합니다.
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf

var amountInput: MutableState<String> = mutableStateOf("0")

amountInput 초기화는 유형을 추론하여 다음과 같이 작성할 수도 있습니다.

var amountInput = mutableStateOf("0")

mutableStateOf() 함수는 초깃값 "0"을 인수로 수신하여 amountInput을 관찰할 수 있도록 합니다. 그 경우 Android 스튜디오에서 컴파일 경고가 표시됩니다. 하지만 곧 수정할 것입니다.

Creating a state object during composition without using remember.
  1. 구성 가능한 TextField 함수에서 amountInput.value 속성을 사용합니다.
TextField(
   value = amountInput.value,
   onValueChange = {},
   modifier = modifier
)

Compose는 상태 value 속성을 읽는 각 구성 가능한 함수를 추적하고 value가 변경되면 재구성하도록 트리거합니다.

onValueChange 콜백은 텍스트 상자의 입력이 변경될 때 트리거됩니다. 람다 표현식의 it 변수에 새 값이 포함됩니다.

  1. onValueChange라는 매개변수의 람다 표현식에서 amountInput.value 속성을 it 변수로 설정합니다.
@Composable
fun EditNumberField(modifier: Modifier = Modifier) {
   var amountInput = mutableStateOf("0")
   TextField(
       value = amountInput.value,
       onValueChange = { amountInput.value = it },
       modifier = modifier
   )
}

TextField의 상태(즉, amountInput 변수)가 업데이트됩니다. 이때 onValueChange 콜백 함수를 통해 텍스트에 변경이 있다고 TextField에서 알려줍니다.

  1. 앱을 실행하고 텍스트 상자에 텍스트를 입력합니다. 다음 그림과 같이 텍스트 상자에 0 값이 계속 표시됩니다.

3a2c62f8ec55e339.gif

사용자가 텍스트 상자에 텍스트를 입력하면 onValueChange 콜백이 호출되고 amountInput 변수가 새 값으로 업데이트됩니다. Compose에 의해 amountInput 상태가 추적되므로 값이 변경되는 즉시 리컴포지션이 예약되고 EditNumberField() 구성 가능한 함수가 다시 실행됩니다. 이 구성 가능한 함수에서는 amountInput 변수가 초기 0 값으로 재설정됩니다. 그래서 텍스트 상자에 0 값이 표시되는 것입니다.

추가된 코드로 상태가 변경되면 리컴포지션이 예약됩니다.

그러나 리컴포지션에서 amountInput 변수의 값을 보존할 방법이 필요합니다. 그래야 EditNumberField() 함수가 재구성될 때마다 값이 0으로 재설정되지 않기 때문입니다. 이 문제는 다음 섹션에서 해결할 것입니다.

6. remember 함수를 사용하여 상태 저장

리컴포지션으로 인해 구성 가능한 메서드가 여러 번 호출될 수 있습니다. 저장되지 않은 경우 컴포저블은 리컴포지션 중에 상태를 재설정합니다.

구성 가능한 함수는 remember를 사용하여 리컴포지션에서 객체를 저장할 수 있습니다. remember 함수로 계산된 값은 초기 컴포지션 중에 컴포지션에 저장되고 저장된 값은 리컴포지션 중에 반환됩니다. 상태와 업데이트가 UI에 적절하게 반영되도록 일반적으로 구성 가능한 함수에 remembermutableStateOf 함수가 함께 사용됩니다.

EditNumberField() 함수에서 remember 함수를 사용합니다.

  1. EditNumberField() 함수에서 remembermutableStateOf() 함수 호출을 감싸는 방식으로 by remember Kotlin 속성 위임으로 amountInput 변수를 초기화합니다.
  2. mutableStateOf() 함수에서 정적 "0" 문자열 대신 빈 문자열을 전달합니다.
var amountInput by remember { mutableStateOf("") }

이제 빈 문자열이 amountInput 변수의 초기 기본값입니다. byKotlin 속성 위임에 해당합니다. amountInput 속성의 기본 getter 함수와 setter 함수가 remember 클래스의 getter 함수와 setter 함수에 각각 위임됩니다.

  1. 다음 함수를 가져옵니다.
import androidx.compose.runtime.remember
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue

위임의 getter 및 setter 가져오기를 추가하면 MutableStatevalue 속성을 참조하지 않고도 amountInput를 읽고 설정할 수 있습니다.

업데이트된 EditNumberField() 함수는 다음과 같습니다.

@Composable
fun EditNumberField(modifier: Modifier = Modifier) {
   var amountInput by remember { mutableStateOf("") }
   TextField(
       value = amountInput,
       onValueChange = { amountInput = it },
       modifier = modifier
   )
}
  1. 앱을 실행하고 텍스트 상자에 텍스트를 입력합니다. 이제 입력한 텍스트가 표시됩니다.

59ac301a208b47c4.png

7. 상태 및 리컴포지션의 작동 방식

이 섹션에서는 중단점을 설정하고 EditNumberField() 구성 가능한 함수를 디버그하여 초기 컴포지션 및 리컴포지션 작동 방식을 확인합니다.

에뮬레이터 또는 기기에서 중단점을 설정하고 앱을 디버그합니다.

  1. onValueChange 이름의 매개변수 옆에 있는 EditNumberField() 함수에서 줄 중단점을 설정합니다.
  2. 탐색 메뉴에서 Debug ‘app'을 클릭합니다. 앱이 에뮬레이터 또는 기기에서 실행됩니다. TextField 요소가 생성되면 앱 실행이 처음으로 일시중지됩니다.

154e060231439307.png

  1. Debug 창에서 2a29a3bad712bec.png Resume Program을 클릭합니다. 텍스트 상자가 만들어집니다.
  2. 에뮬레이터 또는 기기의 텍스트 상자에 문자를 입력합니다. 설정된 중단점에 도달하면 앱 실행이 다시 일시중지됩니다.

텍스트를 입력하면 onValueChange 콜백이 호출됩니다. 람다 it 내에는 키패드에 입력한 새 값이 있습니다.

'it' 값이 amountInput에 할당되면 Compose는 관찰 가능한 값이 변경됨에 따라 새 데이터로 리컴포지션을 트리거합니다.

1d5e08d32052d02e.png

  1. Debug 창에서 2a29a3bad712bec.png Resume Program을 클릭합니다. 에뮬레이터 또는 기기에 입력된 텍스트가 다음 이미지와 같이 중단점이 있는 줄 옆에 표시됩니다.

1f5db6ab5ca5b477.png

이는 텍스트 입력란의 상태입니다.

  1. 2a29a3bad712bec.png Resume Program을 클릭합니다. 입력한 값이 에뮬레이터나 기기에 표시됩니다.

8. 디자인 수정

이전 섹션에서는 텍스트 입력란을 완료했습니다. 이 섹션에서는 UI를 향상하는 작업을 하게 됩니다.

텍스트 상자에 라벨 추가

모든 텍스트 상자에는 사용자에게 입력 가능한 정보를 알려주는 라벨이 있어야 합니다. 다음 첫 번째 예제 이미지에서는 label 텍스트가 텍스트 입력란 중앙에 위치하며 입력줄에 맞게 정렬됩니다. 다음 두 번째 예제 이미지에서는 사용자가 텍스트 입력을 위해 텍스트 상자를 클릭하면 텍스트 입력란에서 label이 위로 이동합니다. 텍스트 입력란 구성에 관한 자세한 내용은 구성을 참고하세요.

A2afd6c7fc547b06.png

텍스트 입력란에 라벨을 추가하도록 EditNumberField() 함수를 수정합니다.

  1. EditNumberField() 함수의 TextField() 구성 가능한 함수에서 빈 람다 표현식으로 설정된 label 이름의 매개변수를 추가합니다.
TextField(
//...
   label = { }
)
  1. 람다 표현식에서 stringResource(R.string.bill_amount)를 허용하는 Text() 함수를 호출합니다.
label = { Text(stringResource(R.string.bill_amount)) },
  1. TextField() 구성 가능한 함수에서 true 값으로 설정된 singleLine 이름의 매개변수를 추가합니다.
TextField(
  // ...
   singleLine = true,
)

그렇게 하면 텍스트 상자가 여러 줄에서 가로로 스크롤 가능한 하나의 줄로 압축됩니다.

  1. KeyboardOptions()에 설정된 keyboardOptions 매개변수를 추가합니다.
import androidx.compose.foundation.text.KeyboardOptions

TextField(
  // ...
   keyboardOptions = KeyboardOptions(),
)

Android에는 화면에 표시되는 키보드를 구성하여 숫자, 이메일 주소, URL, 비밀번호 등을 입력할 수 있는 옵션이 있습니다. 다른 키보드 유형에 관한 자세한 내용은 KeyboardType을 참고하세요.

  1. 숫자 입력을 위해 키보드 유형을 숫자 키보드로 설정합니다. KeyboardOptions 함수에 KeyboardType.Number로 설정된 keyboardType 이름의 매개변수를 전달합니다.
import androidx.compose.ui.text.input.KeyboardType

TextField(
  // ...
   keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
)

완성된 EditNumberField() 함수는 다음 코드 스니펫과 같습니다.

@Composable
fun EditNumberField(modifier: Modifier = Modifier) {
    var amountInput by remember { mutableStateOf("") }
    TextField(
        value = amountInput,
        onValueChange = { amountInput = it },
        singleLine = true,
        label = { Text(stringResource(R.string.bill_amount)) },
        keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
        modifier = modifier
    )
}
  1. 앱을 실행합니다.

키패드의 변경사항은 다음 스크린샷에서 확인할 수 있습니다.

55936268bf007ee9.png

9. 팁 금액 표시

이 섹션에서는 앱의 기본 기능 즉, 팁 금액을 계산하고 표시하는 기능을 구현합니다.

MainActivity.kt 파일에서 시작 코드의 일부로 private calculateTip() 함수가 제공됩니다. 이 함수를 사용하여 팁 금액을 계산합니다.

private fun calculateTip(amount: Double, tipPercent: Double = 15.0): String {
    val tip = tipPercent / 100 * amount
    return NumberFormat.getCurrencyInstance().format(tip)
}

위 메서드에서는 NumberFormat을 사용하여 팁 형식을 통화로 표시합니다.

이제 앱에서 팁을 계산할 수 있지만 여전히 클래스를 사용하여 형식을 지정하고 표시해야 합니다.

calculateTip() 함수를 사용합니다.

사용자가 텍스트 입력란 컴포저블에 입력한 텍스트는 사용자가 숫자를 입력하더라도 onValueChange 콜백 함수에 String으로 반환됩니다. 이 문제를 해결하려면 사용자가 입력한 금액이 포함된 amountInput 값을 변환해야 합니다.

  1. EditNumberField() 구성 가능한 함수에서 amountInput 정의 뒤에 새 변수 amount를 만듭니다. amountInput 변수에서 toDoubleOrNull 함수를 호출하여 StringDouble로 변환합니다.
val amount = amountInput.toDoubleOrNull()

toDoubleOrNull() 함수는 사전 정의된 Kotlin 함수로, 문자열을 Double 숫자로 파싱하고 그 결과 또는 null(문자열이 유효한 숫자 표현이 아닌 경우)을 반환합니다.

  1. amountInput이 null이 되면 0.0 값을 반환하는 ?: Elvis 연산자를 문 끝에 추가합니다.
val amount = amountInput.toDoubleOrNull() ?: 0.0
  1. amount 변수 뒤에 tip이라는 다른 val 변수를 만듭니다. calculateTip()을 사용하여 그 변수를 초기화하고 amount 매개변수를 전달합니다.
val tip = calculateTip(amount)

EditNumberField() 함수는 다음 코드 스니펫과 같습니다.

@Composable
fun EditNumberField(modifier: Modifier = Modifier) {
   var amountInput by remember { mutableStateOf("") }

   val amount = amountInput.toDoubleOrNull() ?: 0.0
   val tip = calculateTip(amount)

   TextField(
       value = amountInput,
       onValueChange = { amountInput = it },
       label = { Text(stringResource(R.string.bill_amount)) },
       modifier = Modifier.fillMaxWidth(),
       singleLine = true,
       keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number)
   )
}

계산된 팁 금액 표시

팁 금액을 계산하는 함수를 작성했습니다. 다음 단계는 계산된 팁 금액을 표시하는 것입니다.

  1. Column() 블록 끝에 있는 TipTimeLayout() 함수에서 $0.00를 표시하는 텍스트 컴포저블을 확인합니다. 계산된 팁 금액으로 이 값을 업데이트합니다.
@Composable
fun TipTimeLayout() {
    Column(
        modifier = Modifier
            .statusBarsPadding()
            .padding(horizontal = 40.dp)
            .verticalScroll(rememberScrollState())
            .safeDrawingPadding(),
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.Center
    ) {
        // ...
        Text(
            text = stringResource(R.string.tip_amount, "$0.00"),
            style = MaterialTheme.typography.displaySmall
        )
        // ...
    }
}

팁 금액을 계산하고 표시하기 위해 TipTimeLayout() 함수의 amountInput 변수에 액세스해야 하지만, amountInput 변수가 구성 가능한 함수 EditNumberField()에 정의된 텍스트 입력란의 상태이므로 아직 TipTimeLayout() 함수에서 호출할 수 없습니다. 다음 이미지에는 코드 구조가 나와 있습니다.

50bf0b9d18ede6be.png

이 구조에서는 Text 컴포저블이 amountInput 변수에서 계산된 amount 변수에 액세스해야 하므로 새 Text 컴포저블에 팁 금액을 표시할 수 없습니다. amount 변수를 TipTimeLayout() 함수에 노출해야 합니다. 다음 이미지에는 EditNumberField() 컴포저블을 스테이트리스(Stateless)로 만드는 훌륭한 코드 구조가 나와 있습니다.

ab4ec72388149f7c.png

이 패턴을 상태 호이스팅이라고 합니다. 다음 섹션에서 컴포저블의 상태를 호이스팅 즉, 끌어올려 컴포저블을 스테이트리스(Stateless)로 만들 것입니다.

10. 상태 호이스팅

이 섹션에서는 컴포저블을 재사용하고 공유할 수 있는 방법으로 상태를 정의하기 위한 위치를 어떻게 결정할지 알아봅니다.

구성 가능한 함수에서는 UI에 표시할 상태를 보유하는 변수를 정의할 수 있습니다. 예를 들어 EditNumberField() 컴포저블에서는 amountInput 변수를 상태로 정의했습니다.

앱이 더 복잡해지고 다른 컴포저블이 EditNumberField() 컴포저블 내의 상태에 액세스해야 하는 경우 EditNumberField() 구성 가능한 함수에서 상태를 호이스팅하거나 추출해야 합니다.

스테이트풀(Stateful)과 스테이트리스(Stateless) 컴포저블 비교

다음과 같은 경우 상태를 호이스팅해야 합니다.

  • 상태를 여러 구성 가능한 함수와 공유하는 경우
  • 앱에서 재사용할 수 있는 스테이트리스(Stateless) 컴포저블을 만드는 경우

구성 가능한 함수에서 상태를 추출할 때 결과로 생성되는 구성 가능한 함수를 스테이트리스(Stateless) 함수라고 합니다. 즉, 구성 가능한 함수에서 상태를 추출하여 구성 가능한 함수를 스테이트리스(Stateless)로 만들 수 있습니다.

스테이트리스(Stateless) 컴포저블은 상태가 없는 컴포저블입니다. 즉, 새 상태를 보유하거나 정의하거나 수정하지 않습니다. 반면 스테이트풀(Stateful) 컴포저블은 시간이 지남에 따라 변할 수 있는 상태를 소유하는 컴포저블입니다.

상태 호이스팅은 구성요소를 스테이트리스(Stateless)로 만들기 위해 상태를 호출자로 이동하는 패턴입니다.

이 패턴이 컴포저블에 적용되는 경우 컴포저블에 매개변수 2개가 추가될 때가 많습니다.

  • value: T 매개변수 - 표시할 현재 값
  • onValueChange: (T) -> Unit - 사용자가 텍스트 상자에 텍스트를 입력하는 경우 등 값이 변경될 때 상태가 업데이트될 수 있도록 트리거되는 콜백 람다입니다.

EditNumberField() 함수에서 상태를 호이스팅합니다.

  1. 상태 호이스팅을 위해 valueonValueChange 매개변수를 추가하여 EditNumberField() 함수 정의를 업데이트합니다.
@Composable
fun EditNumberField(
   value: String,
   onValueChange: (String) -> Unit,
   modifier: Modifier = Modifier
) {
//...

value 매개변수는 String 유형이고 onValueChange 매개변수는 (String) -> Unit 유형입니다. 따라서 String 값을 입력으로 사용하고 반환 값이 없는 함수입니다. onValueChange 매개변수는 TextField 컴포저블에 전달된 onValueChange 콜백으로 사용됩니다.

  1. EditNumberField() 함수에서 전달된 매개변수를 사용하도록 TextField() 구성 가능한 함수를 업데이트합니다.
TextField(
   value = value,
   onValueChange = onValueChange,
   // Rest of the code
)
  1. 상태를 호이스팅하고 저장된 상태를 EditNumberField() 함수에서 TipTimeLayout() 함수로 이동합니다.
@Composable
fun TipTimeLayout() {
   var amountInput by remember { mutableStateOf("") }

   val amount = amountInput.toDoubleOrNull() ?: 0.0
   val tip = calculateTip(amount)
  
   Column(
       //...
   ) {
       //...
   }
}
  1. 상태를 TipTimeLayout()으로 호이스팅했으니, 이제 EditNumberField()에 전달합니다. TipTimeLayout() 함수에서 호이스팅한 상태를 사용하도록 EditNumberField() 함수 호출을 업데이트합니다.
EditNumberField(
   value = amountInput,
   onValueChange = { amountInput = it },
   modifier = Modifier
       .padding(bottom = 32.dp)
       .fillMaxWidth()
)

이렇게 하면 EditNumberField가 스테이트리스(Stateless)가 됩니다. UI 상태를 상위 TipTimeLayout()으로 호이스팅했습니다. 이제 TipTimeLayout()이 상태(amountInput) 소유자입니다.

위치 형식 지정

위치 형식 지정은 문자열로 동적 콘텐츠를 표시하는 데 사용됩니다. 예를 들어 Tip amount 텍스트 상자에 xx.xx 값을 표시하려고 한다고 가정해 보겠습니다. 이 값은 함수에서 계산되고 형식이 지정된 모든 값에 해당할 수 있습니다. strings.xml 파일에서 이를 실행하려면 다음 코드 스니펫과 같이 자리표시자 인수가 있는 문자열 리소스를 정의해야 합니다.

// No need to copy.

// In the res/values/strings.xml file
<string name="tip_amount">Tip Amount: %s</string>

Compose 코드에서는 유형과 상관없이 자리표시자 인수를 여러 개 보유할 수 있습니다. string 자리표시자는 %s입니다.

TipTimeLayout()의 텍스트 컴포저블을 확인하고 형식이 지정된 팁을 stringResource() 함수의 인수로 전달합니다.

// No need to copy
Text(
   text = stringResource(R.string.tip_amount, "$0.00"),
   style = MaterialTheme.typography.displaySmall
)
  1. TipTimeLayout() 함수에서 tip 속성을 사용하여 팁 금액을 표시합니다. tip 변수를 매개변수로 사용하도록 Text 컴포저블의 text 매개변수를 업데이트합니다.
Text(
     text = stringResource(R.string.tip_amount, tip),
     // ...

완성된 TipTimeLayout()EditNumberField() 함수는 다음 코드 스니펫과 같습니다.

@Composable
fun TipTimeLayout() {
   var amountInput by remember { mutableStateOf("") }
   val amount = amountInput.toDoubleOrNull() ?: 0.0
   val tip = calculateTip(amount)

   Column(
       modifier = Modifier
            .statusBarsPadding()
            .padding(horizontal = 40.dp)
            .verticalScroll(rememberScrollState())
            .safeDrawingPadding(),
       horizontalAlignment = Alignment.CenterHorizontally,
       verticalArrangement = Arrangement.Center
   ) {
       Text(
           text = stringResource(R.string.calculate_tip),
           modifier = Modifier
               .padding(bottom = 16.dp, top = 40.dp)
               .align(alignment = Alignment.Start)
       )
       EditNumberField(
           value = amountInput,
           onValueChange = { amountInput = it },
           modifier = Modifier
               .padding(bottom = 32.dp)
               .fillMaxWidth()
       )
       Text(
           text = stringResource(R.string.tip_amount, tip),
           style = MaterialTheme.typography.displaySmall
       )
       Spacer(modifier = Modifier.height(150.dp))
   }
}

@Composable
fun EditNumberField(
   value: String,
   onValueChange: (String) -> Unit,
   modifier: Modifier = Modifier
) {
   TextField(
       value = value,
       onValueChange = onValueChange,
       singleLine = true,
       label = { Text(stringResource(R.string.bill_amount)) },
       keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
       modifier = modifier
   )
}

요약하면 amountInput 상태를 EditNumberField()에서 TipTimeLayout() 컴포저블로 호이스팅했습니다. 텍스트 상자가 이전처럼 작동하려면 EditNumberField() 구성 가능한 함수에 두 개의 인수를 전달해야 합니다. 하나는 amountInput 값이고, 다른 하나는 사용자 입력의 amountInput 값을 업데이트하는 람다 콜백입니다. 이렇게 변경하면 TipTimeLayout()amountInput 속성에서 팁을 계산하여 사용자에게 표시할 수 있습니다.

  1. 에뮬레이터나 기기에서 앱을 실행한 다음 청구 금액 텍스트 상자에 값을 입력합니다. 다음 이미지와 같이 청구 금액의 15%인 팁 금액이 표시됩니다.

de593783dc813e24.png

11. 솔루션 코드 가져오기

완료된 Codelab의 코드를 다운로드하려면 다음 git 명령어를 사용하면 됩니다.

$ git clone https://github.com/google-developer-training/basic-android-kotlin-compose-training-tip-calculator.git
$ cd basic-android-kotlin-compose-training-tip-calculator
$ git checkout state

또는 ZIP 파일로 저장소를 다운로드한 다음 압축을 풀고 Android 스튜디오에서 열어도 됩니다.

솔루션 코드를 보려면 GitHub에서 확인하세요.

12. 결론

축하합니다. 이 Codelab을 완료하고 Compose 앱에서 상태를 사용하는 방법을 알아봤습니다.

요약

  • 앱의 상태는 시간이 지남에 따라 변할 수 있는 값입니다.
  • 컴포지션은 Compose가 컴포저블을 실행할 때 빌드한 UI에 관한 설명입니다. Compose 앱은 구성 가능한 함수를 호출하여 데이터를 UI로 변환합니다.
  • 초기 컴포지션은 Compose가 구성 가능한 함수를 처음 실행할 때 UI가 생성된 것입니다.
  • 리컴포지션은 동일한 컴포저블을 다시 실행하여 데이터가 변경될 때 트리를 업데이트하는 프로세스입니다.
  • 상태 호이스팅은 구성요소를 스테이트리스(Stateless)로 만들기 위해 상태를 호출자로 이동하는 패턴입니다.

자세히 알아보기