สถาปัตยกรรม UI ของ Compose

ใน Compose นั้น UI จะเปลี่ยนแปลงไม่ได้ คุณจึงอัปเดต UI ไม่ได้หลังจากที่วาดแล้ว สิ่งที่คุณควบคุมได้คือสถานะของ UI ทุกครั้งที่สถานะของ UI เปลี่ยนไป Compose จะสร้างส่วนต่างๆ ของแผนผัง UI ที่มีการ เปลี่ยนแปลงขึ้นมาใหม่ Composable สามารถรับสถานะและแสดงเหตุการณ์ได้ เช่น TextField รับค่าและแสดงการเรียกกลับ onValueChange ที่ขอให้ตัวแฮนเดิลการเรียกกลับเปลี่ยนค่า

var name by remember { mutableStateOf("") }
OutlinedTextField(
    value = name,
    onValueChange = { name = it },
    label = { Text("Name") }
)

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

โฟลว์ข้อมูลแบบทิศทางเดียว

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

วงจรการอัปเดต UI ของแอปที่ใช้โฟลว์ข้อมูลแบบทิศทางเดียวมีลักษณะดังนี้

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

การทำตามรูปแบบนี้เมื่อใช้ Jetpack Compose มีข้อดีหลายประการดังนี้

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

โฟลว์ข้อมูลแบบทิศทางเดียวใน Jetpack Compose

Composable ทำงานโดยอิงตามสถานะและเหตุการณ์ ตัวอย่างเช่น TextField จะอัปเดตก็ต่อเมื่อพารามิเตอร์ value อัปเดต และจะแสดงการเรียกกลับ onValueChange ซึ่งเป็นเหตุการณ์ที่ขอให้เปลี่ยนค่าเป็นค่าใหม่ Compose กำหนดออบเจ็กต์ State เป็นตัวเก็บค่า และการเปลี่ยนแปลงค่าสถานะจะทริกเกอร์การจัดองค์ประกอบใหม่ คุณสามารถเก็บสถานะไว้ใน remember { mutableStateOf(value) } หรือ rememberSaveable { mutableStateOf(value) } ทั้งนี้ขึ้นอยู่กับระยะเวลาที่คุณต้องการจดจำค่า

ประเภทค่าของ Composable TextField คือ String ซึ่งอาจมาจากที่ใดก็ได้ ไม่ว่าจะเป็นค่าที่ฮาร์ดโค้ด, จาก ViewModel หรือส่งมาจาก Composable ระดับบน คุณไม่จำเป็นต้องเก็บค่าไว้ในออบเจ็กต์ State แต่ต้องอัปเดตค่าเมื่อมีการเรียก onValueChange

  • mutableStateOf(value) จะสร้าง MutableState ซึ่งเป็นประเภทที่สังเกตได้ใน Compose การเปลี่ยนแปลงค่าจะกำหนดเวลาการสร้างใหม่ของฟังก์ชัน Composable ที่อ่านค่านั้น
  • remember จะจัดเก็บออบเจ็กต์ไว้ในการจัดองค์ประกอบ และลืมออบเจ็กต์เมื่อนำ Composable ที่เรียก remember ออกจากการจัดองค์ประกอบ
  • rememberSaveable จะเก็บสถานะไว้เมื่อมีการเปลี่ยนแปลงการกำหนดค่าโดยบันทึกสถานะไว้ใน Bundle

กำหนดพารามิเตอร์ Composable

เมื่อกำหนดพารามิเตอร์สถานะของ Composable ให้คำนึงถึงคำถามต่อไปนี้

  • Composable สามารถนำกลับมาใช้ซ้ำหรือยืดหยุ่นได้มากน้อยเพียงใด
  • พารามิเตอร์สถานะส่งผลต่อประสิทธิภาพของ Composable นี้อย่างไร

เพื่อส่งเสริมการแยกและการนำกลับมาใช้ซ้ำ Composable แต่ละรายการควรเก็บข้อมูลให้น้อยที่สุด ตัวอย่างเช่น เมื่อสร้าง Composable เพื่อเก็บส่วนหัวของบทความข่าว ให้ส่งเฉพาะข้อมูลที่ต้องแสดงแทนที่จะส่งบทความข่าวทั้งหมด

@Composable
fun Header(title: String, subtitle: String) {
    // Recomposes when title or subtitle have changed.
}

@Composable
fun Header(news: News) {
    // Recomposes when a new instance of News is passed in.
}

บางครั้งการใช้พารามิเตอร์แต่ละรายการจะช่วยปรับปรุงประสิทธิภาพด้วย เช่น หาก News มีข้อมูลมากกว่าแค่ title และ subtitle เมื่อใดก็ตามที่มีการส่งอินสแตนซ์ใหม่ของ News ไปยัง Header(news) Composable จะ สร้างใหม่ แม้ว่า title และ subtitle จะไม่เปลี่ยนแปลงก็ตาม

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

เหตุการณ์ใน Compose

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

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

ควรส่งค่าที่เปลี่ยนแปลงไม่ได้สำหรับสถานะและแลมบ์ดาตัวแฮนเดิลเหตุการณ์ แนวทางนี้มีสิทธิประโยชน์ดังนี้

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

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

@Composable
fun MyAppTopAppBar(topAppBarText: String, onBackPressed: () -> Unit) {
    TopAppBar(
        title = {
            Text(
                text = topAppBarText,
                textAlign = TextAlign.Center,
                modifier = Modifier
                    .fillMaxSize()
                    .wrapContentSize(Alignment.Center)
            )
        },
        navigationIcon = {
            IconButton(onClick = onBackPressed) {
                Icon(
                    Icons.AutoMirrored.Filled.ArrowBack,
                    contentDescription = localizedString
                )
            }
        },
        // ...
    )
}

ViewModel, สถานะ และเหตุการณ์: ตัวอย่าง

การใช้ ViewModel และ mutableStateOf ยังช่วยให้คุณใช้โฟลว์ข้อมูลแบบทิศทางเดียวในแอปได้หากมีเงื่อนไขข้อใดข้อหนึ่งต่อไปนี้เป็นจริง

  • สถานะของ UI แสดงโดยใช้ตัวเก็บสถานะที่สังเกตได้ เช่น StateFlow หรือ LiveData
  • ViewModel จะจัดการเหตุการณ์ที่มาจาก UI หรือเลเยอร์อื่นๆ ของแอป และอัปเดตตัวเก็บสถานะตามเหตุการณ์

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

หน้าจอมี 4 สถานะดังนี้

  • ลงชื่อออก: เมื่อผู้ใช้ยังไม่ได้ลงชื่อเข้าใช้
  • กำลังดำเนินการ: เมื่อแอปพยายามลงชื่อเข้าใช้ให้ผู้ใช้โดย การเรียกเครือข่าย
  • ข้อผิดพลาด: เมื่อเกิดข้อผิดพลาดขณะลงชื่อเข้าใช้
  • ลงชื่อเข้าใช้แล้ว: เมื่อผู้ใช้ลงชื่อเข้าใช้แล้ว

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

class MyViewModel : ViewModel() {
    private val _uiState = mutableStateOf<UiState>(UiState.SignedOut)
    val uiState: State<UiState>
        get() = _uiState

    // ...
}

นอกจาก API mutableStateOf แล้ว Compose มี ส่วนขยาย สำหรับ LiveData, Flow, และ Observable เพื่อลงทะเบียนเป็น Listener และแสดงค่าเป็นสถานะ

class MyViewModel : ViewModel() {
    private val _uiState = MutableLiveData<UiState>(UiState.SignedOut)
    val uiState: LiveData<UiState>
        get() = _uiState

    // ...
}

@Composable
fun MyComposable(viewModel: MyViewModel) {
    val uiState = viewModel.uiState.observeAsState()
    // ...
}

ดูข้อมูลเพิ่มเติม

ดูข้อมูลเพิ่มเติมเกี่ยวกับสถาปัตยกรรมใน Jetpack Compose ได้ที่แหล่งข้อมูลต่อไปนี้

ตัวอย่าง