เลเยอร์ UI

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

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

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

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

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

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

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

สถาปัตยกรรมชั้น UI

คำว่า UI หมายถึงองค์ประกอบ UI เช่น คอนเทนเนอร์และฟังก์ชันที่ประกอบกันได้ซึ่งแสดงข้อมูล Jetpack Compose เป็นชุดเครื่องมือที่แนะนำสำหรับการสร้าง UI ของ Android เนื่องจากบทบาทของชั้นข้อมูลคือการเก็บ จัดการ และให้สิทธิ์เข้าถึงข้อมูลแอป ชั้น 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 ในคลาส Activity แฟล็กดังกล่าวจะแข่งขันกับชั้นข้อมูลในฐานะแหล่งที่มาของสถานะบุ๊กมาร์กของบทความ คลาสข้อมูลที่เปลี่ยนแปลงไม่ได้มีประโยชน์มากในการป้องกันความไม่สอดคล้องกันประเภทนี้

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

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

ฟังก์ชันการทำงาน + UiState

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

จัดการสถานะด้วย Unidirectional Data Flow

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

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

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

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

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

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

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

ข้อมูลแอปพลิเคชันจะไหลจากชั้นข้อมูลไปยัง 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, การไปยังหน้าจอหนึ่งๆ เมื่อผู้ใช้คลิกปุ่ม หรือการแสดงข้อความของผู้ใช้บนหน้าจอโดยใช้ Toast หรือ Snackbar

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

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

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

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

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

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

แสดงสถานะ UI

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

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

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

    val uiState: NewsUiState = /* ... */
}

ดูข้อมูลเบื้องต้นเกี่ยวกับ Kotlin Flow ได้ที่ Kotlin Flow ใน 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 Flow ให้ใช้เมธอด collect() หรือรูปแบบต่างๆ ของเมธอดนี้

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

@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,
    // ...
)

ค่าของ Flag นี้แสดงถึงการมีหรือไม่มีแถบความคืบหน้าใน 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 เช่น Snackbar ดูข้อมูลเพิ่มเติมเกี่ยวกับวิธีสร้างและใช้เหตุการณ์ UI ได้ที่เหตุการณ์ UI

ชุดข้อความและการเกิดขึ้นพร้อมกัน

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

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

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

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

เพจจิ้ง

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

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

@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 ลองสำรวจแหล่งข้อมูลต่อไปนี้เพื่อดูตัวอย่างการนำคำแนะนำนี้ไปใช้จริง