หลักพื้นฐานของ Android Kotlin 08.2: การโหลดและแสดงรูปภาพจากอินเทอร์เน็ต

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

บทนำ

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

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

  • วิธีสร้างและใช้ Fragment
  • วิธีใช้คอมโพเนนต์สถาปัตยกรรม รวมถึงโมเดลมุมมอง โรงงานโมเดลมุมมอง การแปลง และ LiveData
  • วิธีดึงข้อมูล JSON จากเว็บเซอร์วิส REST และแยกวิเคราะห์ข้อมูลดังกล่าวเป็นออบเจ็กต์ Kotlin โดยใช้ไลบรารี Retrofit และ Moshi
  • วิธีสร้างเลย์เอาต์ตารางกริดด้วย RecyclerView
  • วิธีการทำงานของ Adapter, ViewHolder และ DiffUtil

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

  • วิธีใช้ไลบรารี Glide เพื่อโหลดและแสดงรูปภาพจาก URL ของเว็บ
  • วิธีใช้ RecyclerView และอะแดปเตอร์กริดเพื่อแสดงตารางกริดของรูปภาพ
  • วิธีจัดการข้อผิดพลาดที่อาจเกิดขึ้นขณะดาวน์โหลดและแสดงรูปภาพ

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

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

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

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

การแสดงรูปภาพจาก URL ของเว็บอาจดูเหมือนเป็นเรื่องง่าย แต่ต้องมีการออกแบบทางวิศวกรรมพอสมควรเพื่อให้ทำงานได้ดี ระบบต้องดาวน์โหลด บัฟเฟอร์ และถอดรหัสรูปภาพจากรูปแบบที่บีบอัดเป็นรูปภาพที่ Android ใช้ได้ ควรแคชรูปภาพไปยังแคชในหน่วยความจำ แคชที่อิงตามพื้นที่เก็บข้อมูล หรือทั้ง 2 อย่าง การดำเนินการทั้งหมดนี้ต้องเกิดขึ้นในเธรดเบื้องหลังที่มีลำดับความสำคัญต่ำเพื่อให้ UI ตอบสนองได้ นอกจากนี้ เพื่อให้เครือข่ายและ CPU ทำงานได้ดีที่สุด คุณอาจต้องดึงข้อมูลและถอดรหัสรูปภาพมากกว่า 1 รูปพร้อมกัน การเรียนรู้วิธีโหลดรูปภาพจากเครือข่ายอย่างมีประสิทธิภาพอาจเป็น Codelab ในตัว

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

โดยพื้นฐานแล้ว Glide ต้องการ 2 สิ่งต่อไปนี้

  • URL ของรูปภาพที่คุณต้องการโหลดและแสดง
  • ออบเจ็กต์ ImageView เพื่อแสดงรูปภาพนั้น

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

ขั้นตอนที่ 1: เพิ่มการอ้างอิง Glide

  1. เปิดแอป MarsRealEstate จากโค้ดแล็บล่าสุด (คุณดาวน์โหลด MarsRealEstateNetwork ได้ที่นี่หากไม่มีแอป)
  2. เรียกใช้แอปเพื่อดูว่าแอปทำอะไรได้บ้าง (แสดงรายละเอียดข้อความของพร็อพเพอร์ตี้ที่สมมติว่ามีอยู่บนดาวอังคาร)
  3. เปิด build.gradle (Module: app)
  4. ในส่วน dependencies ให้เพิ่มบรรทัดนี้สำหรับไลบรารี Glide
implementation "com.github.bumptech.glide:glide:$version_glide"


โปรดทราบว่าหมายเลขเวอร์ชันมีการกำหนดแยกต่างหากในไฟล์ Gradle ของโปรเจ็กต์อยู่แล้ว

  1. คลิกซิงค์เลยเพื่อสร้างโปรเจ็กต์ใหม่โดยใช้การอ้างอิงใหม่

ขั้นตอนที่ 2: อัปเดตโมเดลมุมมอง

จากนั้นอัปเดตคลาส OverviewViewModel เพื่อรวมข้อมูลแบบเรียลไทม์สำหรับที่พักของ Mars รายการเดียว

  1. เปิด overview/OverviewViewModel.kt เพิ่มทั้งข้อมูลสดภายใน (เปลี่ยนแปลงได้) และภายนอก (เปลี่ยนแปลงไม่ได้) สำหรับออบเจ็กต์ MarsProperty รายการเดียวที่อยู่ใต้ LiveData สำหรับ _response

    นำเข้าMarsPropertyชั้นเรียน (com.example.android.marsrealestate.network.MarsProperty) เมื่อได้รับคำขอ
private val _property = MutableLiveData<MarsProperty>()

val property: LiveData<MarsProperty>
   get() = _property
  1. ในเมธอด getMarsRealEstateProperties() ให้ค้นหาบรรทัดภายในบล็อก try/catch {} ที่ตั้งค่า _response.value เป็นจำนวนพร็อพเพอร์ตี้ เพิ่มการทดสอบที่แสดงด้านล่าง หากมีออบเจ็กต์ MarsProperty การทดสอบนี้จะตั้งค่า _property LiveData เป็นพร็อพเพอร์ตี้แรกใน listResult
if (listResult.size > 0) {   
    _property.value = listResult[0]
}

ตอนนี้บล็อก try/catch {} ที่สมบูรณ์จะมีลักษณะดังนี้

try {
   var listResult = getPropertiesDeferred.await()
   _response.value = "Success: ${listResult.size} Mars properties retrieved"
   if (listResult.size > 0) {      
       _property.value = listResult[0]
   }
 } catch (e: Exception) {
    _response.value = "Failure: ${e.message}"
 }
  1. เปิดไฟล์ res/layout/fragment_overview.xml ในองค์ประกอบ <TextView> ให้เปลี่ยน android:text เพื่อเชื่อมโยงกับคอมโพเนนต์ imgSrcUrl ของ property LiveData ดังนี้
android:text="@{viewModel.property.imgSrcUrl}"
  1. เรียกใช้แอป TextView จะแสดงเฉพาะ URL ของรูปภาพในพร็อพเพอร์ตี้ Mars แรก สิ่งที่คุณทำไปแล้วจนถึงตอนนี้คือการตั้งค่า ViewModel และข้อมูลแบบเรียลไทม์สำหรับ URL นั้น

ขั้นตอนที่ 3: สร้าง Binding Adapter และเรียกใช้ Glide

ตอนนี้คุณมี URL ของรูปภาพที่จะแสดงแล้ว และถึงเวลาเริ่มทำงานกับ Glide เพื่อโหลดรูปภาพนั้น ในขั้นตอนนี้ คุณจะใช้ตัวดัดแปลงการเชื่อมโยงเพื่อนำ URL จากแอตทริบิวต์ XML ที่เชื่อมโยงกับ ImageView และใช้ Glide เพื่อโหลดรูปภาพ Binding Adapter เป็นเมธอดส่วนขยายที่อยู่ระหว่างมุมมองกับข้อมูลที่เชื่อมโยงเพื่อกำหนดลักษณะการทำงานที่กำหนดเองเมื่อข้อมูลเปลี่ยนแปลง ในกรณีนี้ ลักษณะการทำงานที่กำหนดเองคือการเรียก Glide เพื่อโหลดรูปภาพจาก URL ลงใน ImageView

  1. เปิด BindingAdapters.kt ไฟล์นี้จะมี Binding Adapter ที่คุณใช้ทั่วทั้งแอป
  2. สร้างฟังก์ชัน bindImage() ที่ใช้ ImageView และ String เป็นพารามิเตอร์ ใส่คำอธิบายประกอบฟังก์ชันด้วย @BindingAdapter คำอธิบายประกอบ @BindingAdapter จะบอกการเชื่อมโยงข้อมูลว่าคุณต้องการให้เรียกใช้อะแดปเตอร์การเชื่อมโยงนี้เมื่อรายการ XML มีแอตทริบิวต์ imageUrl

    นำเข้า androidx.databinding.BindingAdapter และ android.widget.ImageView เมื่อได้รับคำขอ
@BindingAdapter("imageUrl")
fun bindImage(imgView: ImageView, imgUrl: String?) {

}
  1. ภายในฟังก์ชัน bindImage() ให้เพิ่มบล็อก let {} สำหรับอาร์กิวเมนต์ imgUrl ดังนี้
imgUrl?.let { 
}
  1. ภายในบล็อก let {} ให้เพิ่มบรรทัดที่แสดงด้านล่างเพื่อแปลงสตริง URL (จาก XML) เป็นออบเจ็กต์ Uri นำเข้า androidx.core.net.toUri เมื่อได้รับคำขอ

    คุณต้องการให้ออบเจ็กต์ Uri สุดท้ายใช้รูปแบบ HTTPS เนื่องจากเซิร์ฟเวอร์ที่คุณดึงรูปภาพมาต้องใช้รูปแบบดังกล่าว หากต้องการใช้รูปแบบ HTTPS ให้ต่อท้าย buildUpon.scheme("https") ในเครื่องมือสร้าง toUri เมธอด toUri() เป็นฟังก์ชันส่วนขยาย Kotlin จากไลบรารีหลักของ Android KTX จึงดูเหมือนเป็นส่วนหนึ่งของคลาส String
val imgUri = imgUrl.toUri().buildUpon().scheme("https").build()
  1. ยังคงอยู่ภายใน let {} ให้เรียกใช้ Glide.with() เพื่อโหลดรูปภาพจากออบเจ็กต์ Uri ลงใน ImageView นำเข้า com.bumptech.glide.Glide เมื่อได้รับคำขอ
Glide.with(imgView.context)
       .load(imgUri)
       .into(imgView)

ขั้นตอนที่ 4: อัปเดตเลย์เอาต์และ Fragment

แม้ว่า Glide จะโหลดรูปภาพแล้ว แต่ก็ยังไม่มีอะไรให้เห็น ขั้นตอนถัดไปคือการอัปเดตเลย์เอาต์และ Fragment ด้วย ImageView เพื่อแสดงรูปภาพ

  1. เปิด res/layout/gridview_item.xml นี่คือไฟล์ทรัพยากรเลย์เอาต์ที่คุณจะใช้สำหรับแต่ละรายการใน RecyclerView ในภายหลังในโค้ดแล็บ คุณใช้ที่นี่ชั่วคราวเพื่อแสดงรูปภาพเดียวเท่านั้น
  2. เหนือองค์ประกอบ <ImageView> ให้เพิ่มองค์ประกอบ <data> สำหรับการเชื่อมโยงข้อมูล และเชื่อมโยงกับคลาส OverviewViewModel ดังนี้
<data>
   <variable
       name="viewModel"
       type="com.example.android.marsrealestate.overview.OverviewViewModel" />
</data>
  1. เพิ่มแอตทริบิวต์ app:imageUrl ลงในองค์ประกอบ ImageView เพื่อใช้ Binding Adapter การโหลดรูปภาพใหม่
app:imageUrl="@{viewModel.property.imgSrcUrl}"
  1. เปิด overview/OverviewFragment.kt ในonCreateView() ให้แสดงความคิดเห็นในบรรทัดที่ขยายคลาส FragmentOverviewBinding และกำหนดให้กับตัวแปรการเชื่อมโยง ซึ่งเป็นการดำเนินการชั่วคราวเท่านั้น และคุณจะกลับไปใช้บัญชีเดิมได้ในภายหลัง
//val binding = FragmentOverviewBinding.inflate(inflater)
  1. โปรดเพิ่มบรรทัดเพื่อขยายคลาส GridViewItemBinding แทน นำเข้า com.example.android.marsrealestate. databinding.GridViewItemBinding เมื่อได้รับคำขอ
val binding = GridViewItemBinding.inflate(inflater)
  1. เรียกใช้แอป ตอนนี้คุณควรเห็นรูปภาพของรูปภาพจาก MarsProperty แรกในรายการผลการค้นหา

ขั้นตอนที่ 5: เพิ่มรูปภาพการโหลดและข้อผิดพลาดอย่างง่าย

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

  1. เปิด res/drawable/ic_broken_image.xml แล้วคลิกแท็บแสดงตัวอย่างทางด้านขวา สำหรับรูปภาพข้อผิดพลาด คุณกำลังใช้ไอคอนรูปภาพเสียซึ่งมีอยู่ในคลังไอคอนในตัว Vector Drawable นี้ใช้แอตทริบิวต์ android:tint เพื่อเปลี่ยนสีไอคอนเป็นสีเทา

  1. เปิด res/drawable/loading_animation.xml Drawable นี้คือภาพเคลื่อนไหวที่กำหนดด้วยแท็ก <animate-rotate> ภาพเคลื่อนไหวจะหมุน Drawable ของรูปภาพ loading_img.xml รอบจุดกึ่งกลาง (คุณจะไม่เห็นภาพเคลื่อนไหวในตัวอย่าง)

  1. กลับไปที่ไฟล์ BindingAdapters.kt ในbindImage() ให้อัปเดตการเรียกใช้ Glide.with() เพื่อเรียกใช้ฟังก์ชัน apply() ระหว่าง load() และ into() นำเข้า com.bumptech.glide.request.RequestOptions เมื่อมีการร้องขอ

    โค้ดนี้จะตั้งค่ารูปภาพการโหลดตัวยึดตำแหน่งที่จะใช้ขณะโหลด (loading_animation drawable) โค้ดยังตั้งค่ารูปภาพที่จะใช้หากโหลดรูปภาพไม่สำเร็จ (broken_image drawable) ตอนนี้bindImage()ที่สมบูรณ์จะมีลักษณะดังนี้
@BindingAdapter("imageUrl")
fun bindImage(imgView: ImageView, imgUrl: String?) {
    imgUrl?.let {
        val imgUri = 
           imgUrl.toUri().buildUpon().scheme("https").build()
        Glide.with(imgView.context)
                .load(imgUri)
                .apply(RequestOptions()
                        .placeholder(R.drawable.loading_animation)
                        .error(R.drawable.ic_broken_image))
                .into(imgView)
    }
}
  1. เรียกใช้แอป คุณอาจเห็นรูปภาพการโหลดชั่วครู่ขณะที่ Glide ดาวน์โหลดและแสดงรูปภาพพร็อพเพอร์ตี้ ทั้งนี้ขึ้นอยู่กับความเร็วของการเชื่อมต่อเครือข่าย แต่คุณจะยังไม่เห็นไอคอนรูปภาพเสีย แม้ว่าจะปิดเครือข่ายก็ตาม โดยคุณจะแก้ไขปัญหานี้ได้ในส่วนสุดท้ายของ Codelab

ตอนนี้แอปจะโหลดข้อมูลพร็อพเพอร์ตี้จากอินเทอร์เน็ต คุณใช้ข้อมูลจากรายการแรกในรายการ MarsProperty เพื่อสร้างพร็อพเพอร์ตี้ LiveData ใน View Model และใช้ URL ของรูปภาพจากข้อมูลพร็อพเพอร์ตี้นั้นเพื่อป้อนข้อมูลใน ImageView แต่เป้าหมายคือให้แอปแสดงตารางกริดของรูปภาพ ดังนั้นคุณจึงต้องการใช้ RecyclerView กับ GridLayoutManager

ขั้นตอนที่ 1: อัปเดตโมเดลมุมมอง

ตอนนี้โมเดลมุมมองมี _property LiveData ที่มีออบเจ็กต์ MarsProperty หนึ่งรายการ ซึ่งเป็นรายการแรกในรายการการตอบกลับจากบริการเว็บ ในขั้นตอนนี้ คุณจะเปลี่ยน LiveData เพื่อเก็บออบเจ็กต์ MarsProperty ทั้งหมด

  1. เปิด overview/OverviewViewModel.kt
  2. เปลี่ยนตัวแปรส่วนตัว _property เป็น _properties เปลี่ยนประเภทเป็นรายการของออบเจ็กต์ MarsProperty
private val _properties = MutableLiveData<List<MarsProperty>>()
  1. แทนที่propertyข้อมูลสดภายนอกด้วย properties เพิ่มรายการลงในประเภท LiveData ที่นี่ด้วย
 val properties: LiveData<List<MarsProperty>>
        get() = _properties
  1. เลื่อนลงไปที่วิธีการ getMarsRealEstateProperties() ในtry {} บล็อก ให้แทนที่การทดสอบทั้งหมดที่คุณเพิ่มในงานก่อนหน้าด้วยบรรทัดที่แสดงด้านล่าง เนื่องจากตัวแปร listResult มีรายการออบเจ็กต์ MarsProperty คุณจึงกำหนดตัวแปรนี้ให้กับ _properties.value ได้เลยแทนที่จะทดสอบการตอบกลับที่สำเร็จ
_properties.value = listResult

ตอนนี้try/catch บล็อกทั้งหมดจะมีลักษณะดังนี้

try {
   var listResult = getPropertiesDeferred.await()
   _response.value = "Success: ${listResult.size} Mars properties retrieved"
   _properties.value = listResult
} catch (e: Exception) {
   _response.value = "Failure: ${e.message}"
}

ขั้นตอนที่ 2: อัปเดตเลย์เอาต์และ Fragment

ขั้นตอนถัดไปคือการเปลี่ยนเลย์เอาต์และ Fragment ของแอปให้ใช้ RecyclerView และ GridLayout แทนที่จะใช้ SingleImageView

  1. เปิด res/layout/gridview_item.xml เปลี่ยนการเชื่อมโยงข้อมูลจาก OverviewViewModel เป็น MarsProperty และเปลี่ยนชื่อตัวแปรเป็น "property"
<variable
   name="property"
   type="com.example.android.marsrealestate.network.MarsProperty" />
  1. ใน <ImageView> ให้เปลี่ยนแอตทริบิวต์ app:imageUrl เพื่ออ้างอิงถึง URL ของรูปภาพในออบเจ็กต์ MarsProperty
app:imageUrl="@{property.imgSrcUrl}"
  1. เปิด overview/OverviewFragment.kt ใน onCreateview() ให้ยกเลิกการแสดงความคิดเห็นในบรรทัดที่ขยาย FragmentOverviewBinding ลบหรือแสดงความคิดเห็นในบรรทัดที่เพิ่ม GridViewBinding การเปลี่ยนแปลงเหล่านี้จะยกเลิกการเปลี่ยนแปลงชั่วคราวที่คุณทำในงานล่าสุด
val binding = FragmentOverviewBinding.inflate(inflater)
 // val binding = GridViewItemBinding.inflate(inflater)
  1. เปิด res/layout/fragment_overview.xml ลบองค์ประกอบ <TextView> ทั้งหมด
  2. ให้เพิ่มองค์ประกอบ <RecyclerView> นี้แทน ซึ่งใช้ GridLayoutManager และเลย์เอาต์ grid_view_item สำหรับสินค้า 1 รายการ
<androidx.recyclerview.widget.RecyclerView
            android:id="@+id/photos_grid"
            android:layout_width="0dp"
            android:layout_height="0dp"
            android:padding="6dp"
            android:clipToPadding="false"
            app:layoutManager=
               "androidx.recyclerview.widget.GridLayoutManager"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            app:spanCount="2"
            tools:itemCount="16"
            tools:listitem="@layout/grid_view_item" />

ขั้นตอนที่ 3: เพิ่มอแดปเตอร์ตารางกริดรูปภาพ

ตอนนี้เลย์เอาต์ fragment_overview มี RecyclerView ในขณะที่เลย์เอาต์ grid_view_item มี ImageView เดียว ในขั้นตอนนี้ คุณจะเชื่อมโยงข้อมูลกับ RecyclerView ผ่านอะแดปเตอร์ RecyclerView

  1. เปิด overview/PhotoGridAdapter.kt
  2. สร้างคลาส PhotoGridAdapter โดยใช้พารามิเตอร์ของตัวสร้างที่แสดงด้านล่าง คลาส PhotoGridAdapter ขยาย ListAdapter ซึ่งตัวสร้างต้องมีประเภทรายการลิสต์ ตัวยึดมุมมอง และการติดตั้งใช้งาน DiffUtil.ItemCallback

    นำเข้าชั้นเรียน androidx.recyclerview.widget.ListAdapter และ com.example.android.marsrealestate.network.MarsProperty เมื่อได้รับคำขอ ในขั้นตอนต่อไปนี้ คุณจะใช้ส่วนอื่นๆ ที่ขาดหายไปของตัวสร้างนี้ซึ่งทำให้เกิดข้อผิดพลาด
class PhotoGridAdapter : ListAdapter<MarsProperty,
        PhotoGridAdapter.MarsPropertyViewHolder>(DiffCallback) {

}
  1. คลิกที่ใดก็ได้ในPhotoGridAdapter คลาส แล้วกด Control+i เพื่อใช้เมธอด ListAdapter ซึ่งได้แก่ onCreateViewHolder() และ onBindViewHolder()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PhotoGridAdapter.MarsPropertyViewHolder {
   TODO("not implemented") 
}

override fun onBindViewHolder(holder: PhotoGridAdapter.MarsPropertyViewHolder, position: Int) {
   TODO("not implemented") 
}
  1. ที่ส่วนท้ายของคำจำกัดความคลาส PhotoGridAdapter หลังจากเมธอดที่คุณเพิ่งเพิ่ม ให้เพิ่มคำจำกัดความออบเจ็กต์คู่สำหรับ DiffCallback ดังที่แสดงด้านล่าง

    นำเข้าandroidx.recyclerview.widget.DiffUtilเมื่อได้รับคำขอ

    ออบเจ็กต์ DiffCallback ขยาย DiffUtil.ItemCallback ด้วยประเภทออบเจ็กต์ที่คุณต้องการเปรียบเทียบ ซึ่งก็คือ MarsProperty
companion object DiffCallback : DiffUtil.ItemCallback<MarsProperty>() {
}
  1. กด Control+i เพื่อใช้เมธอดตัวเปรียบเทียบสำหรับออบเจ็กต์นี้ ซึ่งได้แก่ areItemsTheSame() และ areContentsTheSame()
override fun areItemsTheSame(oldItem: MarsProperty, newItem: MarsProperty): Boolean {
   TODO("not implemented") 
}

override fun areContentsTheSame(oldItem: MarsProperty, newItem: MarsProperty): Boolean {
   TODO("not implemented") }
  1. สำหรับareItemsTheSame() ให้นำ TODO ออก ใช้ตัวดำเนินการความเท่าเทียมกันโดยอ้างอิงของ Kotlin (===) ซึ่งจะแสดงผล true หากการอ้างอิงออบเจ็กต์สำหรับ oldItem และ newItem เหมือนกัน
override fun areItemsTheSame(oldItem: MarsProperty, 
                  newItem: MarsProperty): Boolean {
   return oldItem === newItem
}
  1. สําหรับ areContentsTheSame() ให้ใช้ตัวดำเนินการความเท่ากันมาตรฐานกับรหัสของ oldItem และ newItem เท่านั้น
override fun areContentsTheSame(oldItem: MarsProperty, 
                  newItem: MarsProperty): Boolean {
   return oldItem.id == newItem.id
}
  1. ยังคงอยู่ในคลาส PhotoGridAdapter ให้เพิ่มคำจำกัดความคลาสภายในสำหรับ MarsPropertyViewHolder ซึ่งขยาย RecyclerView.ViewHolder

    นำเข้า androidx.recyclerview.widget.RecyclerView และ com.example.android.marsrealestate.databinding.GridViewItemBinding เมื่อระบบขอ

    คุณต้องมีตัวแปร GridViewItemBinding สำหรับการเชื่อมโยง MarsProperty กับเลย์เอาต์ ดังนั้นให้ส่งตัวแปรไปยัง MarsPropertyViewHolder เนื่องจากคลาสพื้นฐาน ViewHolder ต้องมี View ในตัวสร้าง คุณจึงส่ง View รูทของการเชื่อมโยงไปยังคลาส
class MarsPropertyViewHolder(private var binding: 
                   GridViewItemBinding):
       RecyclerView.ViewHolder(binding.root) {

}
  1. ใน MarsPropertyViewHolder ให้สร้างเมธอด bind() ที่รับออบเจ็กต์ MarsProperty เป็นอาร์กิวเมนต์และตั้งค่า binding.property เป็นออบเจ็กต์นั้น เรียกใช้ executePendingBindings() หลังจากตั้งค่าพร็อพเพอร์ตี้ ซึ่งจะทําให้อัปเดตทํางานทันที
fun bind(marsProperty: MarsProperty) {
   binding.property = marsProperty
   binding.executePendingBindings()
}
  1. ใน onCreateViewHolder() ให้นำ TODO ออกแล้วเพิ่มบรรทัดที่แสดงด้านล่าง นำเข้า android.view.LayoutInflater เมื่อได้รับคำขอ

    เมธอด onCreateViewHolder() ต้องส่งกลับ MarsPropertyViewHolder ใหม่ที่สร้างขึ้นโดยการขยาย GridViewItemBinding และใช้ LayoutInflater จากบริบท ViewGroup ขององค์ประกอบระดับบนสุด
   return MarsPropertyViewHolder(GridViewItemBinding.inflate(
      LayoutInflater.from(parent.context)))
  1. ในonBindViewHolder() ให้นำ TODO ออกและเพิ่มบรรทัดที่แสดงด้านล่าง ในที่นี้ คุณเรียกใช้ getItem() เพื่อรับออบเจ็กต์ MarsProperty ที่เชื่อมโยงกับตำแหน่ง RecyclerView ปัจจุบัน จากนั้นส่งพร็อพเพอร์ตี้นั้นไปยังเมธอด bind() ใน MarsPropertyViewHolder
val marsProperty = getItem(position)
holder.bind(marsProperty)

ขั้นตอนที่ 4: ติดตั้ง Binding Adapter และเชื่อมต่อชิ้นส่วน

สุดท้าย ให้ใช้ BindingAdapter เพื่อเริ่มต้น PhotoGridAdapter ด้วยรายการออบเจ็กต์ MarsProperty การใช้ BindingAdapter เพื่อตั้งค่าข้อมูล RecyclerView จะทําให้การเชื่อมโยงข้อมูลสังเกต LiveData โดยอัตโนมัติสําหรับรายการออบเจ็กต์ MarsProperty จากนั้นระบบจะเรียกใช้ Binding Adapter โดยอัตโนมัติเมื่อMarsPropertyมีการเปลี่ยนแปลงรายการ

  1. เปิด BindingAdapters.kt
  2. ที่ท้ายไฟล์ ให้เพิ่มเมธอด bindRecyclerView() ที่รับ RecyclerView และรายการออบเจ็กต์ MarsProperty เป็นอาร์กิวเมนต์ ใส่คำอธิบายประกอบในวิธีการนั้นด้วย @BindingAdapter

    นำเข้า androidx.recyclerview.widget.RecyclerView และ com.example.android.marsrealestate.network.MarsProperty เมื่อได้รับคำขอ
@BindingAdapter("listData")
fun bindRecyclerView(recyclerView: RecyclerView, 
    data: List<MarsProperty>?) {
}
  1. ภายในbindRecyclerView()ฟังก์ชัน ให้แคสต์ recyclerView.adapter เป็น PhotoGridAdapter แล้วเรียกใช้ adapter.submitList() ด้วยข้อมูล ซึ่งจะแจ้งให้ RecyclerView ทราบเมื่อมีรายการใหม่พร้อมใช้งาน

นำเข้า com.example.android.marsrealestate.overview.PhotoGridAdapter เมื่อได้รับคำขอ

val adapter = recyclerView.adapter as PhotoGridAdapter
adapter.submitList(data)
  1. เปิด res/layout/fragment_overview.xml เพิ่มแอตทริบิวต์ app:listData ลงในองค์ประกอบ RecyclerView แล้วตั้งค่าเป็น viewmodel.properties โดยใช้การเชื่อมโยงข้อมูล
app:listData="@{viewModel.properties}"
  1. เปิด overview/OverviewFragment.kt ใน onCreateView() ก่อนที่จะเรียกใช้ setHasOptionsMenu() ให้เริ่มต้นใช้งานอแดปเตอร์ RecyclerView ใน binding.photosGrid เป็นออบเจ็กต์ PhotoGridAdapter ใหม่
binding.photosGrid.adapter = PhotoGridAdapter()
  1. เรียกใช้แอป คุณควรเห็นตารางกริดของรูปภาพ MarsProperty ขณะที่คุณเลื่อนเพื่อดูรูปภาพใหม่ แอปจะแสดงไอคอนความคืบหน้าในการโหลดก่อนที่จะแสดงรูปภาพ หากเปิดโหมดเครื่องบิน รูปภาพที่ยังโหลดไม่เสร็จจะปรากฏเป็นไอคอนรูปภาพเสีย

แอป MarsRealEstate จะแสดงไอคอนรูปภาพเสียเมื่อดึงข้อมูลรูปภาพไม่ได้ แต่เมื่อไม่มีเครือข่าย แอปจะแสดงหน้าจอว่างเปล่า

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

ขั้นตอนที่ 1: เพิ่มสถานะไปยัง ViewModel

ในการเริ่มต้น ให้สร้าง LiveData ใน ViewModel เพื่อแสดงสถานะของคำขอเว็บ โดยมี 3 สถานะที่ต้องพิจารณา ได้แก่ กำลังโหลด สำเร็จ และไม่สำเร็จ สถานะการโหลดจะเกิดขึ้นขณะที่คุณรอข้อมูลในการเรียกไปยัง await()

  1. เปิด overview/OverviewViewModel.kt ที่ด้านบนของไฟล์ (หลังการนำเข้า ก่อนคำจำกัดความของคลาส) ให้เพิ่ม enum เพื่อแสดงสถานะทั้งหมดที่ใช้ได้
enum class MarsApiStatus { LOADING, ERROR, DONE }
  1. เปลี่ยนชื่อทั้ง_responseคำจำกัดความของข้อมูลแบบเรียลไทม์ทั้งภายในและภายนอกตลอดทั้งคลาส OverviewViewModel เป็น _status เนื่องจากคุณได้เพิ่มการรองรับ _properties LiveData ใน Codelab นี้ไปก่อนหน้านี้แล้ว การตอบสนองของบริการเว็บที่สมบูรณ์จึงไม่ได้ใช้ คุณต้องมี LiveData ที่นี่เพื่อติดตามสถานะปัจจุบัน จึงเปลี่ยนชื่อตัวแปรที่มีอยู่ได้เลย

นอกจากนี้ ให้เปลี่ยนประเภทจาก String เป็น MarsApiStatus.

private val _status = MutableLiveData<MarsApiStatus>()

val status: LiveData<MarsApiStatus>
   get() = _status
  1. เลื่อนลงไปที่getMarsRealEstateProperties()วิธีการ_responseและอัปเดตเป็น_statusที่นี่ด้วย เปลี่ยนสตริง "Success" เป็นสถานะ MarsApiStatus.DONE และสตริง "Failure" เป็น MarsApiStatus.ERROR
  2. เพิ่มMarsApiStatus.LOADINGสถานะที่ด้านบนของtry {}บล็อกก่อนการเรียกใช้ await() นี่คือสถานะเริ่มต้นขณะที่โครูทีนทํางานและคุณกําลังรอข้อมูล ตอนนี้บล็อก try/catch {} ที่สมบูรณ์จะมีลักษณะดังนี้
try {
    _status.value = MarsApiStatus.LOADING
   var listResult = getPropertiesDeferred.await()
   _status.value = MarsApiStatus.DONE
   _properties.value = listResult
} catch (e: Exception) {
   _status.value = MarsApiStatus.ERROR
}
  1. หลังจากสถานะข้อผิดพลาดในบล็อก catch {} ให้ตั้งค่า _properties LiveData เป็นรายการที่ว่างเปล่า การดำเนินการนี้จะล้างRecyclerView
} catch (e: Exception) {
   _status.value = MarsApiStatus.ERROR
   _properties.value = ArrayList()
}

ขั้นตอนที่ 2: เพิ่ม Binding Adapter สำหรับ ImageView ของสถานะ

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

  1. เปิด BindingAdapters.kt เพิ่ม Binding Adapter ใหม่ชื่อ bindStatus() ซึ่งรับค่า ImageView และ MarsApiStatus เป็นอาร์กิวเมนต์ นำเข้า com.example.android.marsrealestate.overview.MarsApiStatus เมื่อได้รับคำขอ
@BindingAdapter("marsApiStatus")
fun bindStatus(statusImageView: ImageView, 
          status: MarsApiStatus?) {
}
  1. เพิ่ม when {} ภายในเมธอด bindStatus() เพื่อสลับระหว่างสถานะต่างๆ
when (status) {

}
  1. ภายใน when {} ให้เพิ่มเคสสำหรับสถานะการโหลด (MarsApiStatus.LOADING) สำหรับสถานะนี้ ให้ตั้งค่า ImageView เป็น "มองเห็นได้" และกำหนดภาพเคลื่อนไหวการโหลดให้ ซึ่งเป็นภาพเคลื่อนไหวที่วาดได้แบบเดียวกับที่คุณใช้สำหรับ Glide ในงานก่อนหน้า นำเข้า android.view.View เมื่อได้รับคำขอ
when (status) {
   MarsApiStatus.LOADING -> {
      statusImageView.visibility = View.VISIBLE
      statusImageView.setImageResource(R.drawable.loading_animation)
   }
}
  1. เพิ่มเคสสำหรับสถานะข้อผิดพลาด ซึ่งคือ MarsApiStatus.ERROR เช่นเดียวกับที่คุณทำสำหรับสถานะ LOADING ให้ตั้งค่าสถานะ ImageView เป็น "มองเห็นได้" และใช้ภาพวาดข้อผิดพลาดในการเชื่อมต่อซ้ำ
MarsApiStatus.ERROR -> {
   statusImageView.visibility = View.VISIBLE
   statusImageView.setImageResource(R.drawable.ic_connection_error)
}
  1. เพิ่มเคสสำหรับสถานะเสร็จสิ้น ซึ่งก็คือ MarsApiStatus.DONE คุณได้รับการตอบกลับที่สำเร็จแล้ว ดังนั้นให้ปิดระดับการแชร์ของสถานะ ImageView เพื่อซ่อน
MarsApiStatus.DONE -> {
   statusImageView.visibility = View.GONE
}

ขั้นตอนที่ 3: เพิ่ม ImageView สถานะลงในเลย์เอาต์

  1. เปิด res/layout/fragment_overview.xml ใต้องค์ประกอบ RecyclerView ภายใน ConstraintLayout ให้เพิ่ม ImageView ที่แสดงด้านล่าง

    ImageView นี้มีข้อจำกัดเดียวกันกับ RecyclerView อย่างไรก็ตาม ความกว้างและความสูงจะใช้ wrap_content เพื่อจัดกึ่งกลางรูปภาพแทนที่จะยืดรูปภาพให้เต็มมุมมอง นอกจากนี้ ให้สังเกตแอตทริบิวต์ app:marsApiStatus ซึ่งมีการเรียกดู BindingAdapter เมื่อพร็อพเพอร์ตี้สถานะในโมเดลมุมมองเปลี่ยนแปลง
<ImageView
   android:id="@+id/status_image"
    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"
    app:marsApiStatus="@{viewModel.status}" />
  1. เปิดโหมดบนเครื่องบินในโปรแกรมจำลองหรืออุปกรณ์เพื่อจำลองการเชื่อมต่อเครือข่ายที่ขาดหายไป คอมไพล์และเรียกใช้แอป แล้วสังเกตว่ารูปภาพข้อผิดพลาดปรากฏขึ้น

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

โปรเจ็กต์ Android Studio: MarsRealEstateGrid

  • หากต้องการลดความซับซ้อนของกระบวนการจัดการรูปภาพ ให้ใช้ไลบรารี Glide เพื่อดาวน์โหลด บัฟเฟอร์ ถอดรหัส และแคชรูปภาพในแอป
  • Glide ต้องมี 2 สิ่งในการโหลดรูปภาพจากอินเทอร์เน็ต ได้แก่ URL ของรูปภาพ และImageViewออบเจ็กต์ที่จะใส่รูปภาพ หากต้องการระบุตัวเลือกเหล่านี้ ให้ใช้วิธี load() และ into() กับ Glide
  • Binding Adapters คือเมธอดส่วนขยายที่อยู่ระหว่าง View กับข้อมูลที่ผูกไว้ของ View นั้น Binding Adapter จะให้ลักษณะการทำงานที่กำหนดเองเมื่อข้อมูลเปลี่ยนแปลง เช่น เรียกใช้ Glide เพื่อโหลดรูปภาพจาก URL ลงใน ImageView
  • Binding Adapter เป็นเมธอดส่วนขยายที่มีคำอธิบายประกอบด้วยคำอธิบายประกอบ @BindingAdapter
  • หากต้องการเพิ่มตัวเลือกในคำขอ Glide ให้ใช้วิธี apply() เช่น ใช้ apply() กับ placeholder() เพื่อระบุ Drawable ที่โหลด และใช้ apply() กับ error() เพื่อระบุ Drawable ข้อผิดพลาด
  • หากต้องการสร้างตารางกริดของรูปภาพ ให้ใช้ RecyclerView ที่มี GridLayoutManager
  • หากต้องการอัปเดตรายการพร็อพเพอร์ตี้เมื่อมีการเปลี่ยนแปลง ให้ใช้ Binding Adapter ระหว่าง RecyclerView กับเลย์เอาต์

หลักสูตร Udacity:

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

อื่นๆ:

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

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

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

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

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

คำถามที่ 1

คุณใช้วิธีการใดของ Glide เพื่อระบุ ImageView ที่จะมีรูปภาพที่โหลด

into()

with()

imageview()

apply()

คำถามที่ 2

คุณจะระบุรูปภาพตัวยึดตำแหน่งที่จะแสดงเมื่อ Glide กำลังโหลดได้อย่างไร

▢ ใช้เมธอด into() กับ Drawable

▢ ใช้ RequestOptions() และเรียกใช้เมธอด placeholder() ด้วย Drawable

▢ กำหนดพร็อพเพอร์ตี้ Glide.placeholder ให้กับ Drawable

▢ ใช้ RequestOptions() และเรียกใช้เมธอด loadingImage() ด้วย Drawable

คำถามที่ 3

คุณจะระบุว่าเมธอดเป็น BindingAdapter ได้อย่างไร

▢ เรียกใช้เมธอด setBindingAdapter() ใน LiveData

▢ ใส่เมธอดลงในไฟล์ Kotlin ชื่อ BindingAdapters.kt

▢ ใช้แอตทริบิวต์ android:adapter ในเลย์เอาต์ XML

▢ ใส่คำอธิบายประกอบในเมธอดด้วย @BindingAdapter

เริ่มบทเรียนถัดไป: 8.3 การกรองและมุมมองรายละเอียดด้วยข้อมูลอินเทอร์เน็ต

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