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

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

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

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

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

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

  • mutableStateOf(value) จะสร้าง MutableState ซึ่งเป็นประเภทที่สังเกตได้ใน Compose การเปลี่ยนแปลงค่าจะกำหนดเวลาการจัดองค์ประกอบใหม่ของฟังก์ชันที่ประกอบกันได้ซึ่งอ่านค่านั้น
  • remember จะจัดเก็บออบเจ็กต์ไว้ใน Composition และจะลืมออบเจ็กต์เมื่อนำ Composable ที่เรียกใช้ remember ออกจาก Composition
  • 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) คอมโพสิเบิลจะจัดองค์ประกอบใหม่ แม้ว่า title และ subtitle จะไม่มีการเปลี่ยนแปลงก็ตาม

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

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

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

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

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

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

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

@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 สถานะ ดังนี้

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

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

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

    // ...
}

นอกจาก mutableStateOf API แล้ว 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 ได้ที่แหล่งข้อมูลต่อไปนี้

ตัวอย่าง