팁 계산

1. 시작하기 전에

이 Codelab에서는 이전 Codelab인 Android용 XML 레이아웃 만들기에서 생성한 UI에 사용할 수 있는 팁 계산기에 관한 코드를 작성합니다.

기본 요건

학습할 내용

  • Android 앱의 기본 구조
  • UI에서 값을 읽어 코드에 입력하고 조작하는 방법
  • findViewById() 대신 뷰 결합을 사용하여 뷰와 상호작용하는 코드를 더 쉽게 작성하는 방법
  • Kotlin에서 Double 데이터 유형을 사용하여 십진수로 작업하는 방법
  • 숫자를 통화 형식으로 지정하는 방법
  • 문자열 매개변수를 사용하여 동적으로 문자열을 만드는 방법
  • Android 스튜디오에서 Logcat을 사용하여 앱의 문제를 찾는 방법

빌드할 항목

  • Calculate 버튼이 작동하는 팁 계산기 앱

필요한 항목

  • Android 스튜디오의 최신 안정화 버전이 설치된 컴퓨터
  • 팁 계산기의 레이아웃이 포함된 Tip Time 앱의 시작 코드

2. 시작 앱 개요

지난 CodelabTip Time 앱에는 팁 계산기에 필요한 모든 UI가 있지만 팁을 계산하는 코드는 없습니다. Calculate 버튼이 있지만 아직 작동하지 않습니다. Cost of Service EditText를 사용하면 사용자가 서비스 비용을 입력할 수 있습니다. RadioButtons 목록을 통해 사용자는 팁 비율을 선택할 수 있으며 Switch를 사용하면 사용자가 팁을 올림할지 여부를 선택할 수 있습니다. 팁 금액은 TextView에 표시됩니다. 마지막으로 사용자가 Calculate Button을 탭하면 앱은 다른 필드에서 데이터를 가져와서 팁 금액을 계산합니다. 이러한 기능을 이 Codelab에서 구현해 보겠습니다.

ebf5c40d4e12d4c7.png

앱 프로젝트 구조

IDE의 앱 프로젝트는 Kotlin 코드, XML 레이아웃, 기타 리소스(예: 문자열 및 이미지)를 비롯한 여러 요소로 구성됩니다. 앱을 변경하기 전에 먼저 관련 내용을 학습하는 것이 좋습니다.

  1. Android 스튜디오에서 Tip Time 프로젝트를 엽니다.
  2. Project 창이 표시되지 않으면 Android 스튜디오 왼쪽에 있는 Project 탭을 선택합니다.
  3. 아직 선택되어 있지 않다면 드롭다운에서 Android 뷰를 선택합니다.

2a83e2b0aee106dd.png

  • java - Kotlin 파일(또는 자바 파일)의 폴더
  • MainActivity - 팁 계산기 로직의 모든 Kotlin 코드가 들어갈 클래스
  • res - 앱 리소스의 폴더
  • activity_main.xml - Android 앱의 레이아웃 파일
  • strings.xml - Android 앱의 문자열 리소스가 포함되어 있는 파일
  • Gradle Scripts - 폴더

Gradle은 Android 스튜디오에서 사용하는 자동화된 빌드 시스템입니다. 개발자가 코드를 변경하거나 리소스를 추가하거나 그 외의 방식으로 앱을 변경할 때마다 Gradle이 변경된 사항을 파악하여 앱을 다시 빌드하는 데 필요한 조치를 취합니다. 또한 에뮬레이터 또는 실제 기기에 앱을 설치하고 실행을 제어합니다.

앱 빌드와 관련된 다른 폴더 및 파일도 있지만 위에 설명한 폴더 및 파일이 이 Codelab과 다음 Codelab에서 사용할 주요 폴더 및 파일입니다.

3. 뷰 결합

팁을 계산하려면 코드가 모든 UI 요소에 액세스하여 사용자의 입력을 읽어야 합니다. 이전 Codelab에서 학습한 내용을 떠올려보면 코드가 View에서 메서드를 호출하거나 속성에 액세스하기 전에 먼저 Button 또는 TextView와 같은 View에 대한 참조를 찾아야 합니다. Android 프레임워크는 여기에 필요한 작업(View의 ID가 주어지면 이 뷰에 대한 참조를 반환하는 작업)을 정확히 실행하는 findViewById() 메서드를 제공합니다. 이 접근 방식은 효과적이지만 앱에 뷰를 더 많이 추가하고 UI가 더 복잡해짐에 따라 findViewById()를 사용하는 것이 번거로워질 수 있습니다.

편의를 위해 Android는 뷰 결합이라는 기능도 제공합니다. 사전에 조금만 더 작업하면 뷰 결합을 통해 UI의 뷰에서 메서드를 훨씬 더 쉽고 빠르게 호출할 수 있습니다. Gradle에서 앱의 뷰 결합을 사용 설정하고 몇 가지 코드를 변경해야 합니다.

뷰 결합 사용 설정

  1. 앱의 build.gradle 파일을 엽니다(Gradle Scripts > build.gradle (Module: Tip_Time.app)).
  2. android 섹션에서 다음 줄을 추가합니다.
buildFeatures {
    viewBinding = true
}
  1. 'Gradle files have changed since last project sync.'라는 메시지에 주의합니다.
  2. Sync Now를 누릅니다.

349d99c67c2f40f1.png

잠시 후에 Android 스튜디오 창 하단에 Gradle sync finished라는 메시지가 표시됩니다. 원하는 경우 build.gradle 파일을 닫을 수 있습니다.

결합 객체 초기화

이전 Codelab에서는 MainActivity 클래스의 onCreate() 메서드를 살펴보았습니다. 앱이 시작되고 MainActivity가 초기화될 때 가장 먼저 호출되는 것 중 하나입니다. 앱의 각 View마다 findViewById()를 호출하는 대신, 결합 객체를 한 번 만들고 초기화합니다.

674d243aa6f85b8b.png

  1. MainActivity.kt를 엽니다(app > java > com.example.tiptime > MainActivity).
  2. MainActivity 클래스의 모든 기존 코드를 다음 코드로 대체하여 MainActivity가 뷰 결합을 사용하도록 설정합니다.
class MainActivity : AppCompatActivity() {

    lateinit var binding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)
    }
}
  1. 다음 코드 줄은 클래스에서 결합 객체의 최상위 변수를 선언합니다. 변수가 MainActivity 클래스의 여러 메서드에서 사용되므로 이 수준에서 정의됩니다.
lateinit var binding: ActivityMainBinding

lateinit 키워드는 새로운 키워드로, 코드가 변수를 사용하기 전에 먼저 초기화할 것임을 확인해 줍니다. 변수를 초기화하지 않으면 앱이 비정상 종료됩니다.

  1. 다음 코드 줄은 activity_main.xml 레이아웃에서 Views에 액세스하는 데 사용할 binding 객체를 초기화합니다.
binding = ActivityMainBinding.inflate(layoutInflater)
  1. 활동의 콘텐츠 뷰를 설정합니다. 다음 코드는 레이아웃의 리소스 ID인 R.layout.activity_main을 전달하는 대신, 앱의 뷰 계층 구조 루트인 binding.root를 지정합니다.
setContentView(binding.root)

상위 뷰와 하위 뷰의 개념을 상기하시기 바랍니다. 루트는 모든 뷰에 연결되어 있습니다.

이제 앱에서 View에 대한 참조가 필요한 경우 findViewById()를 호출하는 대신 binding 객체에서 뷰 참조를 가져올 수 있습니다. binding 객체는 ID가 있는 앱의 모든 View를 위한 참조를 자동으로 정의합니다. 뷰 결합을 사용하는 것이 훨씬 더 간결해서 View를 위한 참조를 유지할 변수를 만들 필요조차 없으며 결합 객체에서 직접 뷰 참조를 사용하기만 하면 됩니다.

// Old way with findViewById()
val myButton: Button = findViewById(R.id.my_button)
myButton.text = "A button"

// Better way with view binding
val myButton: Button = binding.myButton
myButton.text = "A button"

// Best way with view binding and no extra variable
binding.myButton.text = "A button"

멋지지 않나요?

4. 팁 계산

사용자가 Calculate 버튼을 탭하면 팁 계산이 시작됩니다. 이때 서비스 비용과 사용자가 주고 싶은 팁의 비율을 표시하는 UI를 확인합니다. 이 정보를 사용하여 서비스 요금 총액을 계산하고 팁 금액을 표시합니다.

버튼에 클릭 리스너 추가

첫 번째 단계는 클릭 리스너를 추가하여 사용자가 Calculate 버튼을 탭할 때 이 버튼이 실행해야 하는 작업을 지정하는 것입니다.

  1. onCreate()MainActivity.kt에서 setContentView() 호출 뒤에 Calculate 버튼에 클릭 리스너를 설정하고 calculateTip()을 호출하도록 합니다.
binding.calculateButton.setOnClickListener{ calculateTip() }
  1. 계속해서 MainActivity 클래스 내에서 그러나 onCreate() 외부에 calculateTip()이라는 도우미 메서드를 추가합니다.
fun calculateTip() {

}

여기에서 UI를 확인하고 팁을 계산하는 코드를 추가합니다.

MainActivity.kt

class MainActivity : AppCompatActivity() {

    lateinit var binding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)
        binding.calculateButton.setOnClickListener{ calculateTip() }
    }

    fun calculateTip() {

    }
}

서비스 비용 가져오기

팁을 계산하기 위해 가장 먼저 필요한 것은 서비스 비용입니다. 텍스트는 EditText에 저장되지만 서비스 비용 텍스트를 계산에 사용하려면 숫자로 된 서비스 비용이 필요합니다. 다른 Codelab에서 학습한 Int 유형을 떠올려보면 Int는 정수만 보유할 수 있습니다. 앱에서 십진수를 사용하려면 Int 대신 Double이라는 데이터 유형을 사용해야 합니다. 자세히 알아보려면 Kotlin의 숫자 데이터 유형에 관한 문서를 참고하시기 바랍니다. Kotlin은 StringDouble로 변환하는 toDouble()이라는 메서드를 제공합니다.

  1. 먼저 서비스 비용의 텍스트를 가져옵니다. calculateTip() 메서드에서 Cost of Service EditText의 텍스트 속성을 가져와서 stringInTextField라는 변수에 할당합니다. 앞서 언급했듯이 binding 객체를 사용하여 UI 요소에 액세스할 수 있으며 카멜 표기법의 리소스 ID 이름을 기반으로 UI 요소를 참조할 수 있습니다.
val stringInTextField = binding.costOfService.text

끝부분에 있는 .text에 유의합니다. 첫 번째 부분인 binding.costOfService는 서비스 비용의 UI 요소를 참조합니다. 끝부분에 .text를 추가하여 그에 따른 결과(EditText 객체)를 얻은 후 이 객체에서 text 속성을 가져옵니다. 이를 체이닝이라고 하며 Kotlin에서 매우 일반적인 패턴입니다.

  1. 다음으로, 텍스트를 십진수로 변환합니다. stringInTextField에서 toDouble()을 호출하여 cost라는 변수에 저장합니다.
val cost = stringInTextField.toDouble()

하지만 이 메서드는 작동하지 않습니다. toDouble()String에서 호출되어야 합니다. EditTexttext 속성은 변경할 수 있는 텍스트를 나타내기 때문에 Editable인 것으로 밝혀졌습니다. 다행히 toString()을 호출하여 EditableString으로 변환할 수 있습니다.

  1. 다음과 같이 binding.costOfService.text에서 toString()을 호출하여 String으로 변환합니다.
val stringInTextField = binding.costOfService.text.toString()

이제 stringInTextField.toDouble()이 작동합니다.

이 시점에서 calculateTip() 메서드는 다음과 같습니다.

fun calculateTip() {
    val stringInTextField = binding.costOfService.text.toString()
    val cost = stringInTextField.toDouble()
}

팁 비율 가져오기

지금까지는 서비스 비용을 가져왔습니다. 이제 사용자가 RadioButtonsRadioGroup에서 선택한 팁 비율이 필요합니다.

  1. calculateTip()에서 tipOptions RadioGroupcheckedRadioButtonId 속성을 가져와서 selectedId라는 변수에 할당합니다.
val selectedId = binding.tipOptions.checkedRadioButtonId

이제 R.id.option_twenty_percent, R.id.option_eighteen_percent 또는 R.id.fifteen_percent 중 어느 RadioButton이 선택되어 있는지 알았습니다. 그러나 그에 상응하는 비율이 필요합니다. 일련의 if/else 문을 작성할 수 있지만, when 표현식을 사용하는 것이 훨씬 더 쉽습니다.

  1. 다음 코드 줄을 추가하여 팁 비율을 가져옵니다.
val tipPercentage = when (selectedId) {
    R.id.option_twenty_percent -> 0.20
    R.id.option_eighteen_percent -> 0.18
    else -> 0.15
}

이 시점에서 calculateTip() 메서드는 다음과 같습니다.

fun calculateTip() {
    val stringInTextField = binding.costOfService.text.toString()
    val cost = stringInTextField.toDouble()
    val selectedId = binding.tipOptions.checkedRadioButtonId
    val tipPercentage = when (selectedId) {
        R.id.option_twenty_percent -> 0.20
        R.id.option_eighteen_percent -> 0.18
        else -> 0.15
    }
}

팁 계산 및 반올림하기

이제 서비스 비용과 팁 비율이 있으므로 팁을 계산하는 것은 간단합니다. 팁은 비용 x 팁 비율이므로 팁 = 서비스 비용 * 팁 비율입니다. 선택사항으로 이 값은 반올림될 수 있습니다.

  1. calculateTip()에서, 앞서 추가한 다른 코드 뒤에 tipPercentagecost를 곱하고 이를 tip이라는 변수에 할당합니다.
var tip = tipPercentage * cost

val 대신 var을 사용한 점에 유의합니다. 이는 사용자가 이 옵션을 선택한 경우 값을 반올림해야 할 수 있어 값이 변경될 수 있기 때문입니다.

Switch 요소의 경우 isChecked 속성을 확인하여 스위치가 '사용 설정' 상태인지 확인할 수 있습니다.

  1. 반올림 스위치의 isChecked 속성을 roundUp이라는 변수에 할당합니다.
val roundUp = binding.roundUpSwitch.isChecked

반올림이라는 용어는 십진수를 가장 가까운 정숫값으로 위 또는 아래로 조정하는 것을 의미하지만, 이 경우에는 위로만 반올림하거나 상한(ceiling)을 찾으려고 합니다. 이를 위해 ceil() 함수를 사용할 수 있습니다. 같은 이름의 함수가 여러 개 있지만, 필요한 함수는 kotlin.math에 정의되어 있습니다. import 문을 추가할 수 있지만, 이 경우에는 kotlin.math.ceil()을 사용하여 의미하고자 하는 것을 Android 스튜디오에 알리는 것이 더 간단합니다.

32c29f73a3f20f93.png

사용하려는 수학 함수가 여러 개인 경우 import 문을 추가하는 것이 더 쉽습니다.

  1. roundUp이 참인 경우 tip 변수에 팁의 상한을 할당하는 if 문을 추가합니다.
if (roundUp) {
    tip = kotlin.math.ceil(tip)
}

이 시점에서 calculateTip() 메서드는 다음과 같습니다.

fun calculateTip() {
    val stringInTextField = binding.costOfService.text.toString()
    val cost = stringInTextField.toDouble()
    val selectedId = binding.tipOptions.checkedRadioButtonId
    val tipPercentage = when (selectedId) {
        R.id.option_twenty_percent -> 0.20
        R.id.option_eighteen_percent -> 0.18
        else -> 0.15
    }
    var tip = tipPercentage * cost
    val roundUp = binding.roundUpSwitch.isChecked
    if (roundUp) {
        tip = kotlin.math.ceil(tip)
    }
}

팁 형식 지정

앱이 거의 작동 단계에 도달했습니다. 지금까지 팁을 계산했습니다. 이제 팁의 형식을 지정하고 표시하기만 하면 됩니다.

예상할 수 있듯이 Kotlin은 다양한 유형의 숫자 형식을 지정하는 메서드를 제공합니다. 그런데 팁 금액은 약간씩 다르며 통화 가치를 나타냅니다. 국가마다 서로 다른 통화를 사용하며 십진수 형식 지정 규칙이 다릅니다. 예를 들어 1234.56의 경우 미국 달러로는 $1,234.56 형식으로 표시되지만 유로화로는 €1.234,56 형식으로 표시됩니다. 다행히 Android 프레임워크에서는 숫자를 통화 형식으로 지정하는 메서드를 제공하므로 개발자가 모든 경우의 수를 알 필요가 없습니다. 사용자가 스마트폰에서 선택한 언어 및 기타 설정에 따라 시스템이 자동으로 통화 형식을 지정합니다. 더 자세히 알아보려면 Android 개발자 문서에서 NumberFormat을 참고하세요.

  1. calculateTip()에서 다른 코드 다음에 NumberFormat.getCurrencyInstance()를 호출합니다.
NumberFormat.getCurrencyInstance()

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

  1. 숫자 형식 지정 클래스를 사용하여 tipformat() 메서드 호출을 체이닝하고 formattedTip이라는 변수에 결과를 할당합니다.
val formattedTip = NumberFormat.getCurrencyInstance().format(tip)
  1. NumberFormat이 빨간색으로 표시된 것을 확인할 수 있습니다. 이는 개발자가 의미하는 NumberFormat의 버전을 Android 스튜디오에서 자동으로 파악할 수 없기 때문입니다.
  2. NumberFormat 위로 마우스 포인터를 가져가서 팝업이 표시되면 Import를 선택합니다. d9d2f92d5ef01df6.png
  3. 가능한 가져오기 목록에서 NumberFormat (java.text)를 선택합니다. Android 스튜디오에서 MainActivity 파일의 맨 위에 import 문을 추가하며 NumberFormat이 더 이상 빨간색이 아닙니다.

팁 표시

이제 팁 금액 TextView 요소에 팁을 표시할 차례입니다. 간단히 formattedTiptext 속성에 할당할 수 있지만 금액이 무엇을 나타내는지 라벨을 지정하는 것이 좋습니다. 영어를 사용하는 미국에서는 Tip Amount: $12.34와 같이 표시할 수 있지만 다른 언어에서는 숫자를 문자열의 시작 부분이나 중간에 표시해야 할 수 있습니다. Android 프레임워크는 이를 위해 문자열 매개변수라고 하는 메커니즘을 제공하므로 앱을 변환하는 개발자는 필요한 경우 숫자가 표시되는 위치를 변경할 수 있습니다.

  1. strings.xml을 엽니다(app > res > values > strings.xml).
  2. tip_amount 문자열을 Tip Amount에서 Tip Amount: %s로 변경합니다.
<string name="tip_amount">Tip Amount: %s</string>

%s는 형식이 지정된 통화가 삽입되는 위치입니다.

  1. 이제 tipResult의 텍스트를 설정합니다. MainActivity.ktcalculateTip() 메서드로 돌아가서 getString(R.string.tip_amount, formattedTip)을 호출하고 반환되는 값을 팁 결과 TextViewtext 속성에 할당합니다.
binding.tipResult.text = getString(R.string.tip_amount, formattedTip)

이 시점에서 calculateTip() 메서드는 다음과 같습니다.

fun calculateTip() {
    val stringInTextField = binding.costOfService.text.toString()
    val cost = stringInTextField.toDouble()
    val selectedId = binding.tipOptions.checkedRadioButtonId
    val tipPercentage = when (selectedId) {
        R.id.option_twenty_percent -> 0.20
        R.id.option_eighteen_percent -> 0.18
        else -> 0.15
    }
    var tip = tipPercentage * cost
    val roundUp = binding.roundUpSwitch.isChecked
    if (roundUp) {
        tip = kotlin.math.ceil(tip)
    }
    val formattedTip = NumberFormat.getCurrencyInstance().format(tip)
    binding.tipResult.text = getString(R.string.tip_amount, formattedTip)
}

거의 완료되었습니다. 앱을 개발하고 미리보기를 확인할 때 TextView에 자리표시자를 사용하면 유용합니다.

  1. activity_main.xml(app > res > layout > activity_main.xml)을 엽니다.
  2. tip_result TextView를 찾습니다.
  3. android:text 속성이 있는 코드 줄을 삭제합니다.
android:text="@string/tip_amount"
  1. Tip Amount: $10로 설정된 tools:text 속성을 위한 코드 줄을 추가합니다.
tools:text="Tip Amount: $10"

이는 자리표시자일 뿐이므로 문자열을 리소스로 추출할 필요가 없습니다. 앱 실행 시에는 표시되지 않습니다.

  1. 도구 텍스트는 Layout Editor에 표시됩니다.
  2. 앱을 실행합니다. 비용 금액을 입력하고 몇 가지 옵션을 선택한 후 Calculate 버튼을 누릅니다.

42fd6cd5e24ca433.png

축하합니다. 앱이 작동합니다! 정확한 팁 금액이 표시되지 않으면 이 섹션의 1단계로 돌아가서 필요한 코드를 모두 변경했는지 확인합니다.

5. 테스트 및 디버그

다양한 단계에서 앱을 실행하여 원하는 작업을 실행하는지 확인했지만, 이제 몇 가지를 추가로 테스트할 차례입니다.

우선, calculateTip() 메서드에서 정보가 앱을 통해 어떻게 이동하는지와 각 단계에서 어떤 문제가 발생할 수 있을지 생각해 보시기 바랍니다.

예를 들어 다음 코드 줄에서는 어떤 일이 일어날까요?

val cost = stringInTextField.toDouble()

stringInTextField가 숫자를 표시하지 않았을 때 말이죠. 또한 사용자가 텍스트를 입력하지 않았고 stringInTextField가 비어 있다면 어떻게 될까요?

  1. 에뮬레이터에서 앱을 실행하지만 Run > Run ‘app'을 사용하는 대신 Run > Debug ‘app'을 사용합니다.
  2. 비용, 팁 금액, 팁 반올림 여부의 다양한 조합을 몇 가지 사용하여 Calculate를 탭할 때 각각의 경우에 예상되는 결과를 얻는지 확인합니다.
  3. 이제 Cost of Service 필드에 있는 모든 텍스트를 삭제하고 Calculate를 탭합니다. 프로그램이 비정상 종료됩니다.

비정상 종료 디버그

버그 처리의 첫 단계는 무슨 일이 일어났는지 파악하는 것입니다. Android 스튜디오에서는 시스템에서 발생한 문제에 관한 로그를 유지하므로 개발자는 이를 사용하여 무엇이 잘못되었는지 파악할 수 있습니다.

  1. Android 스튜디오 하단에 있는 Logcat 버튼을 누르거나 메뉴에서 View > Tool Windows > Logcat을 선택합니다.

1b68ee5190018c8a.png

  1. Android 스튜디오 하단에 이상하게 보이는 텍스트로 가득 찬 Logcat 창이 표시됩니다. 22139575476ae9d.png

이 텍스트는 스택 트레이스로, 비정상 종료가 발생했을 때 호출된 메서드의 목록입니다.

  1. Logcat 텍스트에서 FATAL EXCEPTION 텍스트가 포함된 줄을 찾을 때까지 위로 스크롤합니다.
2020-06-24 10:09:41.564 24423-24423/com.example.tiptime E/AndroidRuntime: FATAL EXCEPTION: main
    Process: com.example.tiptime, PID: 24423
    java.lang.NumberFormatException: empty String
        at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1842)
        at sun.misc.FloatingDecimal.parseDouble(FloatingDecimal.java:110)
        at java.lang.Double.parseDouble(Double.java:538)
        at com.example.tiptime.MainActivity.calculateTip(MainActivity.kt:22)
        at com.example.tiptime.MainActivity$onCreate$1.onClick(MainActivity.kt:17)
  1. NumberFormatException이 있는 줄을 찾을 때까지 아래쪽으로 읽습니다.
java.lang.NumberFormatException: empty String

오른쪽에 empty String이 있는 것을 확인할 수 있습니다. 예외 유형은 문제가 숫자 형식과 관련이 있다는 것을 알려주며 나머지는 문제의 원인을 알려줍니다. 즉, String에 값이 있어야 하는데 비어 있는 String이 발견되었다는 의미입니다.

  1. 아래쪽으로 계속해서 읽으면 몇 번의 parseDouble() 호출을 확인할 수 있습니다.
  2. 이러한 호출 아래에서 calculateTip이 있는 줄을 찾습니다. 이 줄에는 MainActivity 클래스도 포함되어 있습니다.
at com.example.tiptime.MainActivity.calculateTip(MainActivity.kt:22)
  1. 이 줄을 주의 깊게 살펴보면 코드에서 호출이 발생한 정확한 위치 즉, MainActivity.kt의 22번째 코드 줄을 확인할 수 있습니다. (코드를 다르게 입력한 경우 코드 줄 번호가 다를 수 있습니다.) 이 줄은 StringDouble로 변환하고 결과를 cost 변수에 할당합니다.
val cost = stringInTextField.toDouble()
  1. Kotlin 문서를 검토하여 String에서 작동하는 toDouble() 메서드를 찾습니다. 이 메서드는 String.toDouble()이라고 합니다.
  2. 페이지에 'Exceptions: NumberFormatException - if the string is not a valid representation of a number'라고 나와 있습니다.

예외는 시스템에서 문제가 있음을 표현하는 방법입니다. 이 케이스에서 문제는 toDouble()이 비어 있는 StringDouble로 변환할 수 없다는 것입니다. EditTextinputType=numberDecimal이 있더라도 toDouble()이 처리할 수 없는 일부 값(예: 빈 문자열)이 여전히 입력될 수 있습니다.

null에 관해 자세히 알아보기

비어 있는 문자열이나 유효한 십진수를 나타내지 않는 문자열에서 toDouble()을 호출하면 작동하지 않습니다. 다행히 Kotlin은 이러한 문제를 처리하는 toDoubleOrNull()이라는 메서드도 제공합니다. 이 메서드는 가능한 경우 십진수를 반환하고 문제가 있다면 null을 반환합니다.

Null은 '값 없음'을 의미하는 특수 값으로, 값이 0.0Double 또는 문자 수가 0개인 빈 String(즉, "")과는 다릅니다. Null은 값이 없거나 Double이 없거나 String이 없음을 의미합니다. 많은 메서드가 값을 예상하고 null을 처리하는 방법을 몰라서 중지됩니다. 즉, 앱이 비정상 종료됩니다. 따라서 Kotlin은 null이 사용되는 위치를 제한하려고 합니다. 이에 관한 내용은 향후 과정에서 자세히 알아봅니다.

앱은 toDoubleOrNull()에서 null이 반환되는지 확인하여 null이 반환되는 경우 다른 방식으로 처리할 수 있습니다. 따라서 앱이 비정상 종료되지 않습니다.

  1. calculateTip()에서 cost 변수를 선언하는 코드 줄을, toDouble()을 호출하는 대신 toDoubleOrNull()을 호출하도록 변경합니다.
val cost = stringInTextField.toDoubleOrNull()
  1. 이 코드 줄 뒤에 costnull인지 확인하고 그렇다면 메서드에서 반환되는 문을 추가합니다. return 명령은 나머지 명령을 실행하지 않고 메서드를 종료하는 것을 의미합니다. 메서드가 값을 반환해야 하는 경우 표현식에서 return 명령을 사용하여 값 반환을 지정합니다.
if (cost == null) {
    return
}
  1. 앱을 다시 실행합니다.
  2. Cost of Service 필드에 텍스트가 없는 상태에서 Calculate를 탭합니다. 이번에는 앱이 비정상 종료되지 않습니다. 잘하셨습니다. 버그를 찾아 수정했습니다.

다른 케이스 처리

버그로 인해 항상 앱이 비정상 종료되는 것은 아닙니다. 때로 이러한 결과는 사용자를 혼란스럽게 만들 수도 있습니다.

다음은 고려해야 할 또 다른 케이스입니다. 사용자가 다음과 같이 한다면 어떻게 될까요?

  1. 비용에 유효한 금액을 입력합니다.
  2. Calculate를 탭하여 팁을 계산합니다.
  3. 비용을 삭제합니다.
  4. Calculate를 다시 탭합니다.

처음에는 팁이 예상대로 계산되고 표시됩니다. 두 번째에는 앞서 추가한 null 확인 코드로 인해 calculateTip() 메서드가 조기에 반환되지만, 앱에 여전히 이전의 팁 금액이 표시됩니다. 이러한 문제가 발생하는 경우 사용자를 혼란스럽게 만들 수 있으므로 팁 금액을 지우는 코드를 추가합니다.

  1. 이 문제를 파악하려면 유효한 비용을 입력하고 Calculate를 탭한 후 텍스트를 삭제하고 Calculate를 다시 탭하여 어떻게 되는지 확인합니다. 첫 번째 팁 값이 계속 표시됩니다.
  2. 앞서 추가한 if 내에서 return 문 앞에 tipResulttext 속성을 빈 문자열로 설정하는 코드 줄을 추가합니다.
if (cost == null) {
    binding.tipResult.text = ""
    return
}

이렇게 하면 calculateTip()에서 반환되기 전에 팁 금액이 삭제됩니다.

  1. 앱을 다시 실행하고 위의 케이스를 시도해 봅니다. 두 번째에 Calculate를 탭하면 첫 번째 팁 값이 사라집니다.

축하합니다. Android에서 작동하는 팁 계산기 앱을 만들고 일부 특수 사례를 처리했습니다.

6. 적절한 코딩 사례 채택

이제 팁 계산기가 작동하지만, 코딩 권장사항을 채택하면 코드를 조금 더 개선하고 향후 작업을 더 쉽게 할 수 있습니다.

  1. MainActivity.kt를 엽니다(app > java > com.example.tiptime > MainActivity).
  2. calculateTip() 메서드의 시작 부분을 살펴보면 물결 모양의 회색 선으로 밑줄이 그어진 것을 볼 수 있습니다.

3737ebab72be9a5b.png

  1. calculateTip() 위로 마우스 포인터를 가져가면 아래에 Make 'calculateTip' 'private'이라는 추천이 있는 Function 'calculateTip' could be private 메시지가 표시됩니다. 6205e927b4c14cf3.png

이전 Codelab에서 학습한 내용을 떠올려보면 private은 메서드 또는 변수가 클래스(이 경우에는 MainActivity 클래스) 내의 코드에만 공개된다는 것을 의미합니다. MainActivity 외부의 코드가 calculateTip()을 호출할 이유가 없으므로 이 메서드를 아무 문제 없이 private으로 만들 수 있습니다.

  1. Make 'calculateTip' 'private'을 선택하거나 fun calculateTip() 앞에 private 키워드를 추가합니다. calculateTip() 아래의 회색 선이 사라집니다.

코드 검사

회색 선은 매우 감지하기 어려워 간과하기 쉽습니다. 파일 전체를 검토하여 회색 선을 더 많이 찾을 수도 있지만, 모든 추천을 확인할 수 있는 더 간단한 방법이 있습니다.

  1. MainActivity.kt를 연 채로 메뉴에서 Analyze > Inspect Code...를 선택합니다. Specify Inspection Scope라는 대화상자가 표시됩니다. 1d2c6f8415e96231.png
  2. File로 시작하는 옵션을 선택하고 OK를 누릅니다. 이렇게 하면 검사가 MainActivity.kt로만 제한됩니다.
  3. Inspection Results가 있는 창이 하단에 표시됩니다.
  4. 2개의 메시지가 표시될 때까지 Kotlin 옆과 Style issues 옆에 있는 회색 삼각형을 차례로 클릭합니다. 첫 번째 메시지는 Class member can have ‘private' visibility입니다. e40a6876f939c0d9.png
  5. Property ‘binding' could be private 메시지가 표시될 때까지 회색 삼각형을 클릭하고, 메시지를 클릭합니다. Android 스튜디오가 MainActivity의 일부 코드를 표시하고 binding 변수를 강조표시합니다. 8d9d7b5fc7ac5332.png
  6. Make ‘binding' ‘private' 버튼을 누릅니다. Android 스튜디오가 Inspection Results에서 문제를 삭제합니다.
  7. 코드에서 binding을 살펴보면 Android 스튜디오가 선언 앞에 private 키워드를 추가한 것을 확인할 수 있습니다.
private lateinit var binding: ActivityMainBinding
  1. Variable declaration could be inlined 메시지가 표시될 때까지 결과에서 회색 삼각형을 클릭합니다. Android 스튜디오가 다시 일부 코드를 표시하지만 이번에는 selectedId 변수를 강조표시합니다. 781017cbcada1194.png
  2. 코드를 살펴보면 selectedId가 두 번만 사용되는 것을 확인할 수 있습니다. 먼저 tipOptions.checkedRadioButtonId의 값이 할당되는 강조표시된 줄에서 그리고 그다음 줄의 when에서 사용됩니다.
  3. Inline variable 버튼을 누릅니다. Android 스튜디오가 when 표현식의 selectedId를 앞 코드 줄에서 할당된 값으로 바꿉니다. 그런 다음, 더 이상 필요하지 않으므로 이전 코드 줄을 완전히 삭제합니다.
val tipPercentage = when (binding.tipOptions.checkedRadioButtonId) {
    R.id.option_twenty_percent -> 0.20
    R.id.option_eighteen_percent -> 0.18
    else -> 0.15
}

정말 멋지네요! 코드의 줄 수가 한 줄 더 적습니다. 그리고 변수가 하나 더 적습니다.

불필요한 변수 삭제

Android 스튜디오에 검사 결과가 더 이상 없습니다. 그러나 코드를 자세히 살펴보면 방금 변경한 것과 비슷한 패턴을 확인할 수 있습니다. roundUp 변수가 한 줄에 할당되어 그다음 줄에서 사용되고 다른 곳에서는 사용되지 않습니다.

  1. roundUp이 할당된 줄에서 = 오른쪽에 있는 표현식을 복사합니다.
val roundUp = binding.roundUpSwitch.isChecked
  1. 그다음 줄의 roundUp을 방금 복사한 표현식인 binding.roundUpSwitch.isChecked로 바꿉니다.
if (binding.roundUpSwitch.isChecked) {
    tip = kotlin.math.ceil(tip)
}
  1. roundUp이 있는 코드 줄은 더 이상 필요하지 않으므로 삭제합니다.

Android 스튜디오가 selectedId 변수와 관련하여 작업에 도움을 준 것과 동일한 작업을 해냈습니다. 역시, 코드의 줄 수가 한 줄 더 적습니다. 그리고 변수가 하나 더 적습니다. 이는 사소한 변경사항이지만 코드를 더 간결하고 읽기 쉽게 만드는 데 도움이 됩니다.

(선택사항) 반복 코드 제거

앱이 올바르게 실행되면 코드를 정리하고 더 간결하게 만들 수 있는 다른 기회도 모색할 수 있습니다. 예를 들어 서비스 비용에 값을 입력하지 않으면 앱이 tipResult를 빈 문자열("")이 되도록 업데이트합니다. 값이 있는 경우 NumberFormat을 사용하여 값의 형식을 지정합니다. 이 기능은 앱의 다른 곳에 적용될 수 있습니다. 예를 들어 빈 문자열 대신 0.0의 팁을 표시할 수 있습니다.

매우 유사한 코드의 중복을 줄이기 위해 이러한 두 줄의 코드를 고유한 자체 함수로 추출할 수 있습니다. 이 도우미 함수는 팁 금액 입력을 Double로 가져와서 형식을 지정하고 화면에서 tipResult TextView를 업데이트할 수 있습니다.

  1. MainActivity.kt에서 중복된 코드를 식별합니다. 이러한 코드 줄을 calculateTip() 함수에서는 여러 번, 0.0 케이스에서는 한 번 그리고 일반적인 케이스에서도 한 번 사용할 수 있습니다.
val formattedTip = NumberFormat.getCurrencyInstance().format(0.0)
binding.tipResult.text = getString(R.string.tip_amount, formattedTip)
  1. 중복된 코드를 자체 함수로 이동합니다. 코드의 한 가지 변경사항은 코드가 여러 위치에서 작동하도록 팁을 매개변수로 사용하는 것입니다.
private fun displayTip(tip : Double) {
   val formattedTip = NumberFormat.getCurrencyInstance().format(tip)
   binding.tipResult.text = getString(R.string.tip_amount, formattedTip)
}
  1. displayTip() 도우미 함수를 사용하고 0.0도 확인하도록 calculateTip() 함수를 업데이트합니다.

MainActivity.kt

private fun calculateTip() {
    ...

        // If the cost is null or 0, then display 0 tip and exit this function early.
        if (cost == null || cost == 0.0) {
            displayTip(0.0)
            return
        }

    ...
    if (binding.roundUpSwitch.isChecked) {
        tip = kotlin.math.ceil(tip)
    }

    // Display the formatted tip value on screen
    displayTip(tip)
}

참고

이제 앱이 작동하지만 아직 프로덕션용으로는 준비되지 않았습니다. 더 많이 테스트해야 합니다. 그리고 몇 가지 시각적 효과를 추가하고 머티리얼 디자인 가이드라인을 따라야 합니다. 또한 다음 Codelab에서 앱 테마 및 앱 아이콘을 변경하는 방법도 알아보아야 합니다.

7. 솔루션 코드

이 Codelab의 솔루션 코드는 다음과 같습니다.

966018df4a149822.png

MainActivity.kt

(첫 번째 줄에 관한 참고: 패키지 이름이 com.example.tiptime과 다른 경우 패키지 이름을 바꿉니다.)

package com.example.tiptime

import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import com.example.tiptime.databinding.ActivityMainBinding
import java.text.NumberFormat

class MainActivity : AppCompatActivity() {

   private lateinit var binding: ActivityMainBinding

   override fun onCreate(savedInstanceState: Bundle?) {
       super.onCreate(savedInstanceState)

       binding = ActivityMainBinding.inflate(layoutInflater)
       setContentView(binding.root)

       binding.calculateButton.setOnClickListener { calculateTip() }
   }

   private fun calculateTip() {
       val stringInTextField = binding.costOfService.text.toString()
       val cost = stringInTextField.toDoubleOrNull()
       if (cost == null) {
           binding.tipResult.text = ""
           return
       }

       val tipPercentage = when (binding.tipOptions.checkedRadioButtonId) {
           R.id.option_twenty_percent -> 0.20
           R.id.option_eighteen_percent -> 0.18
           else -> 0.15
       }

       var tip = tipPercentage * cost
       if (binding.roundUpSwitch.isChecked) {
           tip = kotlin.math.ceil(tip)
       }

       val formattedTip = NumberFormat.getCurrencyInstance().format(tip)
       binding.tipResult.text = getString(R.string.tip_amount, formattedTip)
   }
}

strings.xml 수정

<string name="tip_amount">Tip Amount: %s</string>

activity_main.xml 수정

...

<TextView
   android:id="@+id/tip_result"
   ...
   tools:text="Tip Amount: $10" />

...

앱 모듈의 build.gradle 수정

android {
    ...

    buildFeatures {
        viewBinding = true
    }
    ...
}

8. 요약

  • 뷰 결합을 사용하면 앱의 UI 요소와 상호작용하는 코드를 더 쉽게 작성할 수 있습니다.
  • Kotlin의 Double 데이터 유형은 십진수를 저장할 수 있습니다.
  • RadioGroupcheckedRadioButtonId 속성을 사용하여 어떤 RadioButton이 선택되었는지 확인할 수 있습니다.
  • NumberFormat.getCurrencyInstance()를 사용하여 숫자를 통화 형식으로 지정하는 데 이용할 형식 지정 클래스를 가져올 수 있습니다.
  • %s와 같은 문자열 매개변수를 사용하여 다른 언어로 쉽게 변환할 수 있는 동적 문자열을 만들 수 있습니다.
  • 테스트가 중요합니다!
  • Android 스튜디오에서 Logcat을 사용하여 앱 비정상 종료와 같은 문제를 해결할 수 있습니다.
  • 스택 트레이스는 호출된 메서드 목록을 보여 줍니다. 이는 코드가 예외를 생성하는 경우에 유용합니다.
  • 예외는 코드가 예상하지 못한 문제를 나타냅니다.
  • Null은 '값 없음'을 의미합니다.
  • 일부 코드는 null 값을 처리할 수 없으므로 주의해서 사용해야 합니다.
  • Analyze > Inspect Code를 통해 추천을 확인하여 코드를 개선합니다.

9. UI 개선을 위한 추가 Codelab

훌륭합니다. 팁 계산기가 잘 작동합니다! 앱이 더 세련되게 보이도록 UI를 개선하는 방법이 있습니다. 관심이 있는 경우 다음과 같은 추가 Codelab을 확인하여 앱 테마와 앱 아이콘을 변경하는 방법은 물론 Tip Time 앱을 위한 머티리얼 디자인 가이드라인의 권장사항을 준수하는 방법에 관해 더 자세히 알아보세요.

10. 자세히 알아보기

11. 연습하기

  • 이전 연습의 요리용 단위 변환기 앱을 통해 밀리리터와 같은 단위를 액량 온스로 또는 그 반대로 변환하는 로직 및 계산 코드를 추가합니다.