เลเยอร์ UI

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

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

ในสถาปัตยกรรมทั่วไป องค์ประกอบ UI ของเลเยอร์ UI จะขึ้นอยู่กับตัวยึดสถานะ ซึ่งจะขึ้นอยู่กับคลาสจากเลเยอร์ข้อมูลหรือเลเยอร์โดเมนที่ไม่บังคับ
รูปที่ 1 บทบาทของเลเยอร์ UI ในสถาปัตยกรรมแอป

กรณีศึกษาพื้นฐาน

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

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

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

สถาปัตยกรรมเลเยอร์ UI

คำว่า UI หมายถึงองค์ประกอบ UI เช่น คอนเทนเนอร์และฟังก์ชัน Composable ที่แสดงข้อมูล สำหรับสร้าง UI ของ Android Jetpack Compose เป็นชุดเครื่องมือที่แนะนำ เนื่องจากบทบาทของชั้นข้อมูลคือการจัดเก็บ จัดการ และให้สิทธิ์เข้าถึงข้อมูลแอป ชั้น UI จึงต้องทำตามขั้นตอนต่อไปนี้

  1. ใช้ข้อมูลแอปและเปลี่ยนเป็นข้อมูลที่ UI แสดงผลได้ง่าย
  2. ใช้ข้อมูลที่แสดงผลได้ใน UI และเปลี่ยนเป็นองค์ประกอบ UI เพื่อนำเสนอ ต่อผู้ใช้
  3. ใช้เหตุการณ์อินพุตของข้อมูลจากผู้ใช้จากองค์ประกอบ UI ที่ประกอบขึ้นเหล่านั้น และแสดงผลลัพธ์ในข้อมูล UI ตามที่จำเป็น
  4. ทำขั้นตอนที่ 1-3 ซ้ำตามที่จำเป็น

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

  • วิธีกำหนดสถานะ UI
  • โฟลว์ข้อมูลแบบทางเดียว (UDF) เป็นวิธีการสร้างและจัดการสถานะ UI
  • วิธีแสดงสถานะ UI ด้วยประเภทข้อมูลที่สังเกตได้ตามหลักการของ UDF
  • วิธีใช้ UI ที่ใช้สถานะ UI ที่สังเกตได้

ซึ่งพื้นฐานที่สุดคือคำจำกัดความของสถานะ UI

กำหนดสถานะ UI

ในกรณีศึกษาที่กล่าวถึงก่อนหน้านี้ UI จะแสดงรายการบทความพร้อมข้อมูลเมตาบางอย่างของแต่ละบทความ ข้อมูลนี้ ที่แอปแสดงต่อผู้ใช้คือสถานะ UI

กล่าวคือ หาก UI คือสิ่งที่ผู้ใช้เห็น สถานะ UI คือสิ่งที่แอป บอกว่าผู้ใช้ควรเห็น UI เป็นการแสดงภาพของสถานะ UI เช่นเดียวกับเหรียญที่มี 2 ด้าน การเปลี่ยนแปลงสถานะ UI จะแสดงใน UI โดยทันที

UI เป็นผลมาจากการเชื่อมโยงองค์ประกอบ UI บนหน้าจอเข้ากับสถานะ UI
รูปที่ 3 UI เป็นผลมาจากการเชื่อมโยงองค์ประกอบ UI บนหน้าจอเข้ากับ สถานะ UI

พิจารณากรณีศึกษา: เพื่อให้เป็นไปตามข้อกำหนดของแอปข่าวสาร ข้อมูลที่จำเป็นต่อการแสดงผล UI อย่างเต็มรูปแบบสามารถห่อหุ้มใน NewsUiState คลาสข้อมูลที่กำหนดไว้ดังนี้

data class NewsUiState(
    val isSignedIn: Boolean = false,
    val isPremium: Boolean = false,
    val newsItems: List<NewsItemUiState> = listOf(),
    val userMessages: List<Message> = listOf()
)

data class NewsItemUiState(
    val title: String,
    val body: String,
    val bookmarked: Boolean = false,
    ...
)

ดูข้อมูลเพิ่มเติมเกี่ยวกับสถานะ UI ได้ที่สถานะและ Jetpack Compose

ความเปลี่ยนแปลงไม่ได้

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

เช่น ลองดูกรณีศึกษาที่กล่าวถึงก่อนหน้านี้ หากมีการอัปเดตbookmarkedแฟล็กในออบเจ็กต์ NewsItemUiState จาก UI state ในคลาส Activity แฟล็กนั้น จะแข่งขันกับชั้นข้อมูลในฐานะแหล่งที่มาของสถานะบุ๊กมาร์กของบทความ คลาสข้อมูลที่ไม่เปลี่ยนแปลงมีประโยชน์อย่างยิ่งในการป้องกันความไม่สอดคล้องประเภทนี้

แบบแผนการตั้งชื่อในคู่มือนี้

ในคู่มือนี้ คลาสสถานะ UI จะตั้งชื่อตามฟังก์ชันการทำงานของ หน้าจอหรือส่วนของหน้าจอที่อธิบาย โดยมีรูปแบบดังนี้

functionality + UiState

เช่น สถานะของหน้าจอที่แสดงข่าวอาจเรียกว่า NewsUiState และสถานะของรายการข่าวในรายการข่าวอาจเป็น NewsItemUiState

จัดการสถานะด้วยการไหลของข้อมูลแบบทิศทางเดียว

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

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

ส่วนนี้จะอธิบายถึงการไหลของข้อมูลแบบทิศทางเดียว (UDF) ซึ่งเป็นรูปแบบสถาปัตยกรรม ที่ช่วยบังคับใช้การแยกความรับผิดชอบที่เหมาะสมนี้

ตัวเก็บสถานะ

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

ในกรณีหลัง การใช้งานทั่วไปคืออินสแตนซ์ของ ViewModel แม้ว่าคลาสอย่างง่ายอาจเพียงพอแล้วก็ตาม ทั้งนี้ขึ้นอยู่กับข้อกำหนดของแอปพลิเคชัน ตัวอย่างเช่น แอปข่าวจากกรณีศึกษาใช้คลาส NewsViewModel เป็นตัวเก็บสถานะเพื่อสร้างสถานะ UI สำหรับหน้าจอที่แสดงในส่วนนั้น

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

ข้อมูลแอปพลิเคชันจะไหลจากชั้นข้อมูลไปยัง ViewModel สถานะ UI
    ไหลจาก ViewModel ไปยังองค์ประกอบ UI และเหตุการณ์ไหลจากองค์ประกอบ UI
    กลับไปยัง ViewModel
รูปที่ 4 แผนภาพแสดงวิธีการทำงานของ UDF ในสถาปัตยกรรมแอป

รูปแบบที่สถานะไหลลงและเหตุการณ์ไหลขึ้นเรียกว่า โฟลว์ข้อมูลแบบทิศทางเดียว (UDF) ผลกระทบของรูปแบบนี้ต่อสถาปัตยกรรมของแอป มีดังนี้

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

สำหรับปลายทางหรือหน้าจอการนำทาง ViewModel จะทำงานร่วมกับที่เก็บหรือคลาส Use Case เพื่อรับข้อมูลและเปลี่ยนเป็นสถานะ UI พร้อมทั้งรวมผลกระทบของเหตุการณ์ที่อาจทำให้เกิดการเปลี่ยนแปลงสถานะ กรณีศึกษาที่กล่าวถึงก่อนหน้านี้มีรายการบทความ ซึ่งแต่ละบทความมีชื่อ คำอธิบาย แหล่งที่มา ชื่อผู้เขียน วันที่ตีพิมพ์ และระบุว่ามีการบุ๊กมาร์กหรือไม่ UI สำหรับรายการบทความแต่ละรายการมีลักษณะดังนี้

รายการบทความเดียวจากแอปกรณีศึกษา UI แสดงภาพขนาดย่อ ชื่อบทความ ผู้เขียน เวลาอ่านโดยประมาณของบทความ และไอคอนบุ๊กมาร์ก
รูปที่ 5 UI ของรายการบทความในแอปกรณีศึกษา

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

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

ส่วนต่อไปนี้จะเจาะลึกเหตุการณ์ที่ทำให้เกิดการเปลี่ยนแปลงสถานะ และวิธีประมวลผลเหตุการณ์โดยใช้ UDF

ประเภทของตรรกะ

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

  • ตรรกะทางธุรกิจคือการนำข้อกำหนดของผลิตภัณฑ์ไปใช้กับข้อมูลแอป ดังที่ได้กล่าวไปแล้ว ตัวอย่างหนึ่งคือการบุ๊กมาร์กบทความใน แอปกรณีศึกษา โดยปกติแล้วตรรกะทางธุรกิจจะอยู่ในเลเยอร์โดเมนหรือเลเยอร์ข้อมูล แต่จะไม่อยู่ในเลเยอร์ UI
  • ตรรกะลักษณะการทำงานของ UI หรือตรรกะ UI คือวิธีแสดงการเปลี่ยนแปลงสถานะบน หน้าจอ ตัวอย่างเช่น การรับข้อความที่เหมาะสมเพื่อแสดงบนหน้าจอ โดยใช้ Resources ของ Android การไปยังหน้าจอหนึ่งๆ เมื่อผู้ใช้คลิกปุ่ม หรือ การแสดงข้อความของผู้ใช้บนหน้าจอโดยใช้ ข้อความโทสต์หรือ แถบแสดงข้อความ

เก็บตรรกะ UI ไว้ใน UI ไม่ใช่ใน ViewModel โดยเฉพาะเมื่อเกี่ยวข้องกับ ประเภท UI เช่น Context หาก UI มีความซับซ้อนมากขึ้นและคุณต้องการมอบสิทธิ์ตรรกะ UI ให้กับคลาสอื่นเพื่อเพิ่มความสามารถในการทดสอบและการแยกความกังวล คุณสามารถสร้างคลาสอย่างง่ายเพื่อใช้เป็นตัวเก็บสถานะ คลาสอย่างง่ายที่สร้างขึ้นใน UI สามารถใช้การอ้างอิง Android SDK ได้เนื่องจากเป็นไปตามวงจรของ UI ส่วนออบเจ็กต์ ViewModel มีอายุการใช้งานยาวนานกว่า

ดูข้อมูลเพิ่มเติมเกี่ยวกับตัวเก็บสถานะและวิธีที่ตัวเก็บสถานะเข้ากับบริบทของการช่วยสร้าง UI ได้ที่คู่มือสถานะ Jetpack Compose

เหตุผลที่ควรใช้ UDF

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

กล่าวคือ UDF อนุญาตให้ทำสิ่งต่อไปนี้

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

เปิดเผยสถานะ UI

หลังจากกำหนดสถานะ UI และพิจารณาว่าจะจัดการการสร้างสถานะดังกล่าวอย่างไร ขั้นตอนถัดไปคือการนำเสนอสถานะที่สร้างขึ้นต่อ UI

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

class NewsViewModel(...) : ViewModel() {

    val uiState: NewsUiState = 
}

ดูข้อมูลเบื้องต้นเกี่ยวกับโฟลว์ Kotlin ได้ที่โฟลว์ Kotlin ใน Android ดูวิธีใช้ StateFlow เป็นที่เก็บข้อมูลที่สังเกตได้ ใน Codelab สถานะขั้นสูงและผลสะท้อนใน Jetpack Compose

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

วิธีทั่วไปในการสร้างสตรีมของ UiState คือการเปิดเผยmutableStateOf พร็อพเพอร์ตี้ที่มี private set โดยรักษาสถานะให้เปลี่ยนแปลงได้ภายใน ViewModel แต่เป็นแบบอ่านอย่างเดียวสำหรับ UI

class NewsViewModel(...) : ViewModel() {

    var uiState by mutableStateOf(NewsUiState())
        private set

    ...
}

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

class NewsViewModel(
    private val repository: NewsRepository,
    ...
) : ViewModel() {

    var uiState by mutableStateOf(NewsUiState())
        private set

    private var fetchJob: Job? = null

    fun fetchArticles(category: String) {
        fetchJob?.cancel()
        fetchJob = viewModelScope.launch {
            try {
                val newsItems = repository.newsItemsForCategory(category)
                uiState = uiState.copy(newsItems = newsItems)
            } catch (ioe: IOException) {
                // Handle the error and notify the UI when appropriate.
                val messages = getMessagesFromThrowable(ioe)
                uiState = uiState.copy(userMessages = messages)
            }
        }
    }
}

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

ข้อพิจารณาเพิ่มเติม

นอกเหนือจากคำแนะนำก่อนหน้านี้ ให้พิจารณาสิ่งต่อไปนี้เมื่อแสดงสถานะ UI

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

    data class NewsUiState(
        val isSignedIn: Boolean = false,
        val isPremium: Boolean = false,
        val newsItems: List<NewsItemUiState> = listOf()
    )
    
    val NewsUiState.canBookmarkNews: Boolean get() = isSignedIn && isPremium
    

    ในการประกาศนี้ ระดับการมองเห็นของปุ่มบุ๊กมาร์กเป็นพร็อพเพอร์ตี้ที่ดึงมาจากพร็อพเพอร์ตี้อื่นๆ 2 รายการ เมื่อตรรกะทางธุรกิจมีความซับซ้อนมากขึ้น การมีUiState คลาสเดียวที่พร็อพเพอร์ตี้ทั้งหมดพร้อมใช้งานทันที จึงมีความสำคัญมากขึ้น

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

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

    • UiStateการเปรียบเทียบ: ยิ่งมีฟิลด์ในออบเจ็กต์ UiState มากเท่าใด สตรีมก็จะยิ่งมีแนวโน้มที่จะปล่อยออกมาเป็นผลจากการอัปเดตฟิลด์ใดฟิลด์หนึ่ง มากขึ้นเท่านั้น เนื่องจากองค์ประกอบ UI ไม่มีกลไกการเปรียบเทียบเพื่อทำความเข้าใจว่าการปล่อยสัญญาณที่ต่อเนื่องนั้นแตกต่างกันหรือเหมือนกัน การปล่อยสัญญาณทุกครั้งจึงทำให้องค์ประกอบ UI ได้รับการอัปเดต ซึ่งหมายความว่าการลดผลกระทบโดยใช้ Flow เมธอด API เช่น distinctUntilChanged() อาจเป็นสิ่งจำเป็น

ดูข้อมูลเพิ่มเติมเกี่ยวกับการแสดงผลและสถานะ UI ได้ที่วงจรของ Composable

ใช้สถานะ UI

หากต้องการใช้สตรีมของออบเจ็กต์ UiState ใน UI ให้ใช้ตัวดำเนินการเทอร์มินัลสำหรับประเภทข้อมูลที่ได้รับอนุญาตให้สังเกตพฤติกรรมผู้ใช้ได้ที่คุณใช้ เช่น สำหรับโฟลว์ Kotlin ให้ใช้วิธี collect() หรือรูปแบบต่างๆ ของวิธีนี้

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

@Composable
private fun ConversationScreen(
    conversationViewModel: ConversationViewModel = viewModel()
) {

    val messages by conversationViewModel.messages.collectAsStateWithLifecycle()

    ConversationScreen(
        messages = messages,
        onSendMessage = { message: Message -> conversationViewModel.sendMessage(message) }
    )
}

@Composable
private fun ConversationScreen(
    messages: List<Message>,
    onSendMessage: (Message) -> Unit
) {

    MessagesList(messages, onSendMessage)
    /* ... */
}

แสดงการดำเนินการที่อยู่ระหว่างดำเนินการ

วิธีง่ายๆ ในการแสดงสถานะการโหลดในคลาส UiState คือการใช้ฟิลด์บูลีน

data class NewsUiState(
    val isFetchingArticles: Boolean = false,
    ...
)

ค่าของแฟล็กนี้แสดงถึงการมีหรือไม่มีแถบความคืบหน้าใน UI

@Composable
fun LatestNewsScreen(
    modifier: Modifier = Modifier,
    viewModel: NewsViewModel = viewModel()
) {
    Box(modifier.fillMaxSize()) {

        if (viewModel.uiState.isFetchingArticles) {
            CircularProgressIndicator(Modifier.align(Alignment.Center))
        }

        // Add other UI elements. For example, the list.
    }
}

แสดงข้อผิดพลาดบนหน้าจอ

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

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

data class Message(val id: Long, val message: String)

data class NewsUiState(
    val userMessages: List<Message> = listOf(),
    ...
)

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

การแยกเธรดและการทำงานพร้อมกัน

ตรวจสอบว่างานทั้งหมดที่ดำเนินการใน ViewModel เป็นเทรดหลักที่ปลอดภัย ซึ่งเรียกใช้จากเทรดหลักได้อย่างปลอดภัย เลเยอร์ข้อมูลและโดเมนมีหน้าที่ ย้ายงานไปยังเธรดอื่น

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

การเปลี่ยนแปลงในการนำทางของแอปมักเกิดจากการปล่อยก๊าซที่คล้ายกับเหตุการณ์ เช่น หลังจากSignInViewModelชั้นเรียนเข้าสู่ระบบแล้ว UiState อาจมี ฟิลด์ isSignedIn ตั้งค่าเป็น true ใช้ทริกเกอร์เช่นนี้เหมือนกับที่กล่าวถึงในส่วนสถานะ UI การใช้ก่อนหน้านี้ แต่เลื่อนการติดตั้งใช้งานการใช้ไปยังคอมโพเนนต์การนำทาง

ดูข้อมูลเพิ่มเติมเกี่ยวกับการนำทาง UI ได้ที่การนำทาง 3

การแบ่งหน้า

ไลบรารีการแบ่งหน้าจะ ใช้ใน UI ด้วยประเภทที่เรียกว่า PagingData เนื่องจาก PagingData แสดงและมีรายการที่เปลี่ยนแปลงได้เมื่อเวลาผ่านไป กล่าวคือ PagingData ไม่ใช่ประเภทที่ไม่เปลี่ยนแปลง จึงไม่ควรแสดงในสถานะ UI ที่ไม่เปลี่ยนแปลง แต่ให้แสดงจาก ViewModel แยกกันในสตรีมของตัวเอง

ตัวอย่างต่อไปนี้แสดง Compose API ของไลบรารีการแบ่งหน้า

@Composable
fun MyScreen(flow: Flow<PagingData<String>>) {
    val lazyPagingItems = flow.collectAsLazyPagingItems()
    LazyColumn {
        items(
            lazyPagingItems.itemCount,
            key = lazyPagingItems.itemKey { it }
        ) { index ->
            val item = lazyPagingItems[index]
            Text("Item is $item")
        }
    }
}

ภาพเคลื่อนไหว

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

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

แหล่งข้อมูลเพิ่มเติม

ดูเนื้อหา

ตัวอย่าง

ตัวอย่างต่อไปนี้ของ Google แสดงการใช้เลเยอร์ UI ลองสำรวจเพื่อดูคำแนะนำนี้ในทางปฏิบัติ