프로젝트: Lunch Tray 앱

1. 시작하기 전에

이 Codelab에서는 개발자가 직접 빌드할 Lunch Tray라는 새로운 앱을 소개합니다. 이 Codelab에서는 Android 스튜디오 내에서의 프로젝트 설정 및 테스트를 비롯하여 Lunch Tray 앱 프로젝트를 완료하는 단계를 안내합니다.

이 Codelab은 이 과정의 나머지 Codelab과 다릅니다. 이전 Codelab과 달리 이 Codelab의 목적은 앱을 빌드하는 방법에 관한 단계별 튜토리얼을 제공하는 것이 아닙니다. 대신 이 Codelab은 개발자가 독립적으로 완료할 프로젝트를 설정하기 위한 것으로, 앱을 완성하고 직접 작업을 확인하는 방법에 관한 안내를 제공합니다.

솔루션 코드 대신 Google에서는 다운로드할 앱의 일부로 테스트 모음을 제공합니다. Android 스튜디오에서 이러한 테스트를 실행하고(이 Codelab 뒷부분에서 실행 방법을 설명함) 코드가 통과하는지 확인합니다. 여러 번 시도해야 할 수 있습니다. 전문 개발자도 첫 번째 시도에서 모든 테스트에 통과하는 경우는 거의 없습니다. 코드가 모든 테스트에 통과하면 이 프로젝트를 완료된 것으로 간주할 수 있습니다.

대조하여 확인할 솔루션 코드만 원할 수 있음을 알고 있습니다. Google에서는 의도적으로 솔루션 코드를 제공하지 않습니다. 전문 개발자가 되는 것이 어떤 것인지 실제로 경험하길 바라기 때문입니다. 그러려면 다음과 같이 아직 연습을 많이 하지 않은 다양한 기술을 사용해야 할 수 있습니다.

  • 알 수 없는 앱의 용어와 오류 메시지, 코드를 Google에서 검색합니다.
  • 코드를 테스트하고 오류를 읽은 후 코드를 변경하고 다시 코드를 테스트합니다.
  • Android 기본사항의 이전 콘텐츠로 돌아가 학습한 내용을 복습합니다.
  • 작동이 보장된 코드(프로젝트에 제공된 코드나 3단원에 있는 다른 앱의 이전 솔루션 코드 등)를 작성하고 있는 코드와 비교합니다.

처음에는 이 작업이 어려워 보일 수도 있지만 3단원을 완료했다면 이 프로젝트도 완수할 수 있을 것입니다. 천천히 진행하면서 끝까지 포기하지 마세요. 할 수 있습니다.

기본 요건

  • 이 프로젝트는 Kotlin으로 배우는 Android 기본사항 과정의 3단원을 완료한 사용자를 대상으로 합니다.

빌드할 항목

  • Lunch Tray라는 음식 주문 앱을 사용하여 데이터 결합과 함께 ViewModel을 구현하고 프래그먼트 간 탐색을 추가합니다.

준비 사항

  • Android 스튜디오가 설치된 컴퓨터

2. 완성된 앱 개요

프로젝트: Lunch Tray에 오신 것을 환영합니다.

아시다시피 탐색은 Android 개발의 기초 부분입니다. 앱을 사용하여 레시피를 검색하든 좋아하는 음식점으로 가는 길을 찾든 음식을 주문하든 여러 콘텐츠 화면을 탐색할 가능성이 큽니다. 이 프로젝트에서는 3단원에서 배운 기술을 활용하여 ViewModel과 데이터 결합, 화면 간 탐색을 구현하는 Lunch Tray라는 점심 주문 앱을 빌드합니다.

다음은 최종 앱 스크린샷입니다. Lunch Tray 앱을 처음 실행하면 'Start Order'라는 버튼 하나가 있는 화면이 표시됩니다.

20fa769d4ba93ef3.png

Start Order를 클릭하면 제공된 선택 항목 중에서 메인 요리를 선택할 수 있습니다. 선택한 요리를 변경할 수 있습니다. 변경하면 하단에 표시되는 Subtotal이 업데이트됩니다.

438b61180d690b3a.png

다음 화면에서는 사이드 메뉴를 추가할 수 있습니다.

768352680759d3e2.png

그다음 화면에서는 주문에 곁들일 수 있는 디저트를 선택할 수 있습니다.

8ee2bf41e9844614.png

마지막으로, 소계, 판매세, 총비용으로 분류된 주문 비용이 간략하게 표시됩니다. 주문을 제출하거나 취소할 수도 있습니다.

61c883c34d94b7f7.png

어느 옵션을 선택하든 첫 화면으로 돌아가게 됩니다. 주문을 제출하면 이를 알리는 토스트 메시지가 화면 하단에 표시됩니다.

acb7d7a5d9843bac.png

3. 시작하기

프로젝트 코드 다운로드

폴더 이름은 android-basics-kotlin-lunch-tray-app입니다. Android 스튜디오에서 프로젝트를 열 때 이 폴더를 선택하세요.

  1. 프로젝트에 제공된 GitHub 저장소 페이지로 이동합니다.
  2. 브랜치 이름이 Codelab에 지정된 브랜치 이름과 일치하는지 확인합니다. 예를 들어 다음 스크린샷에서 브랜치 이름은 main입니다.

1e4c0d2c081a8fd2.png

  1. 프로젝트의 GitHub 페이지에서 Code 버튼을 클릭하여 팝업을 엽니다.

1debcf330fd04c7b.png

  1. 팝업에서 Download ZIP 버튼을 클릭하여 컴퓨터에 프로젝트를 저장합니다. 다운로드가 완료될 때까지 기다립니다.
  2. 컴퓨터에서 파일을 찾습니다(예: Downloads 폴더).
  3. ZIP 파일을 더블클릭하여 압축을 해제합니다. 프로젝트 파일이 포함된 새 폴더가 만들어집니다.

Android 스튜디오에서 프로젝트 열기

  1. Android 스튜디오를 시작합니다.
  2. Welcome to Android Studio 창에서 Open을 클릭합니다.

d8e9dbdeafe9038a.png

참고: Android 스튜디오가 이미 열려 있는 경우 File > Open 메뉴 옵션을 대신 선택합니다.

8d1fda7396afe8e5.png

  1. 파일 브라우저에서 압축 해제된 프로젝트 폴더가 있는 위치로 이동합니다(예: Downloads 폴더).
  2. 프로젝트 폴더를 더블클릭합니다.
  3. Android 스튜디오가 프로젝트를 열 때까지 기다립니다.
  4. Run 버튼 8de56cba7583251f.png을 클릭하여 앱을 빌드하고 실행합니다. 예상대로 작동하는지 확인합니다.

ViewModel과 탐색을 구현하기 전에 잠시 시간을 내어 프로젝트가 제대로 빌드되었는지 확인하고 프로젝트를 숙지합니다. 앱을 처음 실행하면 빈 화면이 표시됩니다. 탐색 그래프를 아직 설정하지 않아 MainActivity가 아무런 프래그먼트도 표시하지 않습니다.

프로젝트 구조는 이미 작업해 본 다른 프로젝트와 비슷합니다. 리소스를 위한 별도의 디렉터리와 함께 데이터, 모델, UI를 위한 별도의 패키지가 제공됩니다.

a19fd8a4bc92f2fc.png

사용자가 주문할 수 있는 모든 점심 옵션(메인 요리, 사이드 메뉴, 디저트)은 model 패키지에서 MenuItem 클래스로 표현됩니다. MenuItem 객체에는 이름, 설명, 가격, 유형이 있습니다.

data class MenuItem(
    val name: String,
    val description: String,
    val price: Double,
    val type: Int
) {
    fun getFormattedPrice(): String = NumberFormat.getCurrencyInstance().format(price)
}

유형은 constants 패키지의 ItemType 객체에서 가져온 정수로 표현됩니다.

object ItemType {
    val ENTREE = 1
    val SIDE_DISH = 2
    val ACCOMPANIMENT = 3
}

개별 MenuItem 객체는 데이터 패키지의 DataSource.kt에서 찾을 수 있습니다.

object DataSource {
    val menuItems = mapOf(
        "cauliflower" to
        MenuItem(
            name = "Cauliflower",
            description = "Whole cauliflower, brined, roasted, and deep fried",
            price = 7.00,
            type = ItemType.ENTREE
        ),
    ...
}

이 객체에는 간단하게 키와 관련 MenuItem으로 구성된 맵이 포함되어 있습니다. 가장 먼저 구현할 ObjectViewModel에서 DataSource에 액세스하게 됩니다.

ViewModel 정의

이전 페이지의 스크린샷에서 보았듯이 앱은 사용자에게 세 가지 선택 항목(메인 요리, 사이드 메뉴, 디저트)을 물어봅니다. 그런 다음 주문 요약 화면에서 소계를 표시하고 선택한 내용에 따라 판매세(주문 총계를 계산하는 데 사용됨)를 계산합니다.

model 패키지에서 OrderViewModel.kt를 열면 몇 가지 변수가 이미 정의되어 있음을 알 수 있습니다. menuItems 속성을 사용하면 간단하게 ViewModel에서 DataSource에 액세스할 수 있습니다.

val menuItems = DataSource.menuItems

우선 previousEntreePrice, previousSidePrice, previousAccompanimentPrice와 관련된 몇 가지 변수도 있습니다. 사용자가 선택할 때마다 소계가 업데이트되므로(마지막에 합산되는 것이 아님) 이러한 변수는 사용자가 다음 화면으로 넘어가기 전에 선택 항목을 변경할 경우 사용자의 이전 선택을 추적하는 데 사용됩니다. 이러한 변수를 사용하여 소계가 이전 선택 항목과 현재 선택 항목 간의 금액 차이를 반영할 수 있도록 합니다.

private var previousEntreePrice = 0.0
private var previousSidePrice = 0.0
private var previousAccompanimentPrice = 0.0

현재 선택한 항목을 저장하기 위한 프라이빗 변수인 _entree, _side, _accompaniment도 있습니다. 유형은 MutableLiveData<MenuItem?>입니다. 각 프라이빗 변수는 공개 지원 속성 entree, side, accompaniment를 동반하며 LiveData<MenuItem?>인 불변 유형을 갖습니다. 이러한 변수는 선택된 항목을 화면에 표시하기 위한 프래그먼트의 레이아웃을 통해 액세스됩니다. 사용자가 메인 요리, 사이드 메뉴 또는 디저트를 선택하지 않을 수 있으므로 LiveData 객체에 포함된 MenuItem은 null도 허용됩니다.

// Entree for the order
private val _entree = MutableLiveData<MenuItem?>()
val entree: LiveData<MenuItem?> = _entree

// Side for the order
private val _side = MutableLiveData<MenuItem?>()
val side: LiveData<MenuItem?> = _side

// Accompaniment for the order.
private val _accompaniment = MutableLiveData<MenuItem?>()
val accompaniment: LiveData<MenuItem?> = _accompaniment

또한 소계, 총계, 세금과 관련된 LiveData 변수도 있습니다. 이러한 변수는 관련 항목이 통화로 표시되도록 숫자 형식을 사용합니다.

// Subtotal for the order
private val _subtotal = MutableLiveData(0.0)
val subtotal: LiveData<String> = Transformations.map(_subtotal) {
    NumberFormat.getCurrencyInstance().format(it)
}

// Total cost of the order
private val _total = MutableLiveData(0.0)
val total: LiveData<String> = Transformations.map(_total) {
    NumberFormat.getCurrencyInstance().format(it)
}

// Tax for the order
private val _tax = MutableLiveData(0.0)
val tax: LiveData<String> = Transformations.map(_tax) {
    NumberFormat.getCurrencyInstance().format(it)
}

마지막으로 세율은 하드코딩된 값 0.08(8%)입니다.

private val taxRate = 0.08

OrderViewModel에는 구현해야 하는 6개의 메서드가 있습니다.

setEntree(), setSide(), setAccompaniment()

메인 요리, 사이드 메뉴, 디저트와 관련된 이러한 메서드는 모두 각각 동일한 방식으로 작동해야 합니다. 예를 들어 setEntree()는 다음을 실행해야 합니다.

  1. _entreenull이 아닌 경우(즉, 사용자가 이미 선택한 메인 요리를 변경함) previousEntreePricecurrent _entree의 가격으로 설정합니다.
  2. _subtotalnull이 아니면 소계에서 previousEntreePrice를 차감합니다.
  3. _entree 값을 함수에 전달된 메인 요리로 업데이트합니다(menuItems를 사용하여 MenuItem에 액세스).
  4. updateSubtotal()을 호출하고 새로 선택한 메인 요리의 가격을 전달합니다.

setSide()setAccompaniment()의 로직은 setEntree()의 구현과 동일합니다.

updateSubtotal()

updateSubtotal()은 소계에 추가되어야 하는 새 가격의 인수로 호출됩니다. 이 메서드는 다음 세 가지 작업을 실행해야 합니다.

  1. _subtotalnull이 아니면 _subtotalitemPrice를 추가합니다.
  2. 그 외의 경우 _subtotalnull이면 _subtotalitemPrice로 설정합니다.
  3. _subtotal가 설정(또는 업데이트)되면 calculateTaxAndTotal()를 호출합니다. 그러면 새 소계를 반영하도록 값이 업데이트됩니다.

calculateTaxAndTotal()

calculateTaxAndTotal()은 소계를 기준으로 세금 및 총액의 변수를 업데이트해야 합니다. 다음과 같이 메서드를 구현합니다.

  1. _tax를 세율에 소계를 곱한 값으로 설정합니다.
  2. _total을 소계에 세금을 더한 값으로 설정합니다.

resetOrder()

resetOrder()는 사용자가 주문을 제출하거나 취소하면 호출됩니다. 사용자가 새 주문을 시작할 경우 앱에 남아 있는 데이터가 없도록 하는 게 좋습니다.

OrderViewModel에서 수정한 모든 변수를 원래 값(0.0 또는 null)으로 다시 설정하여 resetOrder()를 구현합니다.

데이터 결합 변수 만들기

레이아웃 파일에서 데이터 결합을 구현합니다. 레이아웃 파일을 열고 OrderViewModel 유형의 데이터 결합 변수 또는 그에 상응하는 프래그먼트 클래스를 추가합니다.

4개의 레이아웃 파일에서 텍스트와 클릭 리스너를 설정하려면 모든 TODO 주석을 구현해야 합니다.

  1. fragment_entree_menu.xml
  2. fragment_side_menu.xml
  3. fragment_accompaniment_menu.xml
  4. fragment_checkout.xml

각 특정 작업은 레이아웃 파일의 TODO 주석에 설명되어 있지만 단계는 아래에 요약되어 있습니다.

  1. fragment_entree_menu.xml<data> 태그에서 EntreeMenuFragment의 결합 변수를 추가합니다. 각 라디오 버튼과 관련해 라디오 버튼이 선택될 경우에는 ViewModel에 메인 요리를 설정해야 합니다. 그에 따라 소계 텍스트 뷰의 텍스트가 업데이트되어야 합니다. 또한 주문을 취소하거나 다음 화면으로 넘어가기 위한 cancel_buttonnext_button에 각각 onClick 속성을 설정해야 합니다.
  2. fragment_side_menu.xml에서도 동일한 작업을 진행하고 SideMenuFragment에 결합 변수를 추가합니다. 단, 각 라디오 버튼이 선택될 경우에는 ViewModel에 사이드 메뉴를 설정합니다. 소계 텍스트도 업데이트되어야 합니다. 개발자는 cancel 및 next 버튼에도 onClick 속성을 설정해야 합니다.
  3. 같은 작업을 한 번 더 반복하되, fragment_accompaniment_menu.xml에서 이번에는 AccompanimentMenuFragment에 결합 변수를 사용하고 각 라디오 버튼이 선택될 경우에는 디저트를 설정합니다. 이 경우에도 소계 텍스트, cancel 버튼, next 버튼에 속성을 설정해야 합니다.
  4. fragment_checkout.xml에서는 결합 변수를 정의할 수 있도록 <data> 태그를 추가해야 합니다. <data> 태그 내에 각각 OrderViewModelCheckoutFragment를 위한 두 개의 결합 변수를 추가합니다. OrderViewModel의 텍스트 뷰에서 선택한 메인 요리, 사이드 메뉴, 디저트의 이름과 가격을 설정해야 합니다. OrderViewModel에서 소계, 세금, 총액도 설정해야 합니다. 그런 다음 주문이 제출되거나 취소되는 경우를 대비하여 CheckoutFragment에서 적절한 함수를 사용하여 onClickAttributes를 설정합니다.

.

프래그먼트에서 데이터 결합 변수 초기화하기

상응하는 프래그먼트 파일의 메서드 onViewCreated() 내에서 데이터 결합 변수를 초기화합니다.

  1. EntreeMenuFragment
  2. SideMenuFragment
  3. AccompanimentMenuFragment
  4. CheckoutFragment

탐색 그래프 만들기

3단원에서 알아본 것처럼, 탐색 그래프는 활동에 포함된 FragmentContainerView에 호스팅됩니다. activity_main.xml을 열고 TODO를 다음 코드로 바꿔 FragmentContainerView를 선언합니다.

<androidx.fragment.app.FragmentContainerView
   android:id="@+id/nav_host_fragment"
   android:name="androidx.navigation.fragment.NavHostFragment"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   app:defaultNavHost="true"
   app:navGraph="@navigation/mobile_navigation"
   app:layout_constraintBottom_toBottomOf="parent"
   app:layout_constraintLeft_toLeftOf="parent"
   app:layout_constraintRight_toRightOf="parent"
   app:layout_constraintTop_toTopOf="parent" />

탐색 그래프 mobile_navigation.xmlres.navigation 패키지에 있습니다.

e3381215c35c1726.png

앱의 탐색 그래프입니다. 하지만 파일은 현재 비어 있습니다. 해야 할 작업은 탐색 그래프에 대상을 추가하고 화면 간의 다음 탐색을 모델링하는 것입니다.

  1. StartOrderFragment에서 EntreeMenuFragment로 탐색
  2. EntreeMenuFragment에서 SideMenuFragment로 탐색
  3. SideMenuFragment에서 AccompanimentMenuFragment로 탐색
  4. AccompanimentMenuFragment에서 CheckoutFragment로 탐색
  5. CheckoutFragment에서 StartOrderFragment로 탐색
  6. EntreeMenuFragment에서 StartOrderFragment로 탐색
  7. SideMenuFragment에서 StartOrderFragment로 탐색
  8. AccompanimentMenuFragment에서 StartOrderFragment로 탐색
  9. 시작 대상StartOrderFragment여야 합니다.

탐색 그래프를 설정했으면 프래그먼트 클래스에서 탐색을 실행해야 합니다. MainActivity.kt뿐만 아니라 프래그먼트에서도 남은 TODO 주석을 구현합니다.

  1. EntreeMenuFragment, SideMenuFragment, AccompanimentMenuFragmentgoToNextScreen() 메서드의 경우 앱의 다음 화면으로 이동합니다.
  2. EntreeMenuFragment, SideMenuFragment, AccompanimentMenuFragment, CheckoutFragmentcancelOrder() 메서드의 경우 먼저 sharedViewModel에서 resetOrder()를 호출한 다음 StartOrderFragment로 이동합니다.
  3. StartOrderFragment에서 EntreeMenuFragment로 이동하기 위한 setOnClickListener()를 구현합니다.
  4. CheckoutFragment에서 submitOrder() 메서드를 구현합니다. sharedViewModel에서 resetOrder()를 호출한 후 StartOrderFragment로 이동합니다.
  5. 마지막으로 MainActivity.kt에서 navControllerNavHostFragment에서 navController로 설정합니다.

4. 앱 테스트

Lunch Tray 프로젝트에는 여러 테스트 사례, 즉 MenuContentTests, NavigationTests, OrderFunctionalityTests가 있는 'androidTest' 타겟이 포함되어 있습니다.

테스트 실행

테스트를 실행하려면 다음 중 하나를 실행하면 됩니다.

단일 테스트 사례의 경우 테스트 사례 클래스를 열고 클래스 선언 왼쪽의 녹색 화살표를 클릭합니다. 그런 다음 메뉴에서 실행 옵션을 선택하면 됩니다. 그러면 테스트 사례의 테스트가 모두 실행됩니다.

8ddcbafb8ec14f9b.png

예를 들어 실패한 테스트는 하나뿐이고 나머지 테스트는 통과된 경우와 같이 단일 테스트만 실행하려는 때가 많습니다. 전체 테스트 사례와 마찬가지로 단일 테스트를 실행할 수 있습니다. 녹색 화살표를 사용하여 실행 옵션을 선택합니다.

335664b7fc8b4fb5.png

테스트 사례가 여러 개라면 전체 테스트 모음을 실행할 수도 있습니다. 앱을 실행하는 것과 마찬가지로 Run 메뉴에서 이 옵션을 찾을 수 있습니다.

80312efedf6e4dd3.png

Android 스튜디오는 마지막으로 실행한 타겟(앱, 테스트 타겟 등)을 기본값으로 설정하므로 메뉴에 여전히 Run > Run 'app'이라고 표시되면 Run > Run을 선택하여 테스트 타겟을 실행할 수 있습니다.

95aacc8f749dee8e.png

그런 다음 팝업 메뉴에서 테스트 타겟을 선택합니다.

8b702efbd4d21d3d.png

5. 선택사항: Google에 의견 보내기

이 프로젝트에 관한 의견을 기다리고 있습니다. 간단한 설문조사에 참여하여 의견을 들려주세요. 여러분의 의견은 이 과정의 향후 프로젝트에 도움이 됩니다.