Android Kotlin Fundamentals 08.3 การกรองและมุมมองรายละเอียดด้วยข้อมูลอินเทอร์เน็ต

Codelab นี้เป็นส่วนหนึ่งของหลักสูตรหลักพื้นฐานของ Android Kotlin คุณจะได้รับประโยชน์สูงสุดจากหลักสูตรนี้หากทำตาม Codelab ตามลำดับ Codelab ของหลักสูตรทั้งหมดแสดงอยู่ในหน้า Landing Page ของ Codelab หลักพื้นฐานของ Android Kotlin

บทนำ

ใน Codelab ก่อนหน้าของบทเรียนนี้ คุณได้เรียนรู้วิธีรับข้อมูลเกี่ยวกับอสังหาริมทรัพย์บนดาวอังคารจากบริการบนเว็บ และวิธีสร้าง RecyclerView ที่มีเลย์เอาต์ตารางกริดเพื่อโหลดและแสดงรูปภาพจากข้อมูลดังกล่าว ในโค้ดแล็บนี้ คุณจะสร้างแอป MarsRealEstate ให้เสร็จสมบูรณ์โดยการใช้ความสามารถในการกรองอสังหาริมทรัพย์ในดาวอังคารตามสถานะว่าพร้อมให้เช่าหรือซื้อ นอกจากนี้ คุณยังสร้างมุมมองแบบละเอียดเพื่อให้ผู้ใช้เห็นมุมมองแบบละเอียดพร้อมรายละเอียดเกี่ยวกับที่พักนั้นหากแตะรูปภาพที่พักในภาพรวม

สิ่งที่คุณควรทราบอยู่แล้ว

  • วิธีสร้างและใช้ Fragment
  • วิธีไปยังส่วนต่างๆ ระหว่าง Fragment และใช้ Safe Args (ปลั๊กอิน Gradle) เพื่อส่งข้อมูลระหว่าง Fragment
  • วิธีใช้คอมโพเนนต์สถาปัตยกรรม รวมถึงโมเดลมุมมอง โรงงานโมเดลมุมมอง การแปลง และ LiveData
  • วิธีดึงข้อมูลที่เข้ารหัส JSON จากเว็บเซอร์วิส REST และแยกวิเคราะห์ข้อมูลนั้นเป็นออบเจ็กต์ Kotlin ด้วยไลบรารี Retrofit และ Moshi

สิ่งที่คุณจะได้เรียนรู้

  • วิธีใช้การแสดงผลการเชื่อมโยงที่ซับซ้อนในไฟล์เลย์เอาต์
  • วิธีส่งคำขอ Retrofit ไปยังบริการเว็บด้วยตัวเลือกการค้นหา

สิ่งที่คุณต้องทำ

  • แก้ไขแอป MarsRealEstate เพื่อทำเครื่องหมายที่พักในดาวอังคารที่ขาย (เทียบกับที่พักที่ให้เช่า) ด้วยไอคอนดอลลาร์
  • ใช้เมนูตัวเลือกในหน้าภาพรวมเพื่อสร้างคำขอบริการเว็บที่กรองพร็อพเพอร์ตี้ของ Mars ตามประเภท
  • สร้างส่วนย่อยรายละเอียดสำหรับพร็อพเพอร์ตี้ Mars เชื่อมต่อส่วนย่อยนั้นกับตารางภาพรวมด้วยการนำทาง และส่งข้อมูลพร็อพเพอร์ตี้ไปยังส่วนย่อยนั้น

ในโค้ดแล็บนี้ (และโค้ดแล็บที่เกี่ยวข้อง) คุณจะได้ทำงานกับแอปชื่อ MarsRealEstate ซึ่งแสดงอสังหาริมทรัพย์ที่ขายบนดาวอังคาร แอปนี้เชื่อมต่อกับเซิร์ฟเวอร์อินเทอร์เน็ตเพื่อดึงและแสดงข้อมูลพร็อพเพอร์ตี้ รวมถึงรายละเอียดต่างๆ เช่น ราคา และสถานะของพร็อพเพอร์ตี้ว่าพร้อมขายหรือให้เช่าหรือไม่ รูปภาพที่แสดงถึงที่พักแต่ละแห่งเป็นรูปภาพจากดาวอังคารที่ถ่ายจากยานสำรวจดาวอังคารของ NASA ใน Codelab ก่อนหน้านี้ คุณได้สร้าง RecyclerView ที่มีเลย์เอาต์ตารางกริดสำหรับรูปภาพที่พักทั้งหมดแล้ว

ในแอปเวอร์ชันนี้ คุณจะทำงานกับประเภทของพร็อพเพอร์ตี้ (เช่าเทียบกับซื้อ) และเพิ่มไอคอนลงในเลย์เอาต์ตารางกริดเพื่อทำเครื่องหมายพร็อพเพอร์ตี้ที่ขาย

คุณแก้ไขเมนูตัวเลือกของแอปเพื่อกรองตารางให้แสดงเฉพาะที่พักที่ให้เช่าหรือขายได้โดยทำดังนี้

และสุดท้าย คุณสร้างมุมมองรายละเอียดสำหรับพร็อพเพอร์ตี้แต่ละรายการ และเชื่อมต่อไอคอนในตารางภาพรวมกับรายละเอียดดังกล่าวด้วยการนําทาง

จนถึงตอนนี้ ส่วนเดียวของข้อมูลที่พักของ Mars ที่คุณใช้คือ URL ของรูปภาพที่พัก แต่ข้อมูลพร็อพเพอร์ตี้ที่คุณกําหนดในคลาส MarsProperty ยังมีรหัส ราคา และประเภท (เช่าหรือขาย) ด้วย หากต้องการทบทวนความจำ นี่คือข้อมูล 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 เพื่อแยกวิเคราะห์การตอบกลับ JSON ดิบจากเว็บเซอร์วิสของ Mars เป็นออบเจ็กต์ข้อมูล MarsProperty แต่ละรายการ

ในขั้นตอนนี้ คุณจะเพิ่มตรรกะบางอย่างลงในคลาส MarsProperty เพื่อระบุว่าพร็อพเพอร์ตี้มีไว้ให้เช่าหรือไม่ (กล่าวคือ ประเภทเป็นสตริง "rent" หรือ "buy") คุณจะใช้ตรรกะนี้ในหลายที่ ดังนั้นจึงควรมีตรรกะนี้ในคลาสข้อมูลมากกว่าที่จะทำซ้ำ

  1. เปิดแอป MarsRealEstate จากโค้ดแล็บล่าสุด (คุณดาวน์โหลด MarsRealEstateGrid ได้หากยังไม่มีแอป)
  2. เปิด network/MarsProperty.kt เพิ่มเนื้อหาลงในคำจำกัดความของคลาส MarsProperty และเพิ่มตัวรับที่กำหนดเองสำหรับ isRental ที่แสดงผล true หากออบเจ็กต์เป็นประเภท "rent"
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: อัปเดตเลย์เอาต์ของรายการในตารางกริด

ตอนนี้คุณอัปเดตเลย์เอาต์ของรายการสำหรับตารางกริดของรูปภาพเพื่อแสดง Drawable เครื่องหมายดอลลาร์เฉพาะในรูปภาพของที่พักที่ขายเท่านั้นได้แล้ว

ด้วยนิพจน์การเชื่อมโยงข้อมูล คุณจะทำการทดสอบนี้ในเลย์เอาต์ XML สำหรับรายการกริดได้ทั้งหมด

  1. เปิด res/layout/grid_view_item.xml นี่คือไฟล์เลย์เอาต์สำหรับแต่ละเซลล์ในเลย์เอาต์ตารางกริดสำหรับ RecyclerView ปัจจุบันไฟล์มีเฉพาะองค์ประกอบ <ImageView> สำหรับรูปภาพพร็อพเพอร์ตี้
  2. เพิ่มองค์ประกอบ <import> สำหรับคลาส View ในองค์ประกอบ <data> คุณใช้การนำเข้าเมื่อต้องการใช้คอมโพเนนต์ของคลาสภายในนิพจน์การเชื่อมโยงข้อมูลในไฟล์เลย์เอาต์ ในกรณีนี้ คุณจะใช้ค่าคงที่ View.GONE และ View.VISIBLE ดังนั้นคุณจึงต้องมีสิทธิ์เข้าถึงคลาส View
<import type="android.view.View"/>
  1. ล้อมรอบมุมมองรูปภาพทั้งหมดด้วย FrameLayout เพื่อให้วาง Drawable เครื่องหมายดอลลาร์ซ้อนทับบนรูปภาพพร็อพเพอร์ตี้ได้
<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. เพิ่มองค์ประกอบ <ImageView> ที่ 2 ไว้ใต้องค์ประกอบแรกภายใน FrameLayout ใช้คำจำกัดความที่แสดงด้านล่าง รูปภาพนี้จะปรากฏที่มุมขวาล่างของรายการตารางกริด เหนือรูปภาพดาวอังคาร และใช้ Drawable ที่กำหนดไว้ใน 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 นิพจน์ (?:) เพื่อทำการทดสอบ (ออบเจ็กต์นี้เป็นรายการเช่าใช่ไหม) คุณระบุผลลัพธ์หนึ่งสำหรับค่าจริง (ซ่อนไอคอนเครื่องหมายดอลลาร์ด้วย View.GONE) และอีกผลลัพธ์หนึ่งสำหรับค่าเท็จ (แสดงไอคอนดังกล่าวด้วย 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. คอมไพล์และเรียกใช้แอป แล้วสังเกตว่าที่พักที่ไม่ใช่ที่พักให้เช่าจะมีไอคอนเครื่องหมายดอลลาร์

ปัจจุบันแอปของคุณแสดงพร็อพเพอร์ตี้ของ Mars ทั้งหมดในตารางภาพรวม หากผู้ใช้กำลังเลือกดูที่พักให้เช่าบนดาวอังคาร การมีไอคอนเพื่อระบุว่าที่พักใดที่พร้อมจำหน่ายจะเป็นประโยชน์ แต่ก็ยังมีที่พักอีกจำนวนมากที่ต้องเลื่อนดูในหน้า ในงานนี้ คุณจะเพิ่มเมนูตัวเลือกไปยังภาพรวมของ Fragment ซึ่งช่วยให้ผู้ใช้แสดงเฉพาะภาพยนตร์/รายการทีวีที่เช่า แสดงเฉพาะภาพยนตร์/รายการทีวีที่ขาย หรือแสดงทั้งหมดได้

วิธีหนึ่งในการทํางานนี้คือการทดสอบประเภทสําหรับแต่ละ MarsProperty ในตารางกริดภาพรวม และแสดงเฉพาะพร็อพเพอร์ตี้ที่ตรงกัน อย่างไรก็ตาม เว็บบริการของดาวอังคารมีพารามิเตอร์หรือตัวเลือกการค้นหา (เรียกว่า filter) ที่ช่วยให้คุณรับเฉพาะพร็อพเพอร์ตี้ประเภท rent หรือประเภท buy ได้ คุณใช้การค้นหาตัวกรองนี้กับ URL ของบริการเว็บ realestate ในเบราว์เซอร์ได้ดังนี้

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

ในงานนี้ คุณจะแก้ไขคลาส MarsApiService เพื่อเพิ่มตัวเลือกการค้นหาลงในคำขอของบริการเว็บด้วย Retrofit จากนั้นเชื่อมต่อเมนูตัวเลือกเพื่อดาวน์โหลดข้อมูลพร็อพเพอร์ตี้ทั้งหมดของดาวอังคารอีกครั้งโดยใช้ตัวเลือกการค้นหานั้น เนื่องจากการตอบกลับที่คุณได้รับจากบริการเว็บมีเฉพาะพร็อพเพอร์ตี้ที่คุณสนใจ คุณจึงไม่จำเป็นต้องเปลี่ยนตรรกะการแสดงผลของมุมมองสำหรับตารางภาพรวมเลย

ขั้นตอนที่ 1: อัปเดตบริการ Mars API

หากต้องการเปลี่ยนคำขอ คุณต้องกลับไปที่คลาส MarsApiService ที่คุณใช้ใน Codelab แรกในชุดนี้ คุณแก้ไขคลาสเพื่อให้ API การกรอง

  1. เปิด network/MarsApiService.kt สร้าง enum ชื่อ MarsApiFilter ใต้การนำเข้าเพื่อกำหนดค่าคงที่ที่ตรงกับค่าการค้นหาที่เว็บเซอร์วิสคาดหวัง
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: อัปเดตโมเดลมุมมองภาพรวม

คุณขอข้อมูลจาก MarsApiService ในเมธอด getMarsRealEstateProperties() ใน OverviewViewModel ตอนนี้คุณต้องอัปเดตคำขอนั้นเพื่อใช้อาร์กิวเมนต์ตัวกรอง

  1. เปิด overview/OverviewViewModel.kt คุณจะเห็นข้อผิดพลาดใน Android Studio เนื่องจากการเปลี่ยนแปลงที่คุณทำในขั้นตอนก่อนหน้า เพิ่ม MarsApiFilter (การแจงนับค่าตัวกรองที่เป็นไปได้) เป็นพารามิเตอร์ในการเรียก getMarsRealEstateProperties()

    นำเข้าcom.example.android.marsrealestate.network.MarsApiFilterเมื่อได้รับคำขอ
private fun getMarsRealEstateProperties(filter: MarsApiFilter) {
  1. แก้ไขการเรียกไปยัง getProperties() ในบริการ Retrofit เพื่อส่งต่อการค้นหาตัวกรองนั้นเป็นสตริง
var getPropertiesDeferred = MarsApi.retrofitService.getProperties(filter.value)
  1. ในบล็อก init {} ให้ส่ง MarsApiFilter.SHOW_ALL เป็นอาร์กิวเมนต์ไปยัง getMarsRealEstateProperties() เพื่อแสดงพร็อพเพอร์ตี้ทั้งหมดเมื่อแอปโหลดเป็นครั้งแรก
init {
   getMarsRealEstateProperties(MarsApiFilter.SHOW_ALL)
}
  1. ที่ท้ายคลาส ให้เพิ่มเมธอด updateFilter() ที่รับอาร์กิวเมนต์ MarsApiFilter และเรียกใช้ getMarsRealEstateProperties() ด้วยอาร์กิวเมนต์นั้น
fun updateFilter(filter: MarsApiFilter) {
   getMarsRealEstateProperties(filter)
}

ขั้นตอนที่ 3: เชื่อมต่อ Fragment กับเมนูตัวเลือก

ขั้นตอนสุดท้ายคือการเชื่อมต่อเมนูรายการเพิ่มเติมกับ Fragment เพื่อเรียกใช้ updateFilter() ใน ViewModel เมื่อผู้ใช้เลือกตัวเลือกเมนู

  1. เปิด res/menu/overflow_menu.xml แอป MarsRealEstate มีเมนูแบบเลื่อนลงอยู่แล้วซึ่งมีตัวเลือก 3 รายการ ได้แก่ แสดงที่พักทั้งหมด แสดงเฉพาะที่พักให้เช่า และแสดงเฉพาะที่พักสำหรับขาย
<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() ใน ViewModel ด้วยตัวกรองที่เหมาะสม ใช้บล็อก when {} Kotlin เพื่อสลับระหว่างตัวเลือก ใช้ 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. เลือกซื้อจากเมนูตัวเลือก พร็อพเพอร์ตี้จะโหลดซ้ำอีกครั้ง และทั้งหมดจะปรากฏพร้อมไอคอนดอลลาร์ (แสดงเฉพาะที่พักที่ประกาศขาย)

ตอนนี้คุณมีตารางไอคอนที่เลื่อนได้สำหรับที่พักของ Mars แต่ถึงเวลาดูรายละเอียดเพิ่มเติมแล้ว ในงานนี้ คุณจะเพิ่ม Detail Fragment เพื่อแสดงรายละเอียดของพร็อพเพอร์ตี้ที่เฉพาะเจาะจง โดยรายละเอียดจะแสดงรูปภาพที่ใหญ่ขึ้น ราคา และประเภทที่พัก ไม่ว่าจะเป็นที่พักให้เช่าหรือที่พักสำหรับขาย

ระบบจะเปิดใช้ Fragment นี้เมื่อผู้ใช้แตะรูปภาพในตารางภาพรวม หากต้องการดำเนินการนี้ คุณต้องเพิ่มonClick Listener ไปยังRecyclerViewรายการตารางกริด แล้วไปยัง Fragment ใหม่ คุณจะไปยังส่วนต่างๆ ได้โดยการทริกเกอร์การเปลี่ยนแปลง LiveData ใน ViewModel ดังที่คุณได้ทำตลอดบทเรียนเหล่านี้ นอกจากนี้ คุณยังใช้ปลั๊กอิน Safe Args ของคอมโพเนนต์การนำทางเพื่อส่งMarsPropertyข้อมูลที่เลือกจาก Fragment ภาพรวมไปยัง Fragment รายละเอียดได้ด้วย

ขั้นตอนที่ 1: สร้างโมเดลมุมมองรายละเอียดและอัปเดตเลย์เอาต์รายละเอียด

เช่นเดียวกับกระบวนการที่คุณใช้สำหรับโมเดลมุมมองภาพรวมและ Fragment ตอนนี้คุณต้องใช้โมเดลมุมมองและไฟล์เลย์เอาต์สำหรับ Fragment รายละเอียด

  1. เปิด detail/DetailViewModel.kt เช่นเดียวกับไฟล์ Kotlin ที่เกี่ยวข้องกับเครือข่ายซึ่งอยู่ในโฟลเดอร์ network และไฟล์ภาพรวมใน overview โฟลเดอร์ detail จะมีไฟล์ที่เชื่อมโยงกับมุมมองรายละเอียด โปรดทราบว่า DetailViewModel class (ว่างเปล่าในตอนนี้) จะใช้ marsProperty เป็นพารามิเตอร์ในตัวสร้าง
class DetailViewModel( marsProperty: MarsProperty,
                     app: Application) : AndroidViewModel(app) {
}
  1. ภายในคำจำกัดความของคลาส ให้เพิ่ม LiveData สำหรับพร็อพเพอร์ตี้ Mars ที่เลือก เพื่อแสดงข้อมูลดังกล่าวในมุมมองรายละเอียด ทำตามรูปแบบปกติของการสร้าง MutableLiveData เพื่อเก็บ MarsProperty ไว้ แล้วแสดงพร็อพเพอร์ตี้ LiveData สาธารณะที่ไม่เปลี่ยนแปลง

    นำเข้า androidx.lifecycle.LiveData และนำเข้า androidx.lifecycle.MutableLiveData เมื่อได้รับคำขอ
private val _selectedProperty = MutableLiveData<MarsProperty>()
val selectedProperty: LiveData<MarsProperty>
   get() = _selectedProperty
  1. สร้างinit {}บล็อกและตั้งค่าพร็อพเพอร์ตี้ Mars ที่เลือกด้วยออบเจ็กต์ MarsProperty จากตัวสร้าง
    init {
        _selectedProperty.value = marsProperty
    }
  1. เปิด res/layout/fragment_detail.xml แล้วดูในมุมมองการออกแบบ

    นี่คือไฟล์เลย์เอาต์สำหรับรายละเอียดของ Fragment โดยมี ImageView สำหรับรูปภาพขนาดใหญ่ TextView สำหรับประเภทที่พัก (เช่าหรือขาย) และ TextView สำหรับราคา โปรดสังเกตว่าเลย์เอาต์ข้อจำกัดจะอยู่ใน ScrollView เพื่อให้เลื่อนโดยอัตโนมัติหากมุมมองมีขนาดใหญ่เกินไปสำหรับการแสดงผล เช่น เมื่อผู้ใช้ดูในโหมดแนวนอน
  2. ไปที่แท็บข้อความสำหรับเลย์เอาต์ ที่ด้านบนของเลย์เอาต์ ให้เพิ่มองค์ประกอบ <data> ก่อนองค์ประกอบ <ScrollView> เพื่อเชื่อมโยงโมเดลมุมมองรายละเอียดกับเลย์เอาต์
<data>
   <variable
       name="viewModel"
       type="com.example.android.marsrealestate.detail.DetailViewModel" />
</data>
  1. เพิ่มแอตทริบิวต์ app:imageUrl ลงในองค์ประกอบ ImageView ตั้งค่าเป็น imgSrcUrl จากพร็อพเพอร์ตี้ที่เลือกของโมเดลมุมมอง

    ระบบจะใช้อะแดปเตอร์การเชื่อมโยงที่โหลดรูปภาพโดยใช้ Glide ที่นี่โดยอัตโนมัติด้วย เนื่องจากอะแดปเตอร์ดังกล่าวจะดูแอตทริบิวต์ app:imageUrl ทั้งหมด
 app:imageUrl="@{viewModel.selectedProperty.imgSrcUrl}"

ขั้นตอนที่ 2: กำหนดการนำทางใน ViewModel ของภาพรวม

เมื่อผู้ใช้แตะรูปภาพในโมเดลภาพรวม ระบบควรทริกเกอร์การนำทางไปยัง Fragment ที่แสดงรายละเอียดเกี่ยวกับรายการที่คลิก

  1. เปิด overview/OverviewViewModel.kt เพิ่มพร็อพเพอร์ตี้ _navigateToSelectedProperty MutableLiveData และเปิดเผยด้วย LiveData ที่เปลี่ยนแปลงไม่ได้

    เมื่อ LiveData เปลี่ยนเป็นค่าที่ไม่ใช่ Null ระบบจะทริกเกอร์การนำทาง (ในไม่ช้าคุณจะเพิ่มโค้ดเพื่อสังเกตตัวแปรนี้และทริกเกอร์การนำทาง)
private val _navigateToSelectedProperty = MutableLiveData<MarsProperty>()
val navigateToSelectedProperty: LiveData<MarsProperty>
   get() = _navigateToSelectedProperty
  1. ที่ท้ายคลาส ให้เพิ่มเมธอด displayPropertyDetails() ที่ตั้งค่า _navigateToSelectedProperty เป็นพร็อพเพอร์ตี้ Mars ที่เลือก
fun displayPropertyDetails(marsProperty: MarsProperty) {
   _navigateToSelectedProperty.value = marsProperty
}
  1. เพิ่มเมธอด displayPropertyDetailsComplete() ที่ทำให้ค่าของ _navigateToSelectedProperty เป็น Null คุณต้องใช้ค่านี้เพื่อทำเครื่องหมายสถานะการนำทางว่าเสร็จสมบูรณ์ และเพื่อหลีกเลี่ยงไม่ให้ระบบทริกเกอร์การนำทางอีกครั้งเมื่อผู้ใช้กลับมาจากมุมมองรายละเอียด
fun displayPropertyDetailsComplete() {
   _navigateToSelectedProperty.value = null
}

ขั้นตอนที่ 3: ตั้งค่าเครื่องมือฟังการคลิกใน Grid Adapter และ Fragment

  1. เปิด overview/PhotoGridAdapter.kt เมื่อสิ้นสุดคลาส ให้สร้างOnClickListenerคลาสที่กำหนดเองOnClickListenerซึ่งใช้ Lambda ที่มีพารามิเตอร์ marsProperty ภายในคลาส ให้กำหนดฟังก์ชัน onClick() ที่ตั้งค่าเป็นพารามิเตอร์ Lambda
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. ทำให้รูปภาพคลิกได้โดยการเพิ่ม onClickListener ลงในรายการตารางกริดในเมธอด onBindviewHolder() กำหนดเครื่องมือฟังการคลิกระหว่างการเรียกไปยัง 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 และเรียกใช้ viewModel.displayPropertyDetails() ด้วยออบเจ็กต์ MarsProperty ที่ส่งเข้ามา ซึ่งจะทริกเกอร์ LiveData ใน ViewModel สำหรับการนำทาง
binding.photosGrid.adapter = PhotoGridAdapter(PhotoGridAdapter.OnClickListener {
   viewModel.displayPropertyDetails(it)
})

ขั้นตอนที่ 4: แก้ไขกราฟการนำทางและทำให้ MarsProperty สามารถส่งผ่านได้

เมื่อผู้ใช้แตะรูปภาพในตารางภาพรวม แอปควรไปยัง Fragment รายละเอียดและส่งรายละเอียดของพร็อพเพอร์ตี้ดาวอังคารที่เลือก เพื่อให้มุมมองรายละเอียดแสดงข้อมูลนั้นได้

ตอนนี้คุณมีเครื่องมือฟังการคลิกจาก PhotoGridAdapter เพื่อจัดการการแตะ และวิธีทริกเกอร์การนำทางจาก ViewModel แต่คุณยังไม่มีออบเจ็กต์ MarsProperty ที่ส่งไปยังรายละเอียด Fragment โดยคุณจะใช้ Safe Args จากคอมโพเนนต์การนำทาง

  1. เปิด res/navigation/nav_graph.xml คลิกแท็บข้อความเพื่อดูโค้ด XML สำหรับกราฟการนำทาง
  2. ภายในองค์ประกอบ <fragment> สำหรับรายละเอียดของข้อมูล ให้เพิ่มองค์ประกอบ <argument> ที่แสดงด้านล่าง อาร์กิวเมนต์นี้เรียกว่า selectedProperty มีประเภทเป็น MarsProperty
<argument
   android:name="selectedProperty"
   app:argType="com.example.android.marsrealestate.network.MarsProperty"
   />
  1. คอมไพล์แอป การนำทางจะแสดงข้อผิดพลาดเนื่องจาก MarsProperty ไม่ใช่ parcelable อินเทอร์เฟซ Parcelable ช่วยให้สามารถซีเรียลไลซ์ออบเจ็กต์ได้ เพื่อให้ส่งข้อมูลของออบเจ็กต์ไปมาระหว่าง Fragment หรือกิจกรรมได้ ในกรณีนี้ MarsProperty ต้องใช้Parcelableอินเทอร์เฟซเพื่อให้ส่งข้อมูลภายในออบเจ็กต์ MarsProperty ไปยังรายละเอียด Fragment ผ่าน Safe Args ได้ ข่าวดีคือ 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: เชื่อมต่อ Fragment

คุณยังไม่ได้ไปยังส่วนต่างๆ การไปยังส่วนต่างๆ จริงจะเกิดขึ้นใน Fragment ในขั้นตอนนี้ คุณจะเพิ่มส่วนสุดท้ายสำหรับการติดตั้งใช้งานการนำทางระหว่างภาพรวมและรายละเอียดของ Fragment

  1. เปิด overview/OverviewFragment.kt ใน onCreateView() ใต้บรรทัดที่เริ่มต้นอแดปเตอร์ตารางกริดรูปภาพ ให้เพิ่มบรรทัดที่แสดงด้านล่างเพื่อสังเกต navigatedToSelectedProperty จากโมเดลมุมมองภาพรวม

    นำเข้า androidx.lifecycle.Observer และนำเข้า androidx.navigation.fragment.findNavController เมื่อได้รับคำขอ

    Observer จะทดสอบว่า MarsProperty ซึ่งเป็น it ใน Lambda ไม่ใช่ค่าว่างหรือไม่ หากไม่ใช่ค่าว่าง Observer จะรับ Navigation Controller จาก Fragment ที่มี findNavController() เรียกใช้ displayPropertyDetailsComplete() เพื่อบอกให้ ViewModel รีเซ็ต LiveData เป็นสถานะ Null เพื่อไม่ให้คุณทริกเกอร์การนำทางอีกครั้งโดยไม่ตั้งใจเมื่อแอปกลับไปที่ OverviewFragment
viewModel.navigateToSelectedProperty.observe(this, Observer {
   if ( null != it ) {   
      this.findNavController().navigate(
              OverviewFragmentDirections.actionShowDetail(it))             
      viewModel.displayPropertyDetailsComplete()
   }
})
  1. เปิด detail/DetailFragment.kt เพิ่มบรรทัดนี้ใต้การเรียกใช้ setLifecycleOwner() ในเมธอด onCreateView() บรรทัดนี้จะรับออบเจ็กต์ MarsProperty ที่เลือกจาก Safe Args

    โปรดสังเกตการใช้ตัวดำเนินการยืนยันว่าไม่ใช่ Null ของ Kotlin (!!) หากไม่มี 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 ยังมีประเภทพร็อพเพอร์ตี้ (เช่าหรือซื้อ) และราคาพร็อพเพอร์ตี้ด้วย หน้าจอรายละเอียดควรมีทั้ง 2 ค่านี้ และจะเป็นประโยชน์หากที่พักให้เช่าระบุว่าราคาเป็นค่าต่อเดือน คุณใช้LiveDataการเปลี่ยนรูปแบบใน ViewModel เพื่อใช้ทั้ง 2 อย่าง

  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 {} สตริงทั้ง 2 รายการนี้ต้องมีตัวเลขต่อท้าย คุณจึงต่อท้าย 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 Studio: MarsRealEstateFinal

การเชื่อมโยงนิพจน์

  • ใช้นิพจน์การเชื่อมโยงในไฟล์เลย์เอาต์ XML เพื่อดำเนินการแบบเป็นโปรแกรมอย่างง่าย เช่น การทดสอบทางคณิตศาสตร์หรือแบบมีเงื่อนไขกับข้อมูลที่เชื่อมโยง
  • หากต้องการอ้างอิงคลาสภายในไฟล์เลย์เอาต์ ให้ใช้แท็ก <import> ภายในแท็ก <data>

ตัวเลือกการค้นหาบริการบนเว็บ

  • คำขอไปยังบริการเว็บอาจมีพารามิเตอร์ที่ไม่บังคับ
  • หากต้องการระบุพารามิเตอร์การค้นหาในคำขอ ให้ใช้คำอธิบายประกอบ @Query ใน Retrofit

หลักสูตร Udacity:

เอกสารประกอบสำหรับนักพัฒนาแอป Android

อื่นๆ:

ส่วนนี้แสดงรายการการบ้านที่เป็นไปได้สำหรับนักเรียน/นักศึกษาที่กำลังทำ Codelab นี้เป็นส่วนหนึ่งของหลักสูตรที่สอนโดยผู้สอน ผู้สอนมีหน้าที่ดำเนินการต่อไปนี้

  • มอบหมายการบ้านหากจำเป็น
  • สื่อสารกับนักเรียนเกี่ยวกับวิธีส่งงานที่ได้รับมอบหมาย
  • ให้คะแนนงานการบ้าน

ผู้สอนสามารถใช้คำแนะนำเหล่านี้ได้มากน้อยตามที่ต้องการ และควรมีอิสระในการมอบหมายการบ้านอื่นๆ ที่เห็นว่าเหมาะสม

หากคุณกำลังทำ Codelab นี้ด้วยตนเอง โปรดใช้แบบฝึกหัดเหล่านี้เพื่อทดสอบความรู้ของคุณ

ตอบคำถามต่อไปนี้

คำถามที่ 1

แท็ก <import> ในไฟล์เลย์เอาต์ XML มีหน้าที่อะไร

▢ รวมไฟล์เลย์เอาต์หนึ่งไว้ในอีกไฟล์หนึ่ง

▢ ฝังโค้ด Kotlin ไว้ในไฟล์เลย์เอาต์

▢ ให้สิทธิ์เข้าถึงพร็อพเพอร์ตี้ที่เชื่อมโยงกับข้อมูล

▢ ช่วยให้คุณอ้างอิงชั้นเรียนและสมาชิกในชั้นเรียนในนิพจน์การเชื่อมโยงได้

คำถามที่ 2

คุณจะเพิ่มตัวเลือกการค้นหาในการเรียกใช้เว็บเซอร์วิส REST ใน Retrofit ได้อย่างไร

▢ เพิ่มการค้นหาต่อท้าย URL ของคำขอ

▢ เพิ่มพารามิเตอร์สำหรับการค้นหาไปยังฟังก์ชันที่ส่งคำขอ และใส่คำอธิบายประกอบพารามิเตอร์นั้นด้วย @Query

▢ ใช้คลาส Query เพื่อสร้างคำขอ

▢ ใช้เมธอด addQuery() ในเครื่องมือสร้าง Retrofit

เริ่มบทเรียนถัดไป: 9.1: ที่เก็บ

ดูลิงก์ไปยัง Codelab อื่นๆ ในหลักสูตรนี้ได้ที่หน้า Landing Page ของ Codelab หลักพื้นฐานของ Android Kotlin