"[학습] Android Basics in Kotlin 과정 소개" > Unit 1: Kotlin 기초 > "App에 버튼 생성" 중 다음 내용을 요약했습니다.
- Project: Lemonade app (원문, 원문 최종 업데이트일: 2022.03.23)
학습일: 2023.01.10
- 앱 소개: 레모네이드 한 잔이 나올 때까지 레몬 즙을 짜는 간단한 대화형 앱
- 사용 요소: ConstraintLayout
, TextView
, ImageView
- 주요 학습 내용
- 지금까지 배운 내용으로 "다운받은 소스"에 필요한 코드를 추가하여 앱이 동작하도록 합니다.
- 테스트 코드가 "다운받은 소스"에 포함되어있습니다. 추가한 코드가 테스트를 통과하는지 확인합니다.
- 앱 화면
프로젝트 코드 다운로드
1. 아래 링크로 이동합니다.
https://github.com/google-developer-training/android-basics-kotlin-lemonade-app
2. 해당 Git 브랜치가 "main"으로 설정되어 있는지 확인합니다.
3. Code 버튼 클릭 > "Download ZIP" 버튼 클릭하여 다운로드 받은 후 압축을 풉니다.
4. 안드로이드 스튜디오에서 해당 프로젝트를 엽니다. (File > Open)
5. Run 버튼을 클릭하여, 프로젝트가 정상적으로 실행되는 것을 확인합니다.
정상적으로 실행은 되지만, 아직 완성된 프로젝트는 아닙니다.
필요한 코드를 추가하여, 앱을 완성해야 합니다.
// TODO
주석을 참고 하여 코드를 완성합니다.
레이아웃 코드
레이아웃 코드는 다운로드 받은 코드에서 변경된 내용이 없습니다.
다운로드 받은 코드 그대로이지만, 참고용으로 함께 공유합니다.
최종 레이아웃 코드
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (C) 2021 The Android Open Source Project.
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/constraint_Layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:id="@+id/text_action"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/lemon_select"
android:textSize="18sp"
android:textAppearance="@style/TextAppearance.AppCompat.Body1"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toTopOf="@id/image_lemon_state"/>
<ImageView
android:id="@+id/image_lemon_state"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
문자열 리소스
문자열 리소스를 한글로 변경했습니다.
squeeze_count
의 경우 단위 테스트에서 사용되는 부분이 있어 영어를 그대로 사용했습니다.
최종 문자열 리소스 코드
<!--
~ Copyright (C) 2021 The Android Open Source Project.
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<resources>
<string name="app_name">Lemonade</string>
<string name="lemon_select">레몬을 클릭하여 선택합니다!</string>
<string name="lemon_squeeze">레몬을 클릭하여 즙을 냅니다!</string>
<string name="lemon_drink">레모네이드를 클릭하여 마십니다!</string>
<string name="lemon_empty_glass">클릭하여 다시 시작합니다!</string>
<string name="squeeze_count">Squeeze count: %1$d, keep squeezing!</string>
</resources>
Kotlin 소스 코드
// TODO:
로 표시된 주석을 참고하여 작업을 완료한 저의 소스 코드입니다. (각자의 방식으로 더 좋은 코드를 작성하실 수 있습니다.)
// TODO:
주석을 한글로 변경하였습니다.
// TODO:
표시된 부분에 코드를 추가했습니다.
최종 Kotlin 소스 코드
/*
* Copyright (C) 2021 The Android Open Source Project.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.lemonade
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.util.Log
import android.widget.ImageView
import android.widget.TextView
import com.google.android.material.snackbar.Snackbar
private const val TAG = "MainActivity"
class MainActivity : AppCompatActivity() {
/**
* DO NOT ALTER ANY VARIABLE OR VALUE NAMES OR THEIR INITIAL VALUES.
*
* Anything labeled var instead of val is expected to be changed in the functions but DO NOT
* alter their initial values declared here, this could cause the app to not function properly.
*/
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 been 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: 이미지를 클릭했을 때 상태를 처리하는 메서드 호출
clickLemonImage()
}
lemonImage!!.setOnLongClickListener {
// TODO:'false'를 squeeze 횟수를 보여주는 함수로 바꾸기
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)
}
/**
* Clicking will elicit a different response depending on the state.
* This method determines the state and proceeds with the correct action.
*/
private fun clickLemonImage() {
// TODO: 'if' 또는 'when' 조건문을 사용하여 이미지가 클릭됐을 때 lemonadeState를 추적합니다.
// 레모네이드를 만드는 과정의 다음 단계로 상태를 변경해야 할 수도 있습니다.
// (또는 레몬을 짜는 경우에는 현재 상태를 약간 변경해야합니다.) 이 조건문에서 작업되어야 합니다.
// TODO: SELECT 상태에서 이미지가 클릭되면, 상태는 SQUEEZE가 되어야 합니다.
// - lemonSize 변수는 LemonTree class의 'pick()' 메서드를 사용하여 설정됩니다.
// - squeezeCount는 아직 레몬을 짜지 않았으므로 0이어야 합니다.
if(lemonadeState == SELECT){
lemonadeState = SQUEEZE
lemonSize =lemonTree.pick()
squeezeCount = 0
// 몇 번 짜야하는지 표시가 안되서, 임의로 추가한 LOG입니다.
Log.i(TAG, "lemonSize:$lemonSize")
}
// TODO: SQUEEZE 상태에서 이미지가 클릭되면 squeezeCount는 1씩 증가시키고, lemonSize는 1씩 감소시켜야 합니다.
// - lemonSize가 0이 되면, 주스가 되고 DRINK 상태가 되어야 합니다.
// - 또한, lemonSize는 더이상 관련이 없으므로 -1로 설정되어야 합니다.
else if(lemonadeState == SQUEEZE){
squeezeCount++
if(--lemonSize == 0){
lemonadeState = DRINK
}
}
// TODO: DRINK 상태에서 이미지가 클릭되면 상태는 RESTART가 되어야 합니다.
else if(lemonadeState == DRINK){
lemonadeState = RESTART
lemonSize = -1
}
// TODO: RESTART 상태에서 이미지가 클릭되면 상태는 SELECT가 되어야 합니다.
else if(lemonadeState == RESTART){
lemonadeState = SELECT
}
// TODO: 마지막으로, 함수가 종료되기 전에 뷰 요소들을 설정하여 UI를 상태에 맞게 합니다.
setViewElements()
}
/**
* Set up the view elements according to the state.
*/
private fun setViewElements() {
val textAction: TextView = findViewById(R.id.text_action)
// TODO: lemonadeState를 추적하는 조건문을 설정합니다.
// TODO: 각 상태에서, textAction TextView는 string resource 파일에서 알맞은 문자열로 설정합니다.
// 문자열들은 상태에 맞게 이름지어져 있습니다.
// TODO: 또한, 각 상태에서, lemonImage는 drawable resource에서 알맞은 drawable로 설정합니다.
// drawable은 문자열과 같은 이름을 갖지만, drawable은 문자열이 아니라는 것을 기억하십시오.
when(lemonadeState){
SELECT->{
textAction.text = getString(R.string.lemon_select)
lemonImage!!.setImageResource(R.drawable.lemon_tree)
}
SQUEEZE->{
textAction.text = getString(R.string.lemon_squeeze)
lemonImage!!.setImageResource(R.drawable.lemon_squeeze)
}
DRINK->{
textAction.text = getString(R.string.lemon_drink)
lemonImage!!.setImageResource(R.drawable.lemon_drink)
}
RESTART->{
textAction.text = getString(R.string.lemon_empty_glass)
lemonImage!!.setImageResource(R.drawable.lemon_restart)
}
}
}
/**
* === DO NOT ALTER THIS METHOD ===
*
* Long clicking the lemon image will show how many times the lemon has been squeezed.
*/
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
}
}
/**
* A Lemon tree class with a method to "pick" a lemon. The "size" of the lemon is randomized
* and determines how many times a lemon needs to be squeezed before you get lemonade.
*/
class LemonTree {
fun pick(): Int {
return (2..4).random()
}
}
clickLemonImage()
에는if
문,setViewElements()
에는when
문을 사용했습니다.SELECT
상태에서 클릭할 때, 레몬을 몇 번 짜야하는지(lemonSize
) 표시가 없어Log
를 추가하였습니다:Log.i(TAG, "lemonSize:$lemonSize")
--------------------------------
앱 테스트
app > java > com.example.lemonade (androidTest) > LemonadeTests 파일에 있는 테스트 코드가 모두 성공하는지 확인합니다.
클래스 내의 모든 테스트 실행
방법 1. 프로젝트 창에서 LemonadeTests 우클릭 > Run 'LemonadeTests' 선택
방법 2. LemonadeTests 파일내에서 class LemonadeTests : BaseTest()
왼쪽 옆에 있는 ▶▶ 버튼을 클릭
개별 테스트 실행
방법 3. LemonadeTests 파일내에서 각 함수 옆에 있는 ▶ 버튼 클릭
앱 테스트에 대한 자세한 내용은 원문 > "7. 테스트 안내" 부분을 참고 하세요.
앱 테스트 실패에 대한 내용 추가 (2023.01.11)
어제(2023.01.10)까지는 테스트가 계속 성공했습니다.
오늘(2023.01.11) 다시 테스트를 실행하니, 일부 테스트가 실패합니다. 그것도 랜덤하게 실패합니다.
각 테스트를 개별로 테스트를 하면 성공합니다. 순서를 타는건가 했는데 아니었습니다.
에뮬레이터에서 설정(Settings) 창이 열려있으면 오류가 발생했습니다.
해당 원인은 모르겠습니다만, 설정 창이 영향을 주는 것 같습니다.
오류 메세지는 아래와 같이, TextView 와 view.getText()가 일치 하지 않는 다는 내용입니다.
androidx.test.espresso.base.DefaultFailureHandler$AssertionFailedWithCauseError: 'an instance of android.widget.TextView and view.getText() equals string from resource id: <2131689523>' doesn't match the selected view.
Expected: an instance of android.widget.TextView and view.getText() equals string from resource id: <2131689523> [lemon_squeeze] value: 레몬을 클릭하여 즙을 냅니다!
Got: view.getText() was "레몬을 클릭하여 선택합니다!"
하지만, 이러한 오류가 왜 발생했을까 추적하기 위해, 오류 메세지 발생 전 내용을 확인해 보았습니다.
01-11 01:57:19.825 21611 21611 I ViewInteraction: Performing 'single click' action on view view.getId() is <2131230939/com.example.lemonade:id/image_lemon_state>
01-11 01:57:19.827 586 719 I InputDispatcher: Dropping event because there is no touchable window or gesture monitor at (539, 1253) in display 0.
01-11 01:57:19.827 586 719 W InputDispatcher: Permission denied: injecting event from pid 21611 uid 10149
01-11 01:57:20.087 586 1053 W InputManager-JNI: Input channel object 'b213090 Splash Screen com.example.lemonade.test (client)' was disposed without first being removed with the input manager!
01-11 01:57:20.153 21611 21611 I ViewInteraction: Checking 'MatchesViewAssertion{viewMatcher=(view has effective visibility <VISIBLE> and view.getGlobalVisibleRect() to return non-empty rectangle)}' assertion on view has drawable resource 2131165325
01-11 01:57:20.153 21611 21611 E ViewAssertions: '(view has effective visibility <VISIBLE> and view.getGlobalVisibleRect() to return non-empty rectangle)' check could not be performed because view 'has drawable resource 2131165325' was not found.
01-11 01:57:20.157 21611 21611 I ViewInteraction: Performing 'single click' action on view view.getId() is <2131230939/com.example.lemonade:id/image_lemon_state>
'single click'을 수행하는데, 해당 이벤트를 수행하지 못하고 삭제되었다는 내용으로 읽힙니다.
그러니, 이벤트가 하나씩 밀리게 되니 원하는 상태가 나오지 않는 것이죠.
우선 원인은 알 수 없지만. ㅠㅠ
에뮬레이터에서 실행되는 다른 창들도 닫아주고, 설정(Settings) 창은 필히 닫아주고 다시 실행해보세요.
저의 경우 실패하던 것이 이전처럼 정상적으로 통과되었습니다.
끝까지 읽어 주셔서 감사합니다. ^^
'냐냐한 Dev Study > Android' 카테고리의 다른 글
[학습] 주사위 굴리기 Android 앱 만들기 (Kotlin) (0) | 2023.01.09 |
---|---|
[학습] 생일 축하 메세지 표시하는 간단 Android 앱 만들기 (Kotlin) (0) | 2023.01.06 |
[학습] Kotlin으로 Hello World + 생일 축하 메세지 작성 (0) | 2023.01.05 |
[학습] Android Basics in Kotlin 과정 소개 (0) | 2023.01.04 |
안드로이드 간단 시계(날짜/시간) 만들기 (Java) (0) | 2022.12.27 |