1. 시작하기 전에
이 Codelab에서는 Compose의 상태 소개 Codelab의 솔루션 코드를 사용하여, 청구 금액과 팁 비율을 입력할 때 자동으로 팁 금액을 계산하여 반올림할 수 있는 대화형 팁 계산기를 빌드합니다. 최종 앱은 다음 이미지와 같이 표시됩니다.
기본 요건
- Compose의 상태 소개 Codelab
- 앱에
Text
및TextField
컴포저블을 추가하는 방법 숙지 remember()
함수, 상태, 상태 호이스팅, 구성 가능한 스테이트풀(Stateful) 및 스테이트리스(Stateless) 함수의 차이점에 관한 지식
학습할 내용
- 가상 키보드에 작업 버튼을 추가하는 방법
Switch
컴포저블의 정의 및 사용 방법- 텍스트 필드에 선행 아이콘 추가
빌드할 항목
- 사용자가 입력한 청구 금액과 팁 비율을 기반으로 팁 금액을 계산하는 Tip Time 앱
필요한 항목
- 최신 버전의 Android 스튜디오
- Compose의 상태 소개 Codelab의 솔루션 코드
2. 시작 코드 가져오기
시작하려면 시작 코드를 다운로드하세요.
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 state
Tip Time
GitHub 저장소에서 코드를 둘러볼 수 있습니다.
3. 시작 앱 개요
이 Codelab은 이전 Compose의 상태 소개 Codelab의 Tip Time 앱으로 시작합니다. 이 앱은 고정 팁 비율로 팁을 계산하는 데 필요한 사용자 인터페이스를 제공합니다. Bill Amount 텍스트 상자를 통해 사용자가 서비스 비용을 입력할 수 있습니다. 앱은 팁 금액을 계산하여 Text
컴포저블에 표시합니다.
Tip Time 앱 실행
- Android 스튜디오에서 Tip Time 프로젝트를 열고 에뮬레이터 또는 기기에서 앱을 실행합니다.
- 청구 금액을 입력합니다. 앱이 자동으로 팁 금액을 계산하여 표시합니다.
현재 구현에서는 팁 비율이 15%로 하드코딩되어 있습니다. 이 Codelab에서는 앱이 맞춤 팁 비율을 계산하여 팁 금액을 반올림할 수 있는 텍스트 필드로 이 기능을 확장합니다.
필요한 문자열 리소스 추가
- Project 탭에서 res > values > strings.xml을 클릭합니다.
strings.xml
파일의<resources>
태그 사이에 다음 문자열 리소스를 추가합니다.
<string name="how_was_the_service">Tip Percentage</string>
<string name="round_up_tip">Round up tip?</string>
strings.xml
파일은 다음 코드 스니펫과 같이 표시되며 여기에는 이전 Codelab의 문자열이 포함되어 있습니다.
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="how_was_the_service">Tip Percentage</string>
<string name="round_up_tip">Round up tip?</string>
<string name="tip_amount">Tip Amount: %s</string>
</resources>
4. 팁 비율 텍스트 필드 추가
고객은 제공된 서비스의 품질과 기타 여러 이유를 근거로 팁을 많이 주거나 적게 주려고 할 수 있습니다. 이를 위해 앱에서는 사용자가 맞춤 팁을 계산할 수 있도록 해야 합니다. 이 섹션에서는 다음 이미지와 같이 사용자가 맞춤 팁 비율을 입력하는 텍스트 필드를 추가합니다.
앱에는 이미 구성 가능한 스테이트리스(Stateless) EditNumberField()
함수인 Bill Amount 텍스트 필드 컴포저블이 있습니다. 이전 Codelab에서는 amountInput
상태를 EditNumberField()
컴포저블에서 TipTimeLayout()
컴포저블로 끌어올려 EditNumberField()
컴포저블을 스테이트리스(Stateless)로 만들었습니다.
텍스트 필드를 추가하려면 동일한 EditNumberField()
컴포저블을 재사용하되 다른 라벨을 사용하면 됩니다. 이렇게 변경하려면 구성 가능한 EditNumberField()
함수 내에서 라벨을 하드코딩하는 대신 매개변수로 라벨을 전달해야 합니다.
구성 가능한 EditNumberField()
함수를 재사용 가능하도록 설정하려면 다음 안내를 따르세요.
MainActivity.kt
파일에서 구성 가능한EditNumberField()
함수의 매개변수에Int
유형의label
문자열 리소스를 추가합니다.
@Composable
fun EditNumberField(
label: Int,
value: String,
onValueChanged: (String) -> Unit,
modifier: Modifier = Modifier
)
- 함수 본문에서 하드코딩된 문자열 리소스 ID를
label
매개변수로 바꿉니다.
@Composable
fun EditNumberField(
//...
) {
TextField(
//...
label = { Text(stringResource(label)) },
//...
)
}
label
매개변수가 문자열 리소스 참조여야 함을 나타내려면 함수 매개변수에@StringRes
주석을 추가합니다.
@Composable
fun EditNumberField(
@StringRes label: Int,
value: String,
onValueChanged: (String) -> Unit,
modifier: Modifier = Modifier
)
- 다음을 가져옵니다.
import androidx.annotation.StringRes
- 구성 가능한
TipTimeLayout()
함수의EditNumberField()
함수 호출에서label
매개변수를R.string.bill_amount
문자열 리소스로 설정합니다.
EditNumberField(
label = R.string.bill_amount,
value = amountInput,
onValueChanged = { amountInput = it },
modifier = Modifier.padding(bottom = 32.dp).fillMaxWidth()
)
- Preview 창에는 시각적 변경사항이 없습니다.
- 구성 가능한
TipTimeLayout()
함수에서EditNumberField()
함수 호출 뒤에 맞춤 팁 비율에 사용할 텍스트 필드를 하나 더 추가합니다. 다음 매개변수를 사용하여 구성 가능한EditNumberField()
함수를 호출합니다.
EditNumberField(
label = R.string.how_was_the_service,
value = "",
onValueChanged = { },
modifier = Modifier.padding(bottom = 32.dp).fillMaxWidth()
)
이렇게 하면 맞춤 팁 비율용 텍스트 상자가 추가됩니다.
- 이제 앱 미리보기에 다음 이미지와 같이 Tip Percentage 텍스트 필드가 표시됩니다.
- 구성 가능한
TipTimeLayout()
함수 상단에, 추가된 텍스트 필드의 상태 변수에 사용할tipInput
이라는var
속성을 추가합니다.mutableStateOf("")
를 사용하여 변수를 초기화하고remember
함수로 호출을 둘러쌉니다.
var tipInput by remember { mutableStateOf("") }
- 새
EditNumberField
()
함수 호출에서 이름이value
인 매개변수를tipInput
변수로 설정하고onValueChanged
람다 표현식에서tipInput
변수를 업데이트합니다.
EditNumberField(
label = R.string.how_was_the_service,
value = tipInput,
onValueChanged = { tipInput = it },
modifier = Modifier.padding(bottom = 32.dp).fillMaxWidth()
)
TipTimeLayout()
함수에서tipInput
변수의 정의 뒤에tipInput
변수를Double
유형으로 변환하는tipPercent
라는val
을 정의합니다. Elvis 연산자를 사용하고 값이null
인 경우0
을 반환합니다. 이 값은 텍스트 필드가 비어 있으면null
일 수 있습니다.
val tipPercent = tipInput.toDoubleOrNull() ?: 0.0
TipTimeLayout()
함수에서calculateTip()
함수 호출을 업데이트하고tipPercent
변수를 두 번째 매개변수로 전달합니다.
val tip = calculateTip(amount, tipPercent)
이제 TipTimeLayout()
함수의 코드가 다음 코드 스니펫과 같이 표시됩니다.
@Composable
fun TipTimeLayout() {
var amountInput by remember { mutableStateOf("") }
var tipInput by remember { mutableStateOf("") }
val amount = amountInput.toDoubleOrNull() ?: 0.0
val tipPercent = tipInput.toDoubleOrNull() ?: 0.0
val tip = calculateTip(amount, tipPercent)
Column(
modifier = Modifier.padding(40.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text(
text = stringResource(R.string.calculate_tip),
modifier = Modifier
.padding(bottom = 16.dp)
.align(alignment = Alignment.Start)
)
EditNumberField(
label = R.string.bill_amount,
value = amountInput,
onValueChanged = { amountInput = it },
modifier = Modifier
.padding(bottom = 32.dp)
.fillMaxWidth()
)
EditNumberField(
label = R.string.how_was_the_service,
value = tipInput,
onValueChanged = { tipInput = 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))
}
}
- 에뮬레이터나 기기에서 앱을 실행하고 청구 금액과 팁 비율을 입력합니다. 앱에서 팁 금액을 올바르게 계산하나요?
5. 작업 버튼 설정
이전 Codelab에서는 KeyboardOptions
클래스를 사용하여 키보드 유형을 설정하는 방법을 알아봤습니다. 이 섹션에서는 동일한 KeyboardOptions
로 키보드 작업 버튼을 설정하는 방법을 알아봅니다. 키보드 작업 버튼은 키보드 끝에 있는 버튼입니다. 다음 표에서 몇 가지 예시를 확인할 수 있습니다.
속성 | 키보드의 작업 버튼 |
| |
| |
|
이 작업에서는 텍스트 상자에 다음 두 가지 작업 버튼을 설정합니다.
- Bill Amount 텍스트 상자의 Next 작업 버튼: 사용자가 현재 입력을 완료했고 다음 텍스트 상자로 이동하려고 함을 나타냅니다.
- Tip Percentage 텍스트 상자의 Done 작업 버튼: 사용자가 입력을 완료했음을 나타냅니다.
다음 이미지에서 이러한 작업 버튼이 있는 키보드의 예시를 확인할 수 있습니다.
키보드 옵션을 추가합니다.
EditNumberField()
함수의TextField()
함수 호출에서KeyboardOptions
생성자에ImeAction.Next
값으로 설정된imeAction
이라는 인수를 전달합니다.KeyboardOptions.Default.copy()
함수를 사용하여 다른 기본 옵션을 사용하는지 확인합니다.
import androidx.compose.ui.text.input.ImeAction
@Composable
fun EditNumberField(
//...
) {
TextField(
//...
keyboardOptions = KeyboardOptions.Default.copy(
keyboardType = KeyboardType.Number,
imeAction = ImeAction.Next
)
)
}
- 에뮬레이터나 기기에서 앱을 실행합니다. 다음 이미지와 같이 이제 키보드에 Next 작업 버튼이 표시됩니다.
Tip Percentage 텍스트 필드가 선택되면 키보드에 동일한 Next 작업 버튼이 표시됩니다. 그러나 텍스트 필드에는 두 가지 작업 버튼이 있어야 합니다. 이 문제는 곧 수정합니다.
EditNumberField()
함수를 검사합니다.TextField()
함수의keyboardOptions
매개변수가 하드코딩됩니다. 텍스트 필드에 다른 작업 버튼을 만들려면KeyboardOptions
객체를 인수로 전달해야 하며 이 작업은 다음 단계에서 실행합니다.
// No need to copy, just examine the code.
fun EditNumberField(
@StringRes label: Int,
value: String,
onValueChanged: (String) -> Unit,
modifier: Modifier = Modifier
) {
TextField(
//...
keyboardOptions = KeyboardOptions.Default.copy(
keyboardType = KeyboardType.Number,
imeAction = ImeAction.Next
)
)
}
EditNumberField()
함수 정의에서KeyboardOptions
유형의keyboardOptions
매개변수를 추가합니다. 함수 본문에서TextField()
함수의keyboardOptions
라는 매개변수에 할당합니다.
@Composable
fun EditNumberField(
@StringRes label: Int,
keyboardOptions: KeyboardOptions,
// ...
){
TextField(
//...
keyboardOptions = keyboardOptions
)
}
TipTimeLayout()
함수에서 첫 번째EditNumberField()
함수 호출을 업데이트하고 Bill Amount 텍스트 필드의keyboardOptions
라는 매개변수를 전달합니다.
EditNumberField(
label = R.string.bill_amount,
keyboardOptions = KeyboardOptions.Default.copy(
keyboardType = KeyboardType.Number,
imeAction = ImeAction.Next
),
// ...
)
- 두 번째
EditNumberField()
함수 호출에서 Tip Percentage 텍스트 필드의imeAction
을ImeAction.Done
으로 변경합니다. 함수는 다음 코드 스니펫과 같이 표시됩니다.
EditNumberField(
label = R.string.how_was_the_service,
keyboardOptions = KeyboardOptions.Default.copy(
keyboardType = KeyboardType.Number,
imeAction = ImeAction.Done
),
// ...
)
- 앱을 실행합니다. Next 및 Done 작업 버튼이 다음 이미지와 같이 표시됩니다.
- 청구 금액을 입력하고 Next 작업 버튼을 클릭한 후 팁 비율을 입력하고 Done 작업 버튼을 클릭합니다. 그러면 키패드가 닫힙니다.
6. 스위치 추가
스위치는 단일 항목의 상태를 켜거나 끕니다.
전환 스위치에는 두 가지 상태가 있으며 사용자는 두 옵션 중에서 선택할 수 있습니다. 전환 스위치는 다음 이미지와 같이 트랙과 thumb 및 선택적 아이콘으로 구성됩니다.
스위치는 다음 이미지와 같이 결정을 입력하거나 설정과 같은 환경설정을 선언하는 데 사용할 수 있는 선택 컨트롤입니다.
사용자는 thumb을 앞뒤로 드래그하여 옵션을 선택하거나 간단히 스위치를 탭하여 전환할 수 있습니다. 다음 GIF에서 보여주는 또 다른 전환 스위치 예시에서는 시각적 옵션 설정이 어두운 모드로 전환됩니다.
스위치에 관한 자세한 내용은 스위치 문서를 참고하세요.
Switch
컴포저블을 사용하면 다음 이미지와 같이 사용자가 팁을 가장 가까운 정수로 반올림할지 선택할 수 있습니다.
Text
및 Switch
컴포저블의 행을 추가합니다.
EditNumberField()
함수 뒤에 구성 가능한RoundTheTipRow()
함수를 추가한 다음 기본Modifier
를EditNumberField()
함수와 비슷한 인수로 전달합니다.
@Composable
fun RoundTheTipRow(modifier: Modifier = Modifier) {
}
RoundTheTipRow()
함수를 구현하고 다음modifier
를 사용하여Row
레이아웃 컴포저블을 추가하여 하위 요소의 너비를 화면에 최대로 설정하고 정렬을 가운데로 설정하고 크기가48dp
가 되도록 합니다.
Row(
modifier = modifier
.fillMaxWidth()
.size(48.dp),
verticalAlignment = Alignment.CenterVertically
) {
}
- 다음을 가져옵니다.
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.size
Row
레이아웃 컴포저블의 람다 블록에서R.string.round_up_tip
문자열 리소스를 사용하여Round up tip?
문자열을 표시하는Text
컴포저블을 추가합니다.
Text(text = stringResource(R.string.round_up_tip))
Text
컴포저블 뒤에Switch
컴포저블을 추가하고checked
라는 매개변수를 전달한 후roundUp
으로 설정하고onCheckedChange
라는 매개변수를 전달하고onRoundUpChanged
로 설정합니다.
Switch(
checked = roundUp,
onCheckedChange = onRoundUpChanged,
)
다음 표에는 이러한 매개변수에 관한 정보가 포함되어 있으며 RoundTheTipRow()
함수에 정의한 매개변수와 같습니다.
매개변수 | 설명 |
| 스위치 선택 여부를 나타냅니다. |
| 스위치를 클릭할 때 호출될 콜백입니다. |
- 다음을 가져옵니다.
import androidx.compose.material3.Switch
RoundTheTipRow()
함수에서Boolean
유형의roundUp
매개변수와Boolean
을 사용하고 아무것도 반환하지 않는onRoundUpChanged
람다 함수를 추가합니다.
@Composable
fun RoundTheTipRow(
roundUp: Boolean,
onRoundUpChanged: (Boolean) -> Unit,
modifier: Modifier = Modifier
)
그러면 스위치의 상태를 끌어올립니다.
Switch
컴포저블에서 이modifier
를 추가하여Switch
컴포저블을 화면 끝에 맞춥니다.
Switch(
modifier = modifier
.fillMaxWidth()
.wrapContentWidth(Alignment.End),
//...
)
- 다음을 가져옵니다.
import androidx.compose.foundation.layout.wrapContentWidth
TipTimeLayout()
함수에서Switch
컴포저블의 상태에 관한 var 변수를 추가합니다.roundUp
이라는var
변수를 만들어mutableStateOf()
로 설정합니다. 초깃값은false
입니다.remember { }
로 호출을 둘러쌉니다.
fun TipTimeLayout() {
//...
var roundUp by remember { mutableStateOf(false) }
//...
Column(
...
) {
//...
}
}
이는 Switch
컴포저블 상태의 변수이며 false가 기본 상태가 됩니다.
TipTimeLayout()
함수의Column
블록에서 Tip Percentage 텍스트 필드 뒤에roundUp
으로 설정된roundUp
이라는 매개변수,roundUp
값을 업데이트하는 람다 콜백으로 설정된onRoundUpChanged
라는 매개변수를 인수로 사용하여RoundTheTipRow()
함수를 호출합니다.
@Composable
fun TipTimeLayout() {
//...
Column(
...
) {
Text(
...
)
Spacer(...)
EditNumberField(
...
)
EditNumberField(
...
)
RoundTheTipRow(
roundUp = roundUp,
onRoundUpChanged = { roundUp = it },
modifier = Modifier.padding(bottom = 32.dp)
)
Text(
...
)
}
}
그러면 Round up tip? 행이 표시됩니다.
- 앱을 실행합니다. 앱에 Round up tip? 전환 스위치가 표시됩니다.
- 청구 금액과 팁 비율을 입력한 다음 Round up tip? 전환 스위치를 선택합니다. 팁 금액은 반올림되지 않습니다. 여전히
calculateTip()
함수를 업데이트해야 하기 때문입니다. 다음 섹션에서 알아봅니다.
calculateTip()
함수를 업데이트하여 팁 반올림하기
Boolean
변수를 허용하여 팁을 가장 가까운 정수로 반올림하도록 calculateTip()
함수를 수정합니다.
- 팁을 반올림하려면
calculateTip()
함수가Boolean
인 스위치의 상태를 알아야 합니다.calculateTip()
함수에서Boolean
유형의roundUp
매개변수를 추가합니다.
private fun calculateTip(
amount: Double,
tipPercent: Double = 15.0,
roundUp: Boolean
): String {
//...
}
calculateTip()
함수에서return
문 앞에roundUp
값을 확인하는if()
조건을 추가합니다.roundUp
이true
이면tip
변수를 정의하고kotlin.math.
ceil
()
함수로 설정한 후tip
함수를 인수로 전달합니다.
if (roundUp) {
tip = kotlin.math.ceil(tip)
}
완성된 calculateTip()
함수는 다음 코드 스니펫과 같습니다.
private fun calculateTip(amount: Double, tipPercent: Double = 15.0, roundUp: Boolean): String {
var tip = tipPercent / 100 * amount
if (roundUp) {
tip = kotlin.math.ceil(tip)
}
return NumberFormat.getCurrencyInstance().format(tip)
}
TipTimeLayout()
함수에서calculateTip()
함수 호출을 업데이트하고roundUp
매개변수를 전달합니다.
val tip = calculateTip(amount, tipPercent, roundUp)
- 앱을 실행합니다. 이제 다음 이미지와 같이 팁 금액이 반올림됩니다.
7. 가로 모드 방향 지원 추가
Android 기기는 화면 크기가 다양한 스마트폰, 태블릿, 폴더블, ChromeOS 기기 등 여러 폼 팩터로 출시됩니다. 앱이 세로 모드와 가로 모드 방향을 모두 지원해야 합니다.
- 앱을 가로 모드로 테스트하고 자동 회전을 사용 설정합니다.
- 에뮬레이터나 기기를 왼쪽으로 회전합니다. 팁 금액을 볼 수 없습니다. 이를 해결하려면 앱 화면을 스크롤하는 데 도움이 되는 세로 스크롤바가 필요합니다.
- 열을 세로로 스크롤할 수 있도록 수정자에
.verticalScroll(rememberScrollState())
을 추가합니다.rememberScrollState()
는 스크롤 상태를 만들고 자동으로 기억합니다.
@Composable
fun TipTimeLayout() {
// ...
Column(
modifier = Modifier
.padding(40.dp)
.verticalScroll(rememberScrollState()),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
//...
}
}
- 다음을 가져옵니다.
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
- 앱을 다시 실행합니다. 가로 모드로 스크롤해 봅니다.
8. 텍스트 필드에 선행 아이콘 추가(선택사항)
아이콘은 텍스트 필드를 시각적으로 더 매력적으로 만들고 텍스트 필드에 관한 추가 정보를 제공합니다. 아이콘은 예상되는 데이터 유형이나 필요한 입력 종류와 같은 텍스트 필드 용도에 관한 정보를 전달하는 데 사용할 수 있습니다. 예를 들어 텍스트 필드 옆의 전화 아이콘은 사용자가 전화번호를 입력해야 함을 나타낼 수 있습니다.
아이콘은 예상되는 사항에 관한 시각적 단서를 제공하여 사용자의 입력을 안내하는 데 사용할 수 있습니다. 예를 들어 텍스트 필드 옆에 있는 캘린더 아이콘은 사용자가 날짜를 입력해야 함을 나타냅니다.
다음은 검색어를 입력할 것을 나타내는 검색 아이콘이 있는 텍스트 필드의 예입니다.
EditNumberField()
컴포저블에 Int
유형의 leadingIcon
이라는 다른 매개변수를 추가합니다. @DrawableRes
로 주석을 답니다.
@Composable
fun EditNumberField(
@StringRes label: Int,
@DrawableRes leadingIcon: Int,
keyboardOptions: KeyboardOptions,
value: String,
onValueChanged: (String) -> Unit,
modifier: Modifier = Modifier
)
- 다음을 가져옵니다.
import androidx.annotation.DrawableRes
import androidx.compose.material3.Icon
- 텍스트 필드에 선행 아이콘을 추가합니다.
leadingIcon
은 컴포저블을 사용합니다. 다음Icon
컴포저블을 전달합니다.
TextField(
value = value,
leadingIcon = { Icon(painter = painterResource(id = leadingIcon), null) },
//...
)
- 선행 아이콘을 텍스트 필드에 전달합니다. 편의를 위해 시작 코드에 아이콘이 이미 있습니다.
EditNumberField(
label = R.string.bill_amount,
leadingIcon = R.drawable.money,
// Other arguments
)
EditNumberField(
label = R.string.how_was_the_service,
leadingIcon = R.drawable.percent,
// Other arguments
)
- 앱을 실행합니다.
축하합니다. 이제 앱에서 맞춤 팁을 계산할 수 있습니다.
9. 솔루션 코드 가져오기
완료된 Codelab의 코드를 다운로드하려면 이 git 명령어를 사용하면 됩니다.
$ git clone https://github.com/google-developer-training/basic-android-kotlin-compose-training-tip-calculator.git
또는 ZIP 파일로 저장소를 다운로드한 다음 압축을 풀고 Android 스튜디오에서 열어도 됩니다.
솔루션 코드를 보려면 GitHub에서 확인하세요.
10. 결론
축하합니다. Tip Time 앱에 맞춤 팁 기능을 추가했습니다. 이제 사용자가 앱에서 맞춤 팁 비율을 입력하고 팁 금액을 반올림할 수 있습니다. #AndroidBasics를 사용해 소셜 미디어에서 작업을 공유하세요.