Compose의 상태 소개

1. 시작하기 전에

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

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

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

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

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

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

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

761df483de663721.png

기본 요건

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

학습할 내용

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

빌드할 항목

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

필요한 항목

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

2. 시작하기

  1. Google의 팁 계산기를 사용해 봅니다.

18da3c120daa0759.png

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

46bf4366edc1055f.png

C0980ba3e9ebba02.png

값을 입력하는 즉시 TipTotal이 업데이트됩니다.

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

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

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

3. 프로젝트 만들기

Android 스튜디오에서 Empty Compose Activity 템플릿과 필수 문자열 리소스를 사용하여 프로젝트를 설정합니다.

  1. Android 스튜디오에서 Empty Compose Activity 템플릿으로 프로젝트를 만들고 Tip Time을 이름으로 입력한 다음, API 21: Android 5.0 (Lollipop) 이상을 최소 SDK로 선택합니다. 프로젝트 파일이 로드됩니다.
  2. Project 창에서 res > values > strings.xml을 클릭합니다. 앱 이름에는 단일 문자열 리소스가 있습니다.
  3. <resources> 태그 사이에 다음 문자열 리소스를 입력합니다.
<string name="calculate_tip">Calculate Tip</string>
<string name="cost_of_service">Cost of Service</string>
<string name="tip_amount">Tip amount: %s</string>

strings.xml 파일은 다음 코드 스니펫과 같습니다.

strings.xml

<resources>
   <string name="app_name">TipTime</string>
   <string name="calculate_tip">Calculate Tip</string>
   <string name="cost_of_service">Cost of Service</string>
   <string name="tip_amount">Tip amount: %s</string>
</resources>

4. 화면 제목 추가

이 섹션에서는 구성 가능한 Text 함수를 사용하여 앱에 화면 제목을 추가합니다.

Greeting() 함수를 삭제하고 TipTimeScreen() 함수를 추가하여 앱에 필요한 UI 요소를 추가합니다.

  1. MainActivity.kt 파일에서 Greeting() 함수를 삭제합니다.
// Delete this.
@Composable
fun Greeting(name: String) {
   //...
}
  1. onCreate()DefaultPreview() 함수에서 Greeting() 함수 호출을 삭제합니다.
// Delete this.
Greeting("Android")
  1. onCreate() 함수 아래에 앱 화면을 표현하기 위한 구성 가능한 TipTimeScreen() 함수를 추가합니다.
@Composable
fun TipTimeScreen() {
}
  1. onCreate() 함수의 Surface() 블록에서 TipTimeScreen() 함수를 호출합니다.
override fun onCreate(savedInstanceState: Bundle?) {
   //...
   setContent {
       TipTimeTheme {
           Surface(
           //...
           ) {
               TipTimeScreen()
           }
       }
   }
}
  1. DefaultPreview() 함수의 TipTimeTheme 블록에서 TipTimeScreen() 함수를 호출합니다.
@Preview(showBackground = true)
@Composable
fun DefaultPreview() {
   TipTimeTheme {
       TipTimeScreen()
   }
}

화면 제목 표시

화면 제목을 표시하기 위한 TipTimeScreen() 함수를 구현합니다.

  1. TipTimeScreen() 함수에 Column 요소를 추가합니다. 요소가 세로 열로 되어 있으므로 Column 요소를 사용합니다.
  2. Column 블록에서 32.dp 인수를 허용하는 Modifier.padding 함수에 설정된 modifier라는 이름의 매개변수를 전달합니다.
Column(
   modifier = Modifier.padding(32.dp)
) {}
  1. 다음 함수와 이 속성을 가져옵니다.
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.ui.unit.dp
  1. Column 함수에서 8.dp 인수를 허용하는 Arrangement.spacedBy 함수에 설정된 verticalArrangement라는 이름의 인수를 전달합니다.
Column(
   modifier = Modifier.padding(32.dp),
   verticalArrangement = Arrangement.spacedBy(8.dp)
) {}

그러면 하위 요소 사이에 고정된 8dp 공백이 추가됩니다.

  1. 이 함수를 가져옵니다.
import androidx.compose.foundation.layout.Arrangement
  1. stringResource(R.string.calculate_tip) 함수에 설정된 text라는 이름의 매개변수를 사용하는 Text 요소와 24.sp 값에 설정된 fontSize라는 이름의 매개변수, Modifier.align(Alignment.CenterHorizontally) 함수에 설정된 modifier라는 이름의 인수를 추가합니다.
Text(
   text = stringResource(R.string.calculate_tip),
   fontSize = 24.sp,
   modifier = Modifier.align(Alignment.CenterHorizontally)
)
  1. 다음 가져오기를 불러옵니다.
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.sp
import androidx.compose.ui.Alignment
  1. Design 창에서 Build & Refresh를 클릭합니다. Calculate Tip이 추가한 텍스트 요소인 화면 제목으로 표시됩니다.

da56236494529e77.png

TextField 컴포저블 추가

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

58671affa01fb9e1.png

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

30d9c9123b5d26fe.png

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

  1. Column 블록에서 Text 요소 다음에 높이가 16dp인 구성 가능한 Spacer() 함수를 추가합니다.
@Composable
fun TipTimeScreen() {
   Column(
       modifier = Modifier.padding(32.dp),
       verticalArrangement = Arrangement.spacedBy(8.dp)
   ) {
       Text(
           ...
       )
       Spacer(Modifier.height(16.dp))
   }
}

그러면 화면 제목 다음에 빈 16dp 공간이 표시됩니다.

  1. 다음 함수를 가져옵니다.
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
  1. MainActivity.kt 파일에서 구성 가능한 EditNumberField() 함수를 추가합니다.
  2. EditNumberField() 함수의 본문에서 빈 문자열로 설정된 value라는 이름의 매개변수와 빈 람다 표현식으로 설정된 onValueChange라는 이름의 매개변수를 취하는 TextField를 추가합니다.
@Composable
fun EditNumberField() {
   TextField(
      value = "",
      onValueChange = {},
   )
}
  1. 전달한 매개변수를 확인합니다.
  • value 매개변수는 여기에서 전달하는 문자열 값을 표시하는 텍스트 상자입니다.
  • onValueChange 매개변수는 사용자가 텍스트 상자에 텍스트를 입력할 때 트리거되는 람다 콜백입니다.
  1. 이 함수를 가져옵니다.
import androidx.compose.material.TextField
  1. 구성 가능한 Spacer() 함수 다음 줄에서 EditNumberField() 함수를 호출합니다.
@Composable
fun TipTimeScreen() {
   Column(
       modifier = Modifier.padding(32.dp),
       verticalArrangement = Arrangement.spacedBy(8.dp)
   ) {
       Text(
           ...
       )
       Spacer(Modifier.height(16.dp))
       EditNumberField()
   }
}

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

  1. Design 창에서 be24da86724b252c.pngBuild & refresh를 클릭합니다. Calculate Tip 화면 제목과 빈 텍스트 상자가 표시되고 그 사이에는 16dp 공백이 있습니다.

1ff60ec32d3b15c1.png

5. Compose에서 상태 사용

앱의 상태는 시간이 지남에 따라 변할 수 있는 값입니다. 이 앱에서 상태는 서비스 비용입니다.

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

  1. EditNumberField() 함수 시작 부분에서 val 키워드를 사용하여, 정적 "0" 값에 할당된 amountInput 변수를 추가합니다.
val amountInput = "0"

이는 서비스 비용에 관한 앱 상태입니다.

  1. value라는 이름의 매개변수를 amountInput 값으로 설정합니다.
TextField(
   value = amountInput,
   onValueChange = {},
)
  1. 앱을 빌드하고 다시 실행합니다. 다음 이미지와 같이 상태 변수에 설정된 값이 텍스트 상자에 표시됩니다.

ba0f07ef1162855b.png

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

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

6. 컴포지션

앱의 컴포저블은 일부 텍스트, 공백, 텍스트 상자와 함께 열이 표시된 UI를 설명해 줍니다. 텍스트에는 Calculate tip 제목이 표시되고 Spacer의 높이는 16dp이며 텍스트 상자에는 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"

그렇게 하면 변경 가능한 상태가 됩니다.

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

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

var amountInput = mutableStateOf("0")

mutableStateOf() 함수는 초기 "0" 값을 State 객체에 래핑된 매개변수로 수신한 다음, value의 값을 관찰 가능한 상태로 만듭니다. 그 경우 Android 스튜디오에서 컴파일 경고가 표시됩니다. 하지만 곧 수정할 것입니다.

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

Compose는 상태 value 속성을 읽는 각 컴포저블을 추적하고 그 value가 변경되면 리컴포지션을 트리거합니다.

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

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

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

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

6cb691703cc7ecbf.gif

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

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

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

7. 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.mutableStateOf
import androidx.compose.runtime.remember
  1. 다음 함수를 직접 가져옵니다.
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue

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

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

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

270943a84f18572d.png

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

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

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

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

e225f2d67e9f2c40.png

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

다음 이미지에서 볼 수 있듯이, 텍스트를 입력하면 즉시 Compose가 리컴포지션을 트리거하고 EditNumberField() 함수의 onValueChange 콜백이 새 데이터와 함께 호출됩니다.

1d5e08d32052d02e.png

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

1f5db6ab5ca5b477.png

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

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

9. 디자인 수정

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

텍스트 상자에 라벨 추가

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

A2afd6c7fc547b06.png

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

  1. EditNumberField() 함수의 TextField() 구성 가능한 함수에서 빈 람다 표현식으로 설정된 label 이름의 매개변수를 추가합니다.
TextField(
//...
   label = { }
)
  1. 람다 표현식에서 stringResource(R.string.cost_of_service)를 허용하는 Text() 함수를 호출합니다.
label = { Text(stringResource(R.string.cost_of_service)) }
  1. TextField() 구성 가능한 함수에서 Modifier.fillMaxWidth()로 설정된 modifier 이름의 매개변수를 추가합니다.
TextField(
  // Other parameters
   modifier = Modifier.fillMaxWidth(),
)
  1. 다음을 가져옵니다.
import androidx.compose.foundation.layout.fillMaxWidth
  1. TextField() 구성 가능한 함수에서 true 값으로 설정된 singleLine 이름의 매개변수를 추가합니다.
TextField(
  // Other parameters
   singleLine = true,
)

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

  1. KeyboardOptions()에 설정된 keyboardOptions 매개변수를 추가합니다.
TextField(
  // Other parameters
   keyboardOptions = KeyboardOptions()
)

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

  1. 숫자 입력을 위해 키보드 유형을 숫자 키보드로 설정합니다. KeyboardOptions 함수에 KeyboardType.Number로 설정된 keyboardType 이름의 매개변수를 전달합니다.
TextField(
  // Other parameters
   keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number)
)

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

@Composable
fun EditNumberField() {
   var amountInput by remember { mutableStateOf("") }
   TextField(
       value = amountInput,
       onValueChange = { amountInput = it },
       label = { Text(stringResource(R.string.cost_of_service)) },
       modifier = Modifier.fillMaxWidth(),
       singleLine = true,
       keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number)

   )
}
  1. 다음을 가져옵니다.
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.foundation.text.KeyboardOptions
  1. 앱을 실행합니다.

다음 이미지에서 변경사항을 확인할 수 있습니다.

48368bf5df67af37.png

10. 팁 금액 표시

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

이 작업이 끝나면 앱은 다음과 같이 표시됩니다.

aaf86be8d13431f5.png

팁 금액 계산

서비스 비용과 팁 비율을 허용하고 팁 금액을 반환하는 함수를 정의하고 구현합니다.

  1. MainActivity.kt 파일에서 EditNumberField() 함수 다음에 private calculateTip() 함수를 추가합니다.
  2. amounttipPercent라는 이름의 매개변수를 추가합니다. 모두 Double형입니다. amount 매개변수가 서비스 비용을 전달합니다.
  3. tipPercent 매개변수를 15.0 기본 인수 값으로 설정합니다. 그러면 이제 기본 팁 값이 15%로 설정됩니다. 다음 Codelab에서 사용자의 팁 금액을 가져올 것입니다.
private fun calculateTip(
   amount: Double,
   tipPercent: Double = 15.0
) {
}
  1. 함수 본문에서 val 키워드를 사용하여, tipPercent 매개변수를 100 값으로 나누고 그 결과에 amount 매개변수를 곱하여 팁을 계산하는 tip 변수를 정의합니다.
private fun calculateTip(
   amount: Double,
   tipPercent: Double = 15.0
) {
   val tip = tipPercent / 100 * amount
}

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

  1. calculateTip() 함수 본문의 다음 줄에서 NumberFormat.getCurrencyInstance() 함수를 호출합니다.
NumberFormat.getCurrencyInstance()

이렇게 하면 숫자를 통화 형식으로 지정하는 데 사용할 수 있는 숫자 형식 지정 클래스가 제공됩니다.

  1. NumberFormat.getCurrencyInstance() 함수 호출에서 format() 메서드를 체인화하고 그것을 tip 변수에 매개변수로 전달합니다.
NumberFormat.getCurrencyInstance().format(tip)
  1. Android 스튜디오에서 메시지가 표시되면 다음 클래스를 가져옵니다.
import java.text.NumberFormat
  1. 마지막 단계는 함수에서 형식이 지정된 문자열을 반환하는 것입니다. String 유형을 반환하도록 함수 서명을 수정합니다. NumberFormat 문 앞에 return 키워드를 추가합니다.
private fun calculateTip(
   amount: Double,
   tipPercent: Double = 15.0
): String {
   val tip = tipPercent / 100 * amount
   return NumberFormat.getCurrencyInstance().format(tip)
}

이제 함수가 형식이 지정된 문자열을 반환합니다.

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

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

  1. EditNumberField() 구성 가능한 함수에서 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() {
   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.cost_of_service)) },
       modifier = Modifier.fillMaxWidth(),
       singleLine = true,
       keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number)
   )
}

계산된 팁 금액 표시

팁 금액을 계산하는 함수를 작성했습니다. 다음 단계에서는 계산된 팁 금액을 표시하는 Text 컴포저블을 추가하겠습니다.

97734d91a3844d22.png

  1. Column() 블록 끝에 있는 TipTimeScreen() 함수에서 Spacer() 컴포저블을 추가하고 24.dp 높이를 전달합니다.
@Composable
fun TipTimeScreen() {
   Column(
       //...
   ) {
       Text(
           //...
       )
       //...
       EditNumberField()
       Spacer(Modifier.height(24.dp))
   }
}

그러면 텍스트 입력란 뒤에 공백이 추가됩니다.

  1. Spacer() 컴포저블 뒤에 다음 Text 컴포저블을 추가합니다.
Text(
   text = stringResource(R.string.tip_amount, ""),
   modifier = Modifier.align(Alignment.CenterHorizontally),
   fontSize = 20.sp,
   fontWeight = FontWeight.Bold
)

이 코드는 tip_amount 문자열 리소스를 사용하여 텍스트를 설정하지만 팁 금액이 표시되지 않습니다. 곧 이 문제를 해결할 것입니다. 텍스트가 20.sp 크기로 화면의 중앙에 배치되고 두께는 굵게로 설정됩니다.

  1. 다음 가져오기를 불러옵니다.
import androidx.compose.ui.text.font.FontWeight

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

5ec86acdbfa1907b.png

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

e11d5bba4d8abd0d.png

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

11. 상태 호이스팅

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

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

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

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

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

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

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

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

상태 호이스팅은 구성요소를 스테이트리스(Stateless)로 만들기 위해 상태를 다른 함수 위로 옮기는 패턴입니다.

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

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

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

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

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

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

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

   Column(
       //...
   ) {
       //...
   }
}
  1. 상태를 TipTimeScreen()으로 호이스팅했으니, 이제 EditNumberField()에 전달합니다. TipTimeScreen() 함수에서 호이스팅한 상태를 사용하도록 EditNumberField() 함수 호출을 업데이트합니다.
EditNumberField(value = amountInput,
   onValueChange = { amountInput = it }
)
  1. tip 속성을 사용하여 팁 금액을 표시합니다. tip 변수를 매개변수로 사용하도록 Text 컴포저블의 text 매개변수를 업데이트합니다. 이를 위치 형식이라고 합니다.
Text(
   text = stringResource(R.string.tip_amount, tip),
   // Rest of the code
)

위치 형식을 사용하여 동적 콘텐츠를 문자열로 표시할 수 있습니다. 예를 들어 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>

// In your Compose code
Text(
    text = stringResource(R.string.tip_amount, tip)
)

자리표시자 인수의 개수와 유형은 상관없습니다. string 자리표시자는 %s입니다. Compose에서 형식이 지정된 tip을 stringResource() 함수에 인수로 전달합니다.

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

@Composable
fun TipTimeScreen() {
   var amountInput by remember { mutableStateOf("") }

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

   Column(
       modifier = Modifier.padding(32.dp),
       verticalArrangement = Arrangement.spacedBy(8.dp)
   ) {
       Text(
           text = stringResource(R.string.calculate_tip),
           fontSize = 24.sp,
           modifier = Modifier.align(Alignment.CenterHorizontally)
       )
       Spacer(Modifier.height(16.dp))
       EditNumberField(value = amountInput,
           onValueChange = { amountInput = it }
       )
       Spacer(Modifier.height(24.dp))
       Text(
           text = stringResource(R.string.tip_amount, tip),
           modifier = Modifier.align(Alignment.CenterHorizontally),
           fontSize = 20.sp,
           fontWeight = FontWeight.Bold
       )
   }

}

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

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

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

12. 솔루션 코드 가져오기

완료된 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에서 확인하세요.

13. 결론

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

요약

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

자세히 알아보기