บทบาทของ UI คือการแสดงข้อมูลแอปพลิเคชันบนหน้าจอ นอกจากนี้ UI ยังทำหน้าที่เป็นจุดหลักในการโต้ตอบของผู้ใช้ด้วย เมื่อใดก็ตามที่ข้อมูลมีการเปลี่ยนแปลง ไม่ว่าจะเกิดจากการโต้ตอบของผู้ใช้ (เช่น การกดปุ่ม) หรืออินพุตภายนอก (เช่น การตอบสนองของเครือข่าย) UI จะอัปเดตเพื่อแสดงการเปลี่ยนแปลงเหล่านั้น โดยพื้นฐานแล้ว UI คือการแสดงภาพสถานะของแอปพลิเคชันตามที่ดึงมาจากชั้นข้อมูล
อย่างไรก็ตาม ข้อมูลแอปพลิเคชันที่คุณได้รับจากชั้นข้อมูลมักจะอยู่ในรูปแบบที่แตกต่างจากข้อมูลที่คุณต้องการแสดง เช่น คุณอาจต้องการข้อมูลเพียงบางส่วนสำหรับ UI หรืออาจต้องผสานรวมแหล่งข้อมูล 2 แหล่งที่แตกต่างกันเพื่อนำเสนอข้อมูลที่เกี่ยวข้องกับผู้ใช้ ไม่ว่าจะใช้ตรรกะใด คุณต้องส่งข้อมูลทั้งหมดที่จำเป็นต่อการแสดงผลอย่างสมบูรณ์ไปยัง UI เลเยอร์ UI คือไปป์ไลน์ที่แปลงการเปลี่ยนแปลงข้อมูลแอปพลิเคชันเป็นรูปแบบที่ UI แสดงได้ จากนั้นจึงแสดงข้อมูลดังกล่าว
กรณีศึกษาพื้นฐาน
ลองพิจารณาแอปที่ดึงข้อมูลบทความข่าวให้ผู้ใช้อ่าน แอปมีหน้าจอบทความที่แสดงบทความที่พร้อมให้อ่าน และยังช่วยให้ผู้ใช้ที่ลงชื่อเข้าใช้บุ๊กมาร์กบทความที่โดดเด่นจริงๆ ได้ด้วย เนื่องจากอาจมีบทความจำนวนมากในเวลาใดก็ตาม ผู้อ่านจึงต้องสามารถเรียกดูบทความตามหมวดหมู่ได้ โดยสรุปแล้ว แอปนี้ช่วยให้ผู้ใช้ทำสิ่งต่อไปนี้ได้
- ดูบทความที่พร้อมให้อ่าน
- เรียกดูบทความตามหมวดหมู่
- ลงชื่อเข้าใช้และบุ๊กมาร์กบทความบางรายการ
- เข้าถึงฟีเจอร์พรีเมียมบางรายการหากมีสิทธิ์
ส่วนต่อไปนี้จะใช้ตัวอย่างนี้เป็นกรณีศึกษาเพื่อแนะนำหลักการของโฟลว์ข้อมูลแบบทิศทางเดียว รวมถึงแสดงปัญหาที่หลักการเหล่านี้ช่วยแก้ไขในบริบทของสถาปัตยกรรมแอปสำหรับเลเยอร์ UI
สถาปัตยกรรมชั้น UI
คำว่า UI หมายถึงองค์ประกอบ UI เช่น คอนเทนเนอร์และฟังก์ชันที่ประกอบกันได้ซึ่งแสดงข้อมูล Jetpack Compose เป็นชุดเครื่องมือที่แนะนำสำหรับการสร้าง UI ของ Android เนื่องจากบทบาทของชั้นข้อมูลคือการเก็บ จัดการ และให้สิทธิ์เข้าถึงข้อมูลแอป ชั้น UI จึงต้องทำตามขั้นตอนต่อไปนี้
- ใช้ข้อมูลแอปและเปลี่ยนเป็นข้อมูลที่ UI แสดงผลได้ง่าย
- ใช้ข้อมูลที่แสดงผลได้ใน UI และเปลี่ยนรูปแบบเป็นองค์ประกอบ UI เพื่อนำเสนอต่อผู้ใช้
- ใช้เหตุการณ์การป้อนข้อมูลของผู้ใช้จากองค์ประกอบ UI ที่ประกอบขึ้นเหล่านั้น และแสดงผลลัพธ์ในข้อมูล UI ตามที่จำเป็น
- ทำขั้นตอนที่ 1-3 ซ้ำตามความจำเป็น
ส่วนที่เหลือของคู่มือนี้จะสาธิตวิธีใช้เลเยอร์ UI ที่ทำตามขั้นตอนเหล่านี้ โดยเฉพาะอย่างยิ่ง คู่มือนี้ครอบคลุมงานและแนวคิดต่อไปนี้
- วิธีกำหนดสถานะ UI
- โฟลว์ข้อมูลแบบทิศทางเดียว (UDF) เป็นวิธีการสร้างและจัดการสถานะ UI
- วิธีการแสดงสถานะ UI ด้วยข้อมูลที่ได้รับอนุญาตให้สังเกตพฤติกรรมผู้ใช้ได้ตามหลักการ UDF
- วิธีใช้ UI ที่ใช้สถานะ UI ที่สังเกตได้
สิ่งที่สำคัญที่สุดคือคำจำกัดความของสถานะ UI
กำหนดสถานะ UI
ในกรณีศึกษาที่ระบุไว้ก่อนหน้านี้ UI จะแสดงรายการบทความพร้อมกับข้อมูลเมตาบางอย่างสำหรับแต่ละบทความ ข้อมูลที่แอปแสดงต่อผู้ใช้นี้คือสถานะ UI
กล่าวคือ หาก UI คือสิ่งที่ผู้ใช้เห็น สถานะ UI ก็คือสิ่งที่แอปบอกว่าผู้ใช้ควรเห็น UI เป็นการแสดงภาพของสถานะ UI ซึ่งเปรียบเหมือนเหรียญที่มี 2 ด้าน การเปลี่ยนแปลงสถานะ 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
สามารถเข้าใจได้ว่าเป็นอินพุตเหตุการณ์และเอาต์พุตสถานะที่ตามมา
ความสัมพันธ์จึงแสดงได้ดังที่แสดงในแผนภาพต่อไปนี้
รูปแบบที่สถานะไหลลงและเหตุการณ์ไหลขึ้นเรียกว่าการไหลของข้อมูลแบบทิศทางเดียว (UDF) ผลกระทบของรูปแบบนี้ต่อสถาปัตยกรรมแอปมีดังนี้
- ViewModel จะเก็บและแสดงสถานะที่ UI จะใช้ สถานะ UI คือข้อมูลแอปพลิเคชันที่แปลงโดย ViewModel
- UI จะแจ้ง ViewModel เกี่ยวกับเหตุการณ์ของผู้ใช้
- ViewModel จะจัดการการดำเนินการของผู้ใช้และอัปเดตสถานะ
- ระบบจะป้อนสถานะที่อัปเดตกลับไปยัง UI เพื่อแสดงผล
- ระบบจะทำขั้นตอนข้างต้นซ้ำสำหรับเหตุการณ์ที่ทำให้เกิดการเปลี่ยนแปลงสถานะ
สำหรับปลายทางหรือหน้าจอการนำทาง ViewModel จะทำงานร่วมกับที่เก็บหรือคลาส Use Case เพื่อรับข้อมูลและเปลี่ยนรูปแบบเป็นสถานะ UI พร้อมกับรวมผลกระทบของเหตุการณ์ที่อาจทำให้เกิดการเปลี่ยนแปลงสถานะ กรณีศึกษาที่กล่าวถึงก่อนหน้านี้มีรายการบทความ ซึ่งแต่ละบทความมีชื่อ คำอธิบาย แหล่งที่มา ชื่อผู้เขียน วันที่ตีพิมพ์ และสถานะว่ามีการบุ๊กมาร์กหรือไม่ UI ของรายการบทความแต่ละรายการจะมีลักษณะดังนี้
ตัวอย่างของเหตุการณ์ที่อาจทำให้เกิดการเปลี่ยนแปลงสถานะคือผู้ใช้ขอให้บุ๊กมาร์กบทความ ในฐานะผู้สร้างสถานะ ViewModel มีหน้าที่รับผิดชอบในการกำหนดตรรกะทั้งหมดที่จำเป็นในการป้อนข้อมูลในฟิลด์ทั้งหมดในสถานะ UI และประมวลผลเหตุการณ์ที่จำเป็นเพื่อให้ UI แสดงผลได้อย่างสมบูรณ์
ส่วนต่อไปนี้จะอธิบายรายละเอียดเกี่ยวกับเหตุการณ์ที่ทำให้เกิดการเปลี่ยนแปลงสถานะและวิธีประมวลผลเหตุการณ์เหล่านั้นโดยใช้ 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 ซึ่งหมายความว่าอาจจำเป็นต้องมีการลดความเสี่ยงโดยใช้เมธอดFlowAPI เช่น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 ลองสำรวจแหล่งข้อมูลต่อไปนี้เพื่อดูตัวอย่างการนำคำแนะนำนี้ไปใช้จริง
แนะนำสำหรับคุณ
- หมายเหตุ: ข้อความลิงก์จะแสดงเมื่อ JavaScript ปิดอยู่
- การสร้างสถานะ UI
- ตัวเก็บสถานะและสถานะ UI {:#mad-arch}
- คู่มือเกี่ยวกับสถาปัตยกรรมแอป