Android Kotlin 기초 08.3 인터넷 데이터를 사용한 필터링 및 세부정보 뷰

이 Codelab은 Android Kotlin 기초 교육 과정의 일부입니다. Codelab을 순서대로 진행한다면 이 과정을 통해 최대한의 가치를 얻을 수 있을 것입니다. 모든 과정 Codelab은 Android Kotlin 기초 Codelab 방문 페이지에 나열되어 있습니다.

소개

이 과정의 이전 Codelab에서는 웹 서비스에서 화성 부동산에 관한 데이터를 가져오는 방법과 관련 레이아웃에서 이미지를 로드하고 표시하는 그리드 레이아웃으로 RecyclerView를 만드는 방법을 알아봤습니다. 이 Codelab에서는 화성 속성을 대여할 수 있는지 또는 구매할 수 있는지 여부를 필터링하는 기능을 구현하여 MarsRealEstate 앱을 완성합니다. 또한 사용자가 개요에서 속성 사진을 탭하면 그 속성에 대한 세부정보가 포함된 세부정보 보기가 표시되도록 세부정보 보기를 만들 수 있습니다.

기본 요건

  • 프래그먼트를 만들고 사용하는 방법
  • 프래그먼트 간에 이동하고 Safe Args (Gradle 플러그인)를 사용하여 프래그먼트 간에 데이터를 전달하는 방법
  • 뷰 모델, 뷰 모델 팩토리, 변환, LiveData 등의 아키텍처 구성요소를 사용하는 방법
  • Retrofit 라이브러리와 Moshi 라이브러리를 사용하여 REST 웹 서비스에서 JSON 인코딩 데이터를 검색하고 이 데이터를 Kotlin 객체로 파싱하는 방법

학습할 내용

  • 레이아웃 파일에서 복잡한 결합 표현식을 사용하는 방법
  • 쿼리 옵션을 사용하여 웹 서비스에 Retrofit 요청을 하는 방법

실행할 작업

  • MarsRealEstate 앱을 수정하여 판매 중인 화성 속성을 대여용 동영상 대신 달러 기호 아이콘으로 표시합니다.
  • 개요 페이지의 옵션 메뉴를 사용하여 유형별로 화성 속성을 필터링하는 웹 서비스 요청을 만듭니다.
  • 화성 속성의 세부정보 프래그먼트를 만들고 탐색 기능을 사용하여 이 프래그먼트를 개요 그리드에 연결한 다음 속성 데이터를 이 프래그먼트에 전달합니다.

이 Codelab 및 관련 Codelab에서는 화성 판매의 속성을 표시하는 MarsRealEstate라는 앱을 사용합니다. 이 앱은 인터넷 서버에 연결하여 가격 및 부동산 매매 또는 임대 가능 여부 등의 속성 데이터를 검색하고 표시합니다. 각 속성을 나타내는 이미지는 NASA의 화성 탐사 로봇이 화성에서 촬영한 실제 사진입니다. 이전 Codelab에서는 모든 속성 사진의 그리드 레이아웃으로 RecyclerView를 만들었습니다.

이 버전의 앱에서 다음과 같은 속성 유형 (임대 및 구매)으로 작업하고 그리드 레이아웃에 아이콘을 추가하여 판매 중인 속성을 표시합니다.

대여 또는 판매할 속성만 표시되도록 그리드를 필터링하도록 앱의 옵션 메뉴를 수정합니다.

마지막으로 개별 속성의 세부정보 보기를 만들고 탐색 그리드를 사용하여 개요 그리드의 아이콘을 세부정보 세부정보에 연결합니다.

지금까지는 화성 속성 데이터에서 유일하게 속성 이미지의 URL을 사용했습니다. 그러나 MarsProperty 클래스에서 정의한 속성 데이터에는 ID, 가격, 유형 (대여 또는 판매)도 포함됩니다. 기억을 새로고침하려면 웹 서비스에서 가져온 JSON 데이터의 스니펫입니다.

{
   "price":8000000,
   "id":"424908",
   "type":"rent",
   "img_src": "http://mars.jpl.nasa.gov/msl-raw-images/msss/01000/mcam/1000ML0044631290305226E03_DXXX.jpg"
},

이 작업에서는 화성 속성 유형을 사용하여 판매하는 개요 페이지의 속성에 달러 기호 이미지를 추가합니다.

1단계: 유형을 포함하도록 MarsProperty 업데이트

MarsProperty 클래스는 웹 서비스에서 제공하는 각 속성의 데이터 구조를 정의합니다. 이전 Codelab에서는 Moshi 라이브러리를 사용하여 Mars 웹 서비스의 원시 JSON 응답을 개별 MarsProperty 데이터 객체로 파싱했습니다.

이 단계에서는 속성이 대여용인지 (즉, 유형이 문자열 "rent"인지 "buy"인지) 나타내기 위해 MarsProperty 클래스에 일부 로직을 추가합니다. 이 로직을 둘 이상의 위치에서 사용하므로 이러한 로직을 복제하는 것보다 데이터 클래스에 두는 것이 더 좋습니다.

  1. 이전 Codelab의 MarsRealEstate 앱을 엽니다. 앱이 없는 경우 MarsRealEstateGrid를 다운로드할 수 있습니다.
  2. network/MarsProperty.kt를 엽니다. MarsProperty 클래스 정의에 본문을 추가하고 객체가 "rent" 유형인 경우 true를 반환하는 isRental의 맞춤 getter를 추가합니다.
data class MarsProperty(
       val id: String,
       @Json(name = "img_src") val imgSrcUrl: String,
       val type: String,
       val price: Double)  {
   val isRental
       get() = type == "rent"
}

2단계: 그리드 항목 레이아웃 업데이트하기

이제 이미지 그리드의 항목 레이아웃을 업데이트하여 판매 중인 속성 이미지에만 달러 기호 드로어블을 표시합니다.

데이터 결합 표현식을 사용하면 그리드 항목의 XML 레이아웃에서 이 테스트를 완전히 실행할 수 있습니다.

  1. res/layout/grid_view_item.xml를 엽니다. RecyclerView의 그리드 레이아웃에 있는 각 개별 셀의 레이아웃 파일입니다. 현재 이 파일에는 속성 이미지의 <ImageView> 요소만 포함되어 있습니다.
  2. <data> 요소 내부에서 View 클래스의 <import> 요소를 추가합니다. 레이아웃 파일의 데이터 결합 표현식 내에서 클래스의 구성요소를 사용하려는 경우 가져오기를 사용합니다. 이 경우에는 View.GONEView.VISIBLE 상수를 사용하므로 View 클래스에 액세스해야 합니다.
<import type="android.view.View"/>
  1. 전체 이미지 뷰를 FrameLayout로 둘러싸서 속성 이미지 위에 달러 기호 드로어블을 스택할 수 있습니다.
<FrameLayout
   android:layout_width="match_parent"
   android:layout_height="170dp">
             <ImageView 
                    android:id="@+id/mars_image"
            ...
</FrameLayout>
  1. ImageView의 경우 android:layout_height 속성을 match_parent로 변경하여 새 상위 FrameLayout를 채웁니다.
android:layout_height="match_parent"
  1. FrameLayout 내에서 첫 번째 요소 바로 아래에 두 번째 <ImageView> 요소를 추가합니다. 아래에 나온 정의를 사용하세요. 이 이미지는 그리드 항목의 오른쪽 하단(화성 이미지)에 나타나며 res/drawable/ic_for_sale_outline.xml에 정의된 드로어블을 달러 기호 아이콘에 사용합니다.
<ImageView
   android:id="@+id/mars_property_type"
   android:layout_width="wrap_content"
   android:layout_height="45dp"
   android:layout_gravity="bottom|end"
   android:adjustViewBounds="true"
   android:padding="5dp"
   android:scaleType="fitCenter"
   android:src="@drawable/ic_for_sale_outline"
   tools:src="@drawable/ic_for_sale_outline"/>
  1. android:visibility 속성을 mars_property_type 이미지 뷰에 추가합니다. 결합 표현식을 사용하여 속성 유형을 테스트하고 공개 상태를 View.GONE (대여용) 또는 View.VISIBLE (구매용)에 할당합니다.
 android:visibility="@{property.rental ? View.GONE : View.VISIBLE}"

지금까지 <data> 요소에 정의된 개별 변수를 사용하는 레이아웃에서만 결합 표현식이 발견되었습니다. 결합 표현식은 매우 강력하며 XML 레이아웃 내에서 완전히 테스트 및 수학 계산과 같은 작업을 할 수 있습니다. 이 경우 3항 연산자 (?:)를 사용하여 테스트를 수행합니다 (이 객체는 대여인가요?). true (View.GONE로 달러 기호 아이콘 숨기기) 및 false (View.VISIBLE로 아이콘 표시)에 대한 결과를 제공합니다.

다음은 새로운 전체 grid_view_item.xml 파일입니다.

<layout 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">
   <data>
       <import type="android.view.View"/>
       <variable
           name="property"
           type="com.example.android.marsrealestate.network.MarsProperty" />
   </data>
   <FrameLayout
       android:layout_width="match_parent"
       android:layout_height="170dp">

       <ImageView
           android:id="@+id/mars_image"
           android:layout_width="match_parent"
           android:layout_height="match_parent"
           android:scaleType="centerCrop"
           android:adjustViewBounds="true"
           android:padding="2dp"
           app:imageUrl="@{property.imgSrcUrl}"
           tools:src="@tools:sample/backgrounds/scenic"/>

       <ImageView
           android:id="@+id/mars_property_type"
           android:layout_width="wrap_content"
           android:layout_height="45dp"
           android:layout_gravity="bottom|end"
           android:adjustViewBounds="true"
           android:padding="5dp"
           android:scaleType="fitCenter"
           android:src="@drawable/ic_for_sale_outline"
           android:visibility="@{property.rental ? View.GONE : View.VISIBLE}"
           tools:src="@drawable/ic_for_sale_outline"/>
   </FrameLayout>
</layout>
  1. 앱을 컴파일하고 실행합니다. 대여하지 않은 속성에는 달러 기호 아이콘이 있습니다.

현재 앱은 개요 그리드에 모든 화성 속성을 표시합니다. 사용자가 화성에서 임대 자산을 쇼핑하는 경우 판매 가능한 부동산을 나타내는 아이콘을 사용하는 것이 유용하지만 여전히 스크롤할 수 있는 속성이 많습니다. 이 작업에서는 사용자가 옵션 임대를 개요 프래그먼트에 추가하여 사용자가 대여한 콘텐츠만 표시하거나, 판매 가능한 속성만 표시하거나, 모든 항목을 표시할 수 있도록 합니다.

이 작업을 실행하는 한 가지 방법은 개요 그리드에서 각 MarsProperty의 유형을 테스트하고 일치하는 속성만 표시하는 것입니다. 하지만 실제 Mars 웹 서비스에는 rent 또는 buy 유형의 속성만 가져올 수 있는 쿼리 매개변수 또는 옵션 (filter이라고 함)이 있습니다. 이 필터 쿼리는 브라우저에서 realestate 웹 서비스 URL과 함께 사용할 수 있습니다.

https://android-kotlin-fun-mars-server.appspot.com/realestate?filter=buy

이 작업에서는 MarsApiService 클래스를 수정하여 Retrofit으로 웹 서비스 요청에 쿼리 옵션을 추가합니다. 그런 다음 옵션 메뉴를 연결하여 해당 쿼리 옵션을 사용하여 모든 화성 속성 데이터를 다시 다운로드합니다. 웹 서비스의 응답에는 관심 있는 속성만 포함되어 있기 때문에 개요 그리드의 뷰 표시 로직을 아예 변경할 필요가 없습니다.

1단계: Mars API 서비스 업데이트

요청을 변경하려면 이 시리즈의 첫 번째 Codelab에서 구현한 MarsApiService 클래스를 다시 검토해야 합니다. 필터링 API를 제공하도록 클래스를 수정합니다.

  1. network/MarsApiService.kt를 엽니다. 가져오기 바로 아래에 MarsApiFilter라는 enum을 만들어 웹 서비스에서 예상하는 쿼리 값과 일치하는 상수를 정의합니다.
enum class MarsApiFilter(val value: String) {
   SHOW_RENT("rent"),
   SHOW_BUY("buy"),
   SHOW_ALL("all") }
  1. 필터 쿼리의 문자열 입력을 취하도록 getProperties() 메서드를 수정하고 아래와 같이 입력에 @Query("filter") 주석을 답니다.
    메시지가 표시되면
    retrofit2.http.Query을 가져옵니다.

    @Query 주석은 필터 옵션을 사용하여 웹 서비스 요청을 생성하도록 getProperties() 메서드 (및 Retrofit)에 지시합니다. getProperties()가 호출될 때마다 요청 URL에 ?filter=type 부분이 포함됩니다. 이 부분은 웹 쿼리와 일치하는 결과로 응답하도록 웹 서비스에 지시합니다.
fun getProperties(@Query("filter") type: String):  

2단계: 개요 뷰 모델 업데이트하기

OverviewViewModelgetMarsRealEstateProperties() 메서드에 있는 MarsApiService에서 데이터를 요청합니다. 이제 필터 인수를 취하도록 요청을 업데이트해야 합니다.

  1. overview/OverviewViewModel.kt를 엽니다. 이전 단계에서 변경한 사항으로 인해 Android 스튜디오에서 오류가 표시됩니다. MarsApiFilter (가능한 필터 값의 열거형)을 getMarsRealEstateProperties() 호출에 매개변수로 추가합니다.

    요청을 받으면 com.example.android.marsrealestate.network.MarsApiFilter을 가져옵니다.
private fun getMarsRealEstateProperties(filter: MarsApiFilter) {
  1. Retrofit 서비스에서 getProperties() 호출을 수정하여 필터 쿼리를 문자열로 전달합니다.
var getPropertiesDeferred = MarsApi.retrofitService.getProperties(filter.value)
  1. init {} 블록에서 MarsApiFilter.SHOW_ALLgetMarsRealEstateProperties()의 인수로 전달하여 앱이 처음 로드될 때 모든 속성을 표시합니다.
init {
   getMarsRealEstateProperties(MarsApiFilter.SHOW_ALL)
}
  1. 클래스 끝에 MarsApiFilter 인수를 사용하고 이 인수로 getMarsRealEstateProperties()를 호출하는 updateFilter() 메서드를 추가합니다.
fun updateFilter(filter: MarsApiFilter) {
   getMarsRealEstateProperties(filter)
}

3단계: 프래그먼트를 옵션 메뉴에 연결하기

마지막 단계는 사용자가 메뉴 옵션을 선택할 때 뷰 모델에서 updateFilter()를 호출하기 위해 더보기 메뉴를 프래그먼트에 연결하는 것입니다.

  1. res/menu/overflow_menu.xml를 엽니다. MarsRealEstate 앱에는 세 가지 옵션(모든 속성 표시, 대여만 표시, 판매 매물만 표시)을 제공하는 기존 더보기 메뉴가 있습니다.
<menu xmlns:android="http://schemas.android.com/apk/res/android">
   <item
       android:id="@+id/show_all_menu"
       android:title="@string/show_all" />
   <item
       android:id="@+id/show_rent_menu"
       android:title="@string/show_rent" />
   <item
       android:id="@+id/show_buy_menu"
       android:title="@string/show_buy" />
</menu>
  1. overview/OverviewFragment.kt를 엽니다. 클래스가 끝날 때 사용하는 onOptionsItemSelected() 메서드를 구현하여 메뉴 항목 선택을 처리합니다.
override fun onOptionsItemSelected(item: MenuItem): Boolean {
} 
  1. onOptionsItemSelected()에서 적절한 필터로 뷰 모델에서 updateFilter() 메서드를 호출합니다. Kotlin when {} 블록을 사용하여 옵션 간에 전환하세요. 기본 필터 값에는 MarsApiFilter.SHOW_ALL를 사용합니다. 메뉴 항목을 처리했으므로 true를 반환합니다. 요청이 있는 경우 MarsApiFilter (com.example.android.marsrealestate.network.MarsApiFilter)를 가져옵니다. 전체 onOptionsItemSelected() 메서드는 다음과 같습니다.
override fun onOptionsItemSelected(item: MenuItem): Boolean {
   viewModel.updateFilter(
           when (item.itemId) {
               R.id.show_rent_menu -> MarsApiFilter.SHOW_RENT
               R.id.show_buy_menu -> MarsApiFilter.SHOW_BUY
               else -> MarsApiFilter.SHOW_ALL
           }
   )
   return true
}
  1. 앱을 컴파일하고 실행합니다. 앱은 모든 속성 유형과 달러 아이콘으로 표시된 판매 속성의 첫 번째 개요 그리드를 실행합니다.
  2. 옵션 메뉴에서 대여를 선택합니다. 속성이 새로고침되고 어떠한 속성도 달러 아이콘과 함께 표시되지 않습니다. (숙박 시설만 표시됨) 디스플레이가 새로고침된 후 필터링된 속성만 표시되려면 잠시 기다려야 할 수 있습니다.
  3. 옵션 메뉴에서 구매를 선택합니다. 속성이 다시 새로고침되고 모든 속성이 달러 아이콘과 함께 표시됩니다. 판매 판매 속성만 표시됩니다.

이제 화성 속성의 스크롤 그리드가 생겼지만 더 자세한 내용을 확인할 차례입니다. 이 작업에서는 특정 속성의 세부정보를 표시하는 세부정보 프래그먼트를 추가합니다. 세부정보 프래그먼트에 더 큰 이미지, 가격, 숙박 시설(대여 또는 판매)이 표시됩니다.

이 프래그먼트는 사용자가 개요 그리드에서 이미지를 탭하면 실행됩니다. 이렇게 하려면 onClick 리스너를 RecyclerView 그리드 항목에 추가한 다음 새 프래그먼트로 이동해야 합니다. 이 과정에서는 LiveData ViewModel 변경을 트리거하여 탐색했습니다. 탐색 구성요소의 Safe Args 플러그인을 사용하여 선택된 MarsProperty 정보를 개요 프래그먼트에서 세부정보 프래그먼트로 전달합니다.

1단계: 세부정보 뷰 모델 만들기 및 세부정보 레이아웃 업데이트하기

개요 뷰 모델 및 프래그먼트에 사용한 프로세스와 마찬가지로, 이제 세부정보 프래그먼트의 뷰 모델과 레이아웃 파일을 구현해야 합니다.

  1. detail/DetailViewModel.kt를 엽니다. 네트워크 관련 Kotlin 파일이 network 폴더에 포함되고 overview의 개요 파일에 포함된 것처럼 detail 폴더에는 세부정보 보기와 연결된 파일이 있습니다. DetailViewModel 클래스 (현재 비어 있음)는 생성자에서 marsProperty를 매개변수로 사용합니다.
class DetailViewModel( marsProperty: MarsProperty,
                     app: Application) : AndroidViewModel(app) {
}
  1. 클래스 정의 내에서 선택한 화성 속성의 LiveData를 추가하여 해당 정보를 세부정보 뷰에 노출합니다. MutableLiveData를 만드는 일반적인 패턴을 따라 MarsProperty 자체를 보유하여 변경 불가능한 공개 LiveData 속성을 노출합니다.

    요청을 받으면 androidx.lifecycle.LiveData을 가져오고 androidx.lifecycle.MutableLiveData을 가져옵니다.
private val _selectedProperty = MutableLiveData<MarsProperty>()
val selectedProperty: LiveData<MarsProperty>
   get() = _selectedProperty
  1. init {} 블록을 만들고 생성자의 MarsProperty 객체로 선택한 화성 속성의 값을 설정합니다.
    init {
        _selectedProperty.value = marsProperty
    }
  1. res/layout/fragment_detail.xml를 열고 디자인 뷰에서 확인합니다.

    세부정보 프래그먼트의 레이아웃 파일입니다. 이 속성은 대형 사진의 ImageView, 숙박 시설 유형 (대여 또는 판매)의 TextView, 가격의 TextView를 포함합니다. 제약 조건 레이아웃은 ScrollView로 래핑되므로 뷰가 디스플레이에 비해 너무 커지면(예: 사용자가 가로 모드에서 보는 경우) 자동으로 스크롤됩니다.
  2. 레이아웃의 Text 탭으로 이동합니다. 레이아웃 상단에서 <ScrollView> 요소 바로 앞에 <data> 요소를 추가하여 세부정보 뷰 모델을 레이아웃과 연결합니다.
<data>
   <variable
       name="viewModel"
       type="com.example.android.marsrealestate.detail.DetailViewModel" />
</data>
  1. ImageView 요소에 app:imageUrl 속성을 추가합니다. 뷰 모델의 선택된 속성에서 imgSrcUrl로 설정합니다.

    Glide를 사용하여 이미지를 로드하는 결합 어댑터도 여기에서 자동으로 사용됩니다. 이 어댑터는 모든 app:imageUrl 속성을 확인하기 때문입니다.
 app:imageUrl="@{viewModel.selectedProperty.imgSrcUrl}"

2단계: 개요 뷰 모델에서 탐색 정의

사용자가 개요 모델에서 사진을 탭하면 클릭한 항목의 세부정보를 보여주는 프래그먼트로 이동이 트리거됩니다.

  1. overview/OverviewViewModel.kt를 엽니다. _navigateToSelectedProperty MutableLiveData 속성을 추가하고 변경 불가능한 LiveData로 노출합니다.

    LiveData이 null이 아닌 값으로 변경되면 탐색이 트리거됩니다. (곧 이 변수를 관찰하고 탐색을 트리거하는 코드를 추가합니다.)
private val _navigateToSelectedProperty = MutableLiveData<MarsProperty>()
val navigateToSelectedProperty: LiveData<MarsProperty>
   get() = _navigateToSelectedProperty
  1. 클래스 끝에 _navigateToSelectedProperty을 선택한 화성 속성으로 설정하는 displayPropertyDetails() 메서드를 추가합니다.
fun displayPropertyDetails(marsProperty: MarsProperty) {
   _navigateToSelectedProperty.value = marsProperty
}
  1. _navigateToSelectedProperty의 값을 null로 지정하는 displayPropertyDetailsComplete() 메서드를 추가합니다. 이 상태는 탐색 상태를 완료로 표시하고 사용자가 세부정보 뷰에서 돌아올 때 탐색이 다시 트리거되지 않도록 하는 데 필요합니다.
fun displayPropertyDetailsComplete() {
   _navigateToSelectedProperty.value = null
}

3단계: 그리드 어댑터와 프래그먼트에서 클릭 리스너 설정하기

  1. overview/PhotoGridAdapter.kt를 엽니다. 클래스 끝에서 marsProperty 매개변수를 사용하여 람다를 사용하는 맞춤 OnClickListener 클래스를 만듭니다. 클래스 내에서 람다 매개변수로 설정된 onClick() 함수를 정의합니다.
class OnClickListener(val clickListener: (marsProperty:MarsProperty) -> Unit) {
     fun onClick(marsProperty:MarsProperty) = clickListener(marsProperty)
}
  1. PhotoGridAdapter의 클래스 정의로 스크롤하고 비공개 OnClickListener 속성을 생성자에 추가합니다.
class PhotoGridAdapter( private val onClickListener: OnClickListener ) :
       ListAdapter<MarsProperty,              
           PhotoGridAdapter.MarsPropertyViewHolder>(DiffCallback) {
  1. onBindviewHolder() 메서드의 그리드 항목에 onClickListener를 추가하여 사진을 클릭 가능하도록 설정합니다. getItem() and bind() 호출 간에 클릭 리스너를 정의합니다.
override fun onBindViewHolder(holder: MarsPropertyViewHolder, position: Int) {
   val marsProperty = getItem(position)
   holder.itemView.setOnClickListener {
       onClickListener.onClick(marsProperty)
   }
   holder.bind(marsProperty)
}
  1. overview/OverviewFragment.kt를 엽니다. onCreateView() 메서드에서 binding.photosGrid.adapter 속성을 초기화하는 줄을 아래에 나온 줄로 바꿉니다.

    이 코드는 PhotoGridAdapter.onClickListener 객체를 PhotoGridAdapter 생성자에 추가하고 전달된 MarsProperty 객체로 viewModel.displayPropertyDetails()를 호출합니다. 그러면 탐색의 뷰 모델에서 LiveData이 트리거됩니다.
binding.photosGrid.adapter = PhotoGridAdapter(PhotoGridAdapter.OnClickListener {
   viewModel.displayPropertyDetails(it)
})

4단계: 탐색 그래프 수정 및 MarsProperty parcelable 만들기

사용자가 개요 그리드에서 사진을 탭하면 앱은 세부정보 프래그먼트로 이동하고 선택한 화성 속성의 세부정보를 통과해야 합니다. 그래야 세부정보 뷰에서 해당 정보를 표시할 수 있습니다.

이제 PhotoGridAdapter의 클릭 리스너와 탭 처리 뷰, 그리고 뷰 모델에서 탐색을 트리거하는 방법이 있습니다. 하지만 세부정보 프래그먼트에 전달되는 MarsProperty 객체는 아직 없습니다. 이를 위해 탐색 구성요소의 Safe Args를 사용합니다.

  1. res/navigation/nav_graph.xml를 엽니다. Text 탭을 클릭하면 탐색 그래프의 XML 코드를 볼 수 있습니다.
  2. 세부정보 프래그먼트의 <fragment> 요소 내부에 다음과 같이 <argument> 요소를 추가합니다. selectedProperty라는 인수의 유형은 MarsProperty입니다.
<argument
   android:name="selectedProperty"
   app:argType="com.example.android.marsrealestate.network.MarsProperty"
   />
  1. 앱을 컴파일합니다. MarsPropertyparcelable가 아니기 때문에 탐색에서 오류가 발생합니다. Parcelable 인터페이스를 사용하면 객체를 직렬화할 수 있으므로 객체 간에 데이터를 전달할 수 있습니다. 이 경우 MarsProperty 객체 내의 데이터가 Safe Args를 통해 세부정보 프래그먼트로 전달되도록 하려면 MarsPropertyParcelable 인터페이스를 구현해야 합니다. 다행히 Kotlin에서는 해당 인터페이스를 쉽게 구현할 수 있습니다.
  2. network/MarsProperty.kt를 엽니다. 클래스 정의에 @Parcelize 주석을 추가합니다.

    요청이 있으면 kotlinx.android.parcel.Parcelize를 가져옵니다.

    @Parcelize 주석은 Kotlin Android 확장 프로그램을 사용하여 이 클래스의 Parcelable 인터페이스에 메서드를 자동으로 구현합니다. 별도로 조치를 취할 필요는 없습니다.
@Parcelize
data class MarsProperty (
  1. MarsProperty의 클래스 정의를 변경하여 Parcelable를 확장합니다.

    요청이 있을 때 android.os.Parcelable을 가져옵니다.

    이제 MarsProperty 클래스 정의는 다음과 같습니다.
@Parcelize
data class MarsProperty (
       val id: String,
       @Json(name = "img_src") val imgSrcUrl: String,
       val type: String,
       val price: Double) : Parcelable {

5단계: 프래그먼트 연결하기

여전히 탐색하지 않고 실제 탐색은 프래그먼트에서 발생합니다. 이 단계에서는 개요 및 세부정보 프래그먼트 간의 탐색을 구현하기 위한 마지막 비트를 추가합니다.

  1. overview/OverviewFragment.kt를 엽니다. onCreateView()에서 사진 그리드 어댑터를 초기화하는 줄 아래에 아래 줄을 추가하여 개요 뷰 모델에서 navigatedToSelectedProperty를 관찰합니다.

    요청을 받으면 androidx.lifecycle.Observer을 가져오고 androidx.navigation.fragment.findNavController을 가져옵니다.

    관찰자는 MarsProperty(람다의 it)가 null이 아닌지 테스트하고 null이면 findNavController()가 있는 프래그먼트에서 탐색 컨트롤러를 가져옵니다. displayPropertyDetailsComplete()를 호출하여 뷰 모델에 LiveData를 null 상태로 재설정하도록 지시합니다. 그러면 앱이 OverviewFragment로 다시 돌아갈 때 실수로 탐색을 다시 트리거하지 않습니다.
viewModel.navigateToSelectedProperty.observe(this, Observer {
   if ( null != it ) {   
      this.findNavController().navigate(
              OverviewFragmentDirections.actionShowDetail(it))             
      viewModel.displayPropertyDetailsComplete()
   }
})
  1. detail/DetailFragment.kt를 엽니다. onCreateView() 메서드의 setLifecycleOwner() 호출 바로 아래에 다음 줄을 추가합니다. 이 행은 Safe Args에서 선택된 MarsProperty 객체를 가져옵니다.

    Kotlin's null이 아닌 어설션 연산자 (!!)를 사용하는 것을 확인할 수 있습니다. selectedProperty이 없으면 악몽이 발생했으며 실제로 코드에서 null 포인터를 반환하기를 원합니다. 프로덕션 코드에서는 어떤 식으로든 오류를 처리해야 합니다.
 val marsProperty = DetailFragmentArgs.fromBundle(arguments!!).selectedProperty
  1. 다음 줄을 추가하여 새 DetailViewModelFactory를 만듭니다. DetailViewModelFactory를 사용하여 DetailViewModel의 인스턴스를 가져옵니다. 시작 앱에는 DetailViewModelFactory 구현이 포함되어 있으므로 여기서 이를 초기화하기만 하면 됩니다.
val viewModelFactory = DetailViewModelFactory(marsProperty, application)
  1. 마지막으로 이 줄을 추가하여 팩토리에서 DetailViewModel를 가져오고 모든 부분을 연결합니다.
      binding.viewModel = ViewModelProviders.of(
                this, viewModelFactory).get(DetailViewModel::class.java)
  1. 앱을 컴파일하고 실행한 다음 화성 속성 사진을 탭합니다. 속성의 세부정보에 관한 세부정보 프래그먼트가 표시됩니다. 뒤로 버튼을 탭하여 개요 페이지로 돌아가면 세부정보 화면이 여전히 희박한 것을 볼 수 있습니다. 다음 작업에서 세부정보 데이터를 속성 페이지에 추가하는 작업을 완료합니다.

현재 세부정보 페이지에는 개요 페이지에서 볼 수 있었던 화성 사진만 표시됩니다. MarsProperty 클래스에는 속성 유형 (대여 또는 구매)과 속성 가격도 있습니다. 세부정보 화면에는 이러한 값이 모두 포함되어야 하며, 대여 속성에 가격이 월 월별 가격이라고 표시되어 있는 경우 유용합니다. 뷰 모델에서 LiveData 변환을 사용하여 이 두 가지를 모두 구현합니다.

  1. res/values/strings.xml를 엽니다. 시작 코드에는 세부정보 뷰를 위한 문자열을 빌드하는 데 도움이 되는 다음과 같은 문자열 리소스가 포함되어 있습니다. 가격에는 속성 유형에 따라 display_price_monthly_rental 리소스 또는 display_price 리소스를 사용합니다.
<string name="type_rent">Rent</string>
<string name="type_sale">Sale</string>
<string name="display_type">For %s</string>
<string name="display_price_monthly_rental">$%,.0f/month</string>
<string name="display_price">$%,.0f</string>
  1. detail/DetailViewModel.kt를 엽니다. 클래스 하단에 아래에 표시된 코드를 추가합니다.

    요청이 있는 경우 androidx.lifecycle.Transformations을 가져옵니다.

    이 변환에서는 첫 번째 작업과 동일한 테스트를 사용하여 선택한 속성이 대여 항목인지 테스트합니다. 속성이 대여 항목인 경우 변환은 Kotlin when {} 스위치가 있는 리소스에서 적절한 문자열을 선택합니다. 두 문자열 끝에는 숫자가 필요하므로 나중에 property.price를 연결합니다.
val displayPropertyPrice = Transformations.map(selectedProperty) {
   app.applicationContext.getString(
           when (it.isRental) {
               true -> R.string.display_price_monthly_rental
               false -> R.string.display_price
           }, it.price)
}
  1. 생성된 R 클래스를 가져와 프로젝트의 문자열 리소스에 대한 액세스 권한을 얻습니다.
import com.example.android.marsrealestate.R
  1. displayPropertyPrice 변환 뒤에 아래와 같은 코드를 추가합니다. 이 변환은 속성 유형이 대여인지 여부에 따라 여러 문자열 리소스를 연결합니다.
val displayPropertyType = Transformations.map(selectedProperty) {
   app.applicationContext.getString(R.string.display_type,
           app.applicationContext.getString(
                   when (it.isRental) {
                       true -> R.string.type_rent
                       false -> R.string.type_sale
                   }))
}
  1. res/layout/fragment_detail.xml를 엽니다. 해야 할 작업이 한 가지 더 있습니다. 바로 LiveData 변환으로 만든 새 문자열을 세부정보 뷰에 결합하는 것입니다. 이렇게 하려면 속성 유형 텍스트의 텍스트 필드 값을 viewModel.displayPropertyType로, 가격 값 텍스트의 텍스트 필드를 viewModel.displayPropertyPrice로 설정합니다.
<TextView
   android:id="@+id/property_type_text"
...
android:text="@{viewModel.displayPropertyType}"
...
   tools:text="To Rent" />

<TextView
   android:id="@+id/price_value_text"
...
android:text="@{viewModel.displayPropertyPrice}"
...
   tools:text="$100,000" />
  1. 앱을 컴파일하고 실행합니다. 이제 모든 속성 데이터가 세부정보 페이지에 보기 좋게 표시됩니다.

Android 스튜디오 프로젝트: MarsRealEstateFinal

결합 표현식

  • XML 레이아웃 파일에서 결합 표현식을 사용하여 결합 데이터에 관해 수학이나 조건부 테스트 같은 간단한 프로그래매틱 작업을 실행합니다.
  • 레이아웃 파일 내의 클래스를 참조하려면 <data> 태그 내의 <import> 태그를 사용합니다.

웹 서비스 쿼리 옵션

  • 웹 서비스에 대한 요청에는 선택적 매개변수를 포함할 수 있습니다.
  • 요청에서 쿼리 매개변수를 지정하려면 Retrofit에서 @Query 주석을 사용하세요.

Udacity 과정:

Android 개발자 문서:

기타:

이 섹션에는 강사가 진행하는 과정의 일부로 이 Codelab을 통해 작업하는 학생들의 숙제 과제가 나와 있습니다. 강사는 다음을 처리합니다.

  • 필요한 경우 과제를 할당합니다.
  • 학생에게 과제 과제를 제출하는 방법을 알려주세요.
  • 과제 과제를 채점합니다.

강사는 이러한 추천을 원하는 만큼 사용할 수 있으며 다른 적절한 숙제를 할당해도 좋습니다.

이 Codelab을 직접 학습하고 있다면 언제든지 숙제를 통해 지식을 확인해 보세요.

답변하기

질문 1

XML 레이아웃 파일의 <import> 태그는 어떤 기능을 하나요?

▢ 하나의 레이아웃 파일을 다른 파일에 포함합니다.

▢ 레이아웃 파일 내에 Kotlin 코드를 삽입합니다.

▢ 데이터 결합 속성에 대한 액세스를 제공합니다.

▢ '결합 표현식'에서 클래스 및 클래스 멤버를 참조할 수 있음

질문 2

Retrofit에서 REST 웹 서비스 호출에 쿼리 옵션을 추가하려면 어떻게 해야 하나요?

▢ 요청 URL 끝에 쿼리를 추가합니다.

▢ 쿼리를 실행하는 함수에 쿼리의 매개변수를 추가하고 @Query 주석을 추가합니다.

Query 클래스를 사용하여 요청을 작성합니다.

▢ Retrofit 빌더에서 addQuery() 메서드를 사용합니다.

다음 강의: 9.1: 저장소

이 과정의 다른 Codelab 링크는 Android Kotlin 기초 Codelab 방문 페이지를 참고하세요.