การจัดโครงสร้าง UI ของ Compose

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

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

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

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

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

ลูปการอัปเดต UI สําหรับแอปที่ใช้การไหลของข้อมูลแบบทิศทางเดียวมีลักษณะดังนี้

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

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

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

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

การรับส่งข้อมูลแบบทิศทางเดียวใน Jetpack Compose

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

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

กําหนดพารามิเตอร์แบบคอมโพสิเบิล

เมื่อกําหนดพารามิเตอร์สถานะของคอมโพสิเบิล คุณควรคำนึงถึงคำถามต่อไปนี้

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

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

@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 จะไม่มีการเปลี่ยนแปลง

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

เหตุการณ์ในเครื่องมือเขียน

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

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

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

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

เช่น คอมโพสิเบิลที่ยอมรับ String และ Lambda เป็นพารามิเตอร์สามารถเรียกใช้ได้จากบริบทหลายรายการและนํากลับมาใช้ซ้ำได้อย่างมาก สมมติว่าแถบแอปด้านบนในแอปแสดงข้อความและมีปุ่มย้อนกลับเสมอ คุณสามารถกําหนดคอมโพสิชัน 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 คุณยังนํา Dataflow แบบทิศทางเดียวมาใช้ในแอปได้ด้วยหากเงื่อนไขข้อใดข้อหนึ่งต่อไปนี้เป็นจริง

  • สถานะของ 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

    // ...
}

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

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

ตัวอย่าง