본문 바로가기
  • GDG on campus Ewha Tech Blog
3-1기 스터디/안드로이드 기초

[2주차] Android Basics in Kotlin Unit 1: Kotlin basics (3)(4)

by 해시브라운 🥔 2021. 10. 12.

  Pathway 3  Build a basic layout

Qualities of a great app

  • Effective
  • Efficient
  • Beautiful
  • Accessible

사용자 인터페이스 UI

UI를 통해 앱은 사용자에게 콘텐츠를 표시하고 사용자는 앱과 상호작용한다.

앱의 UI는 화면에 표시되는 텍스트, 이미지, 버튼 등 여러 유형의 요소이다.

이러한 각 요소를 View라고 한다.

ViewGroupView객체가 있을 수 있는 컨테이너로, 내부 View들을 정렬하는 역할을 한다.

ViewGroupConstraintLayout이 있다.

📌 기존 TextView의 Hello World 텍스트를 변경해보자.
📌 ConstraintLayoutTextView를 배치하고 내용을 작성해보자.
📌  텍스트에 스타일 추가해보자.

참고 내용

  • UI의 여백 및 기타 거리 단위는 밀도 독립형 픽셀(dp)입니다. 센티미터나 인치와 비슷하지만 화면상의 거리를 나타냅니다. Android에서는 이 값을 각 기기의 적절한 실제 픽셀 수로 변환합니다. 기준으로 1dp는 1인치의 약 160분의 1이지만 기기에 따라 더 크거나 작을 수 있습니다.
  • dp가 화면상의 거리 측정 단위인 것처럼 sp는 글꼴 크기 측정 단위입니다. Android 앱의 UI 요소는 두 가지 측정 단위를 사용합니다. 하나는 밀도 독립형 픽셀(dp)로, 이전에 레이아웃에서 사용했고 또 하나는 확장 가능한 픽셀(sp)로, 텍스트 크기를 설정할 때 사용합니다. 기본적으로 sp는 dp와 같은 크기이지만 sp는 사용자가 선호하는 텍스트 크기에 따라 크기가 조절됩니다.
  • 대체 비트맵 제공
    픽셀 밀도가 서로 다른 기기에서 좋은 그래픽 품질을 제공하기 위해서는 앱에 있는 각 비트맵에 관해 밀도 버킷마다 해당하는 해상도로 여러 개의 비트맵 버전을 제공한다.
    밀도 한정자 mdpi hdpi xhdpi 등등 에 따라 다른 비트맵 버전 제공.생성된 이미지 파일을 res/의 적절한 하위 디렉터리에 배치하면 시스템에서 앱이 실행되는 기기의 픽셀 밀도에 따라 자동으로 알맞은 크기를 선택한다.
  • 앱 아이콘은 mipmap
    일부 앱 런처는 기기의 밀도 버킷에서 요구하는 것보다 최대 25% 더 크게 아이콘을 표시한다. 즉 아이콘이 선명하지 않게 표시된다.
    앱 아이콘이 확장될 수 있기 때문에 모든 앱 아이콘을 drawable 디렉터리가 아닌 mipmap 디렉터리에 넣어야 한다. 밀도별 APK를 빌드하더라도 drawable 디렉터리와 달리, 모든 mipmap 디렉터리는 APK에 유지되기 때문!
  • 벡터 그래픽 사용하기
    벡터 그래픽은 크기를 확장해도 깨지지 않으니까 이걸 쓰는 것도 하나의 방법

📌  ImageView 추가하기

Android 스튜디오의 Resource Manager를 사용해 이미지와 기타 리소스를 추가하고 구성할 수 있다.

  1. 사용할 이미지를 drawable에 넣고, ImageView를 화면에 배치해 이미지를 화면에 올릴 수 있다.
  2. ConstraintLayout의 가로세로 제약조건을 추가한다.
  3. 전체 화면에 채우기 위해서는 ScaleType을 설정해야 한다. 여기서는 CENTER_CROP으로 설정한다. CENTER_CROP 으로 설정하면 이미지가 비율을 유지하면서 늘어나 화면에 꽉 차게 된다. (실제 이미지는 화면을 넘어간 크기)
    • ScaleType 참고 내용CENTER : center에 배치할뿐 scaling은 없다.CENTER_INSIDE : 비율을 유지하며 scale 한다. 이미지의 크기는 view보다 작거나 같다.
    • CENTER_CROP : 비율을 유지하며 scale 한다. 이미지의 크기는 view보다 크거나 같다.
  4. ImageView를 위로 올려 TextView 뒤로 가도록 배치한다.

📌  하드코딩한 문자열을 수정하자 Extract Resource

앱은 이후에 다른 언어로 번역될 수 있다. 하드코딩 문자열로 인해 앱을 다른 언어로 번역하기가 더 어렵고 앱의 다른 위치에서 문자열을 재사용하기가 어렵다. 이러한 문제는 '리소스 파일로 문자열을 추출'하여 해결할 수 있다.

Extract Resource 한 텍스트는 res>values>strings.xml에 있다.

// extract 하기 전
android:text="Happy Birthday, Android!"
// extract resource 후
android:text="@string/happy_birthday_text"

📌 앱 접근성 확인

앞서 추가한 ImageView에 'contentDescription'속성에 관한 경고가 있다. 콘텐츠 설명을 통해 UI 요소의 목적을 정의하여 음성 안내 지원과 함께 앱을 더 유용하게 활용할 수 있다.

이 이미지는 장식 목적으로만 사용하기 때문에 콘텐츠 설명을 작성하는 대신 importantForAccessibility속성을 no로 설정해 음성 안내 지원이 이 이미지에 대한 설명은 건너뛰도록 한다.

📌  텍스트에 그림자 효과 적용하기

shadowColor, shadowDx, shadowDy, shadowRadius를 설정해 텍스트에 그림자 효과를 적용할 수 있다. 다음과 같이 값을 설정했다.
shadowColor #2E2E2E shadowDx 7.0 shadowDy 7.0 shadowRadius 7.0

Happy Birthday app

  Pathway 4  Add a button to an app

Kotlin의 클래스 및 객체 인스턴스

📌 랜덤 숫자 random()

IntRange는 데이터 유형의 하나로, 시작점부터 끝점까지 정수의 범위를 나타낸다.

fun main() {
    val diceRange = 1..6
    val randomNumber = diceRange.random()
    println("random number: ${randomNumber}")
}

이 코드로 1부터 6 사이의 랜덤 정수를 출력할 수 있다.

val diceRange = 1..6 여기서 자료형을 지정해주지 않았지만 시스템은 알아서
val diceRange: IntRange = 1..6 라고 해석한다.

( 변수형을 처음부터 지정하는 Java에 익숙한 나한테는 신기... 🤔 )

fun main() {
    val randomNumber = (1..6).random()
    println("random number: ${randomNumber}")
}

(1..6).random() 로 작성해도 결과는 똑같다.

📌 Dice 클래스 만들기

Kotlin에서는 주사위에 면이 있고 랜덤 숫자를 굴릴 수 있다고 표시하는 프로그래매틱 방식의 주사위 청사진을 만들 수 있다. 이 청사진을 클래스라고 한다.

클래스는 건축가의 청사진 도면. 즉 주택을 짓는 방법에 관한 안내도이고,
주택은 청사진에 따라 만들어진 실제 사물 또는 객체 인스턴스 다.

주사위에 관한 모든 내용을 클래스로 구성하는 작업을 캡슐화라고 한다. 논리적으로 관련된 기능을 단일 위치로 묶을 수 있다는 의미!

fun main() {
    val myFirstDice = Dice()
    myFirstDice.roll()
}
class Dice{
    var sides = 6
    fun roll(){
        val randomNumber = (1..6).random()
           println(randomNumber)
    }
}

Kotlin에서는 class 키워드로 새 클래스를 만든다.

클래스의 이름은 파스칼 표기법으로 작성한다. 예: CustomerRecord ParkingMeter

Dice 클래스 내부에는 주사위 개념에 대한 청사진이 담겨있다. 실제 주사위를 만드려면 이 청사진을 이용해 Dice 객체 인스턴스를 생성해야 한다.

val myFirstDice = Dice() 이 처럼 Dice 객체를 생성할 수 있다.

클래스 내에는 주사위를 굴리는 함수가 있다. 클래스 내에서 정의된 함수는 메서드라고 한다.

이제 출력은 main에서, 함수는 값을 반환하도록 수정하자.

fun main() {
    val myFirstDice = Dice()
    val diceRoll = myFirstDice.roll()
    println("${myFirstDice.sides} sided dice")
    println("Dice result: ${diceRoll}")
}
class Dice{
    var sides = 6
    fun roll(): Int{
        val randomNumber = (1..6).random()
             return randomNumber
    }
}
// 출력 결과
// 6 sided dice
// Dice result: 3

val diceRoll = myFirstDice.roll() 함수가 반환한 값을 받을 diceRoll 변수를 선언하고, roll() 함수에 반환할 데이터 유형을 지정하고, return 문을 작성한다.

📌 주사위의 면 수 변경하기

fun main() {
    val myFirstDice = Dice()
    val diceRoll = myFirstDice.roll()
    println("${myFirstDice.sides} sided dice")
    println("Dice result: ${diceRoll}")
    myFirstDice.sides = 20
    println("${myFirstDice.sides} sided dice")
    println("Dice result: ${myFirstDice.roll()}")
}
class Dice{
    var sides = 6
    fun roll(): Int{
        val randomNumber = (1..sides).random()
             return randomNumber
    }
}
/* 출력 결과
6 sided dice
Dice result: 2
20 sided dice
Dice result: 11
*/

roll() 함수의 randomNumber 생성 부분을 수정하고, main에서 myFirstDice.sides = 20 로 주사위의 면 수를 변경한다.

println("Dice result: ${myFirstDice.roll()}") 그 다음 프린트문에서 다시 roll()을 실행시켜 새로운 값을 받아와 출력한다.

❗ var 와 val
여기서 sides는 var로 선언되어 있기 때문에 변경 가능하다. val로 선언된 변수는 값을 변경할 수 없다.

📌 주사위 클래스 수정하기

실제 주사위는 면의 수를 변경할 수 없다. 면 수는 구매할 때 결정하는 것이기 때문에 객체 생성시 정하고 그 뒤로는 변경할 수 없는 게 맞다.

fun main() {
    val myFirstDice = Dice(6)
    println("${myFirstDice.numSides} sided dice\nDice result: ${myFirstDice.roll()}")
    val mySecondDice = Dice(20)
    println("${mySecondDice.numSides} sided dice\nDice result: ${mySecondDice.roll()}")
}
class Dice(val numSides: Int){
    fun roll(): Int{
        return (1..numSides).random()
    }
}
/*
6 sided dice
Dice result: 1
20 sided dice
Dice result: 11
*/

class Dice(val numSides: Int) Dice 클래스를 수정하여 생성 시 numSides 변수에 값을 넣도록 한다.

이 변수는 수정할 수 없기 때문에 main의 코드도 수정해야 한다. myFirstDice의 면을 수정하는 것이 아니라 면이 20개인 Dice 객체를 mySecondDice라는 이름으로 새롭게 생성한다.

리팩토링 과정을 통해 코드도 간결하게 수정했다. roll()의 결과값을 담을 변수를 선언하지 않고, 바로 출력하고, roll() 메소드 내부에서도 값을 바로 리턴하도록 한다.

📌 연습하기 1 주사위에 색상 속성 추가하기

fun main() {
    val myFirstDice = Dice(6, "Red")
    println("${myFirstDice.numSides} sided ${myFirstDice.color} dice\nResult: ${myFirstDice.roll()}")
    val mySecondDice = Dice(10, "Blue")
    println("${mySecondDice.numSides} sided ${mySecondDice.color} dice\nResult: ${mySecondDice.roll()}")
    val myThirdDice = Dice(20, "Green")
    println("${myThirdDice.numSides} sided ${myThirdDice.color} dice\nResult: ${myThirdDice.roll()}")
}
class Dice(val numSides: Int, val color: String){
    fun roll(): Int{
        return (1..numSides).random()
    }
}
/*
6 sided Red dice
Result: 1
10 sided Blue dice
Result: 4
20 sided Green dice
Result: 14
*/

📌 연습하기 2 Coin 뒤집기

fun main(){
    val myFirstCoin = Coin()
    println("Result: ${myFirstCoin.flip()}")
}
class Coin(){
    fun flip(): String{
        if((0..1).random()==0){
            return "Head"
        }
        else{
            return "Tail"
        }
    }
}
// Result: Tail

❗ 코틀린에는 삼항연산자가 없다...

대신 if((0..1).random()==0) return "Head" else return "Tail" 이렇게 사용할 수 있다.

fun main(){
    val myFirstCoin = Coin()
    println("Result: ${myFirstCoin.flip()}")
}
class Coin(){
    fun flip(): String{
        if((0..1).random()==0) return "Head" else return "Tail"
    }
}

Dice Roller 앱 만들기

📌 Activity

Activity는 앱이 UI를 그리는 창을 제공한다. 일반적으로 Activity는 실행되는 앱의 전체 화면을 차지한다. 모든 앱에는 하나 이상의 activity가 존재한다.

📌 MainActivity.kt 살펴보기

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
    }
}

기본적으로 빈 프로젝트를 생성했을 때 작성되어 있는 코드다.

MainActivity에는 main() 함수가 없다. Android 앱은 main() 함수를 호출하는 대신 앱이 처음 열릴 때 MainActivityonCreate() 메서드를 호출한다.

setContentView()로 시작 레이아웃을 MainActivity 로 설정한다.

📌 Toast 메시지

val toast = Toast.makeText(this, "Dice Rolled!", Toast.LENGTH_SHORT)
toast.show()
// 또는 
Toast.makeText(this, "Dice Rolled!", Toast.LENGTH_SHORT).show(

📌 1차로 완성된 코드

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        val rollButton: Button = findViewById(R.id.button)
        rollButton.setOnClickListener{
            rollDice()
        }
    }
    private fun rollDice() {
        val dice = Dice(6)
        val diceRoll = dice.roll()
        val resultTV: TextView = findViewById(R.id.textView)
        resultTV.text = diceRoll.toString()
    }
}
class Dice(val numSides: Int){
    fun roll(): Int{
        return (1..numSides).random()
    }
}

어플을 실행하면 버튼을 누를 때마다 랜덤한 주사위의 숫자가 랜덤하게 나오는 것을 확인할 수 있다.


📌 이제 코드를 정리해보자

코드를 전부 선택(ctrl+A)하고 Code > Reformat Code 또는 ctrl+alt+L 하면 공백이나 간격 등이 형식에 알맞게 수정된다.

코드에 대한 주석도 추가한다.

/**
 * 이 액티비티는 사용자가 주사위를 굴려 그 결과를 화면에 볼 수 있도록 한다.
 */
class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        val rollButton: Button = findViewById(R.id.button)
        rollButton.setOnClickListener { rollDice() }
    }

    /**
     * 주사위를 굴리고 그 결과를 화면에 업데이트한다.
     */
    private fun rollDice() {
        // 6면 주사위를 생성한다.
        val dice = Dice(6)
        val diceRoll = dice.roll()
        // 화면에 주사위 결과를 업데이트한다.
        val resultTV: TextView = findViewById(R.id.textView)
        resultTV.text = diceRoll.toString()
    }
}

class Dice(val numSides: Int) {
    fun roll(): Int {
        return (1..numSides).random()
    }
}

📌 연습하기 - 주사위 2개 굴리기

/**
 * 이 액티비티는 사용자가 주사위를 굴려 그 결과를 화면에 볼 수 있도록 한다.
 */
class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        val rollButton: Button = findViewById(R.id.button)
        rollButton.setOnClickListener { rollDice() }
    }

    /**
     * 주사위를 굴리고 그 결과를 화면에 업데이트한다.
     */
    @SuppressLint("SetTextI18n")
    private fun rollDice() {
        // 6면 주사위를 생성한다.
        val dice = Dice(6)
        val diceRoll = dice.roll()
        val dice2 = Dice(20)
        // 화면에 주사위 결과를 업데이트한다.
        val resultTV: TextView = findViewById(R.id.textView)
        resultTV.text = "${dice.numSides} sided Dice: "+diceRoll.toString()
        val resultTV2: TextView = findViewById(R.id.textView2)
        resultTV2.text = "${dice2.numSides} sided Dice: "+dice2.roll().toString()
    }
}

class Dice(val numSides: Int) {
    fun roll(): Int {
        return (1..numSides).random()
    }
}

@SuppressLint("SetTextI18n") 이게 뭔지는 잘 모르겠다🤔 없어도 실행은 잘 되는데 안드로이드 스튜디오가 추가하래서 추가했다😅
Stackoverflow 참고 하드코딩과 관련된 것 같다! 예를들어 돈을 표현할 때 나라마다 단위가 다른데 코드를 수정하지 않아도 알아서 바뀌도록?

조건부 동작 익히기

📌  if

if else 조건문은 다른 언어와 거의 똑같기 때문에 실습한 코드만 첨부한다.

fun main() {
    val myFirstDice = Dice(6)
    val rollResult = myFirstDice.roll()
    val luckyNumber = 4
    if(rollResult==luckyNumber){
        println("You win:)")
    }else if(rollResult==1){
        println("You lose:( You rolled a 1. Try again!")
    }else if(rollResult==2){
        println("You lose:( You rolled a 2. Try again!")
    }else if(rollResult==3){
        println("You lose:( You rolled a 3. Try again!")
    }else if(rollResult==5){
        println("You lose:( You rolled a 5. Try again!")
    }else{
        println("You lose:( You rolled a 6. Try again!")
    }
}
class Dice (val numSides: Int) {
    fun roll(): Int {
        return (1..numSides).random()
    }
}

📌  When 문 사용하기

when 문은 자바나 c++에서 사용했던 switch문과 유사하다. when()안에 있는 rollResult의 값이 어느 것에 해당하냐에 따라 실행할 코드를 작성할 수 있다.

fun main() {
    val myFirstDice = Dice(6)
    val rollResult = myFirstDice.roll()
    val luckyNumber = 4
    when(rollResult){
        luckyNumber -> println("You win:)")
        1 -> println("You lose:( You rolled a 1. Try again!")
        2 -> println("You lose:( You rolled a 2. Try again!")
        3 -> println("You lose:( You rolled a 3. Try again!")
        5 -> println("You lose:( You rolled a 5. Try again!")
        6 -> println("You lose:( You rolled a 6. Try again!")
    }
}
class Dice (val numSides: Int) {
    fun roll(): Int {
        return (1..numSides).random()
    }
}

Dice Roller 앱에 주사위 이미지 추가하기

기존 TextView를 없애고, 주사위 이미지를 담을 ImageView를 추가했다. 6개의 주사위 이미지를 drawable에 담고, MainActivity.kt 코드를 수정한다.

private fun rollDice() {
        // 6면 주사위를 생성한다.
        val dice = Dice(6)
        val diceRoll = dice.roll()
        val diceImage: ImageView = findViewById(R.id.imageView)
        when(diceRoll){
            1 -> diceImage.setImageResource(R.drawable.dice_1)
            2 -> diceImage.setImageResource(R.drawable.dice_2)
            3 -> diceImage.setImageResource(R.drawable.dice_3)
            4 -> diceImage.setImageResource(R.drawable.dice_4)
            5 -> diceImage.setImageResource(R.drawable.dice_5)
            6 -> diceImage.setImageResource(R.drawable.dice_6)
        }
}

rollDice() 함수의 코드를 작성하면 다음과 같이 주사위 앱이 완성된다.

        /**
     * 주사위를 굴리고 그 결과를 화면에 업데이트한다.
     */
    private fun rollDice() {
        // 6면 주사위를 생성하고 주사위를 굴린다
        val dice = Dice(6)
        val diceRoll = dice.roll()
        // imageView 를 찾는다
        val diceImage: ImageView = findViewById(R.id.imageView)
        // 주사위를 굴린 결과에 해당하는 drawable resource 를 찾는다
        val drawableResource = when(diceRoll){
            1 -> R.drawable.dice_1
            2 -> R.drawable.dice_2
            3 -> R.drawable.dice_3
            4 -> R.drawable.dice_4
            5 -> R.drawable.dice_5
            else -> R.drawable.dice_6
        }
        // Update ImageView
        diceImage.setImageResource(drawableResource)
        // Update content description
        diceImage.contentDescription = diceRoll.toString()
    }

📌  연습하기 - 주사위 2개 굴리기

프로젝트 - Lemonade

Lemonade 앱 개요

4가지 state가 있다. 처음 앱은 SELECT 상태로 시작된다.

SELECT 상태에서 이미지를 클릭하면, SQUEEZE 화면으로 바뀌고, squeezeCount가 0으로 초기화되고, lemonSize가 2~4 사이 랜덤 숫자로 지정된다.

SQUEEZE 상태에서 이미지를 클릭하면, 클릭할 때 마다. squeezeCount는 +1, lemonSize는 -1이 되어, lemonSize가 0이 되는 순간 DRINK 상태로 전환한다.

DRINK 상태에서 이미지를 클릭하면 RESTART 상태로 전환하고, lemonSize=-1이 된다.

RESTART 상태에서 이미지를 클릭하면 SELECT로 돌아간다.

lemonImage!!.setOnClickListener {
        // TODO: call the method that handles the state when the image is clicked
        clickLemonImage()
}

중앙의 이미지를 클릭할 때마다 setOnClickListener 가 실행된다. 여기서 clickLemonImage() 함수를 호출한다.

private fun clickLemonImage() {
        if(lemonadeState==SELECT){
            lemonadeState=SQUEEZE
            setViewElements()
            lemonSize = lemonTree.pick()
            squeezeCount=0
        }else if(lemonadeState==SQUEEZE){
            squeezeCount++
            lemonSize--
            if(lemonSize==0) {
                lemonadeState = DRINK
                setViewElements()
            }
        }else if(lemonadeState==DRINK){
            lemonadeState=RESTART
            setViewElements()
            lemonSize=-1
        }else if(lemonadeState==RESTART){
            lemonadeState=SELECT
            setViewElements()
        }
}

clickLemonImage() 함수에서는 문제에서 주어진 조건을 작성했다. SELECT만 보면, lemonadeState=SQUEEZE 로 상태를 SQUEEZE로 변경하고, setViewElements()를 호출해 이미지를 변경하고, lemonSizepick() 함수를 호출해 랜덤 수를 지정하고, sqeezeCount를 0으로 초기화한다.

setViewElements()는 다음과 같다.

private fun setViewElements() {
        // TextView 와 ImageView 를 findViewById로 찾기
        val textAction: TextView = findViewById(R.id.text_action)
        val imageAction: ImageView = findViewById(R.id.image_lemon_state)
        val newText = when (lemonadeState) {
            SELECT -> resources.getString(R.string.lemon_select)
            SQUEEZE -> resources.getString(R.string.lemon_squeeze)
            DRINK -> resources.getString(R.string.lemon_drink)
            else -> resources.getString(R.string.lemon_empty_glass)
        }
        // 화면의 텍스트 변경
        textAction.setText(newText)
        // newImageResource에 lemonadeState에 대응하는 이미지 리소스 담기
        val newImageResource = when (lemonadeState) {
            SELECT -> R.drawable.lemon_tree
            SQUEEZE -> R.drawable.lemon_squeeze
            DRINK -> R.drawable.lemon_drink
            else -> R.drawable.lemon_restart
        }
        // 화면의 이미지 변경
        imageAction.setImageResource(newImageResource)
}

val submitText = getResources().getString(R.string.submit_label)
강의에서 string.xml에 있는 resource에 이렇게 액세스 하라는데, 나는 안된다. 🤔
resources.getString(R.string.lemon_select) 대신 이렇게는 됨.
getString(R.string.lemon_select) 단순히 getString만 사용하는 것도 가능한 것 같다.

MainActivity.kt 전체 코드

package com.example.lemonade

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.widget.ImageView
import android.widget.TextView
import com.google.android.material.snackbar.Snackbar

class MainActivity : AppCompatActivity() {

    private val LEMONADE_STATE = "LEMONADE_STATE"
    private val LEMON_SIZE = "LEMON_SIZE"
    private val SQUEEZE_COUNT = "SQUEEZE_COUNT"

    // SELECT represents the "pick lemon" state
    private val SELECT = "select"

    // SQUEEZE represents the "squeeze lemon" state
    private val SQUEEZE = "squeeze"

    // DRINK represents the "drink lemonade" state
    private val DRINK = "drink"

    // RESTART represents the state where the lemonade has be drunk and the glass is empty
    private val RESTART = "restart"

    // Default the state to select
    private var lemonadeState = "select"

    // Default lemonSize to -1
    private var lemonSize = -1

    // Default the squeezeCount to -1
    private var squeezeCount = -1

    private var lemonTree = LemonTree()
    private var lemonImage: ImageView? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // === DO NOT ALTER THE CODE IN THE FOLLOWING IF STATEMENT ===
        if (savedInstanceState != null) {
            lemonadeState = savedInstanceState.getString(LEMONADE_STATE, "select")
            lemonSize = savedInstanceState.getInt(LEMON_SIZE, -1)
            squeezeCount = savedInstanceState.getInt(SQUEEZE_COUNT, -1)
        }
        // === END IF STATEMENT ===

        lemonImage = findViewById(R.id.image_lemon_state)
        setViewElements()
        lemonImage!!.setOnClickListener {
            // TODO: call the method that handles the state when the image is clicked
            clickLemonImage()
        }
        lemonImage!!.setOnLongClickListener {
            // TODO: replace 'false' with a call to the function that shows the squeeze count
            showSnackbar()
        }
    }

    /**
     * === DO NOT ALTER THIS METHOD ===
     *
     * This method saves the state of the app if it is put in the background.
     */
    override fun onSaveInstanceState(outState: Bundle) {
        outState.putString(LEMONADE_STATE, lemonadeState)
        outState.putInt(LEMON_SIZE, lemonSize)
        outState.putInt(SQUEEZE_COUNT, squeezeCount)
        super.onSaveInstanceState(outState)
    }

    private fun clickLemonImage() {
        if (lemonadeState == SELECT) {
            lemonadeState = SQUEEZE
            setViewElements()
            lemonSize = lemonTree.pick()
            squeezeCount = 0
        } else if (lemonadeState == SQUEEZE) {
            squeezeCount++
            lemonSize--
            if (lemonSize == 0) {
                lemonadeState = DRINK
                setViewElements()
            }
        } else if (lemonadeState == DRINK) {
            lemonadeState = RESTART
            setViewElements()
            lemonSize = -1
        } else if (lemonadeState == RESTART) {
            lemonadeState = SELECT
            setViewElements()
        }
    }

    private fun setViewElements() {
        // TextView 와 ImageView 를 findViewById로 찾기
        val textAction: TextView = findViewById(R.id.text_action)
        val imageAction: ImageView = findViewById(R.id.image_lemon_state)

        // newText 에 lemonadeState에 대응하는 문자열을 담기
        val newText = when (lemonadeState) {
            SELECT -> getString(R.string.lemon_select)
            SQUEEZE -> resources.getString(R.string.lemon_squeeze)
            DRINK -> resources.getString(R.string.lemon_drink)
            else -> resources.getString(R.string.lemon_empty_glass)
        }
        // 화면의 텍스트 변경
        textAction.setText(newText)
        // newImageResource에 lemonadeState에 대응하는 이미지 리소스 담기
        val newImageResource = when (lemonadeState) {
            SELECT -> R.drawable.lemon_tree
            SQUEEZE -> R.drawable.lemon_squeeze
            DRINK -> R.drawable.lemon_drink
            else -> R.drawable.lemon_restart
        }
        // 화면의 이미지 변경
        imageAction.setImageResource(newImageResource)
    }

    private fun showSnackbar(): Boolean {
        if (lemonadeState != SQUEEZE) {
            return false
        }
        val squeezeText = getString(R.string.squeeze_count, squeezeCount)
        Snackbar.make(
            findViewById(R.id.constraint_Layout),
            squeezeText,
            Snackbar.LENGTH_SHORT
        ).show()
        return true
    }
}

class LemonTree {
    fun pick(): Int {
        return (2..4).random()
    }
}

📌 테스트 코드 돌리기

강의에서 기본적으로 제공하는 테스트 코드가 있다.
전체 코드를 run 하면 어플이 우다다다 실행했다 꺼졌다한다. 😲
실행이 완료되면 초록 체크 표시를 확인할 수 있다.

댓글