เฟสของ Jetpack Compose

Compose แสดงผลเฟรมผ่านระยะต่างๆ ที่แตกต่างกันเช่นเดียวกับชุดเครื่องมือ UI อื่นๆ ส่วนใหญ่ ตัวอย่างเช่น ระบบ Android View มี 3 ระยะหลัก ได้แก่ การวัดค่า เลย์เอาต์ และการวาด Compose มีความคล้ายกันมาก แต่มีเฟสเพิ่มเติมที่สำคัญที่เรียกว่า Composition ในตอนเริ่มต้น

เอกสารประกอบของ Compose อธิบาย Composition ไว้ในการคิดใน Compose และสถานะและ Jetpack Compose

3 ระยะของเฟรม

Compose มี 3 ระยะหลักๆ ดังนี้

  1. Composition: สิ่งที่จะแสดงใน UI Compose จะเรียกใช้ฟังก์ชันที่ประกอบกันได้และสร้างคำอธิบาย UI ของคุณ
  2. เลย์เอาต์: ตำแหน่งที่จะวาง UI ระยะนี้ประกอบด้วย 2 ขั้นตอน ได้แก่ การวัดผลและการจัดวาง องค์ประกอบเลย์เอาต์จะวัดและวางตำแหน่งตัวเองและองค์ประกอบย่อยในพิกัด 2 มิติสำหรับแต่ละโหนดในโครงสร้างเลย์เอาต์
  3. การวาด: วิธีแสดงผล องค์ประกอบ UI จะวาดลงใน Canvas ซึ่งมักจะเป็นหน้าจออุปกรณ์
3 ระยะที่ Compose แปลงข้อมูลเป็น UI (ตามลำดับ ข้อมูล, Composition, เลย์เอาต์, การวาด, UI)
รูปที่ 1 3 เฟสที่ Compose แปลงข้อมูลเป็น UI

โดยทั่วไปแล้ว ลำดับของระยะเหล่านี้จะเหมือนกัน ซึ่งช่วยให้ข้อมูลไหลไปในทิศทางเดียวตั้งแต่การจัดองค์ประกอบไปจนถึงเลย์เอาต์และการวาดเพื่อสร้างเฟรม (หรือที่เรียกว่าการไหลของข้อมูลแบบทิศทางเดียว) BoxWithConstraints, LazyColumn และ LazyRow เป็นข้อยกเว้นที่สำคัญ โดยองค์ประกอบขององค์ประกอบย่อยจะขึ้นอยู่กับระยะเลย์เอาต์ขององค์ประกอบหลัก

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

ทำความเข้าใจระยะต่างๆ

ส่วนนี้จะอธิบายรายละเอียดเพิ่มเติมเกี่ยวกับวิธีดำเนินการ 3 ระยะของ Compose สำหรับ Composable

การเรียบเรียง

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

รูปที่ 2 โครงสร้างที่แสดง UI ของคุณซึ่งสร้างขึ้นในระยะการจัดองค์ประกอบ

ส่วนย่อยของโค้ดและโครงสร้าง UI จะมีลักษณะดังนี้

ข้อมูลโค้ดที่มี Composable 5 รายการและโครงสร้าง UI ที่ได้ โดยมีโหนดย่อยแยกสาขาจากโหนดหลัก
รูปที่ 3 ส่วนย่อยของแผนผัง UI ที่มีโค้ดที่เกี่ยวข้อง

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

การจัดวาง

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

รูปที่ 4 การวัดและการวางโหนดเลย์เอาต์แต่ละโหนดในแผนผัง UI ระหว่างระยะเลย์เอาต์

ในระหว่างระยะเลย์เอาต์ ระบบจะข้ามโครงสร้างโดยใช้อัลกอริทึม 3 ขั้นตอนต่อไปนี้

  1. วัดโหนดย่อย: โหนดจะวัดโหนดย่อยหากมี
  2. กำหนดขนาดของตัวเอง: โหนดจะกำหนดขนาดของตัวเองตามการวัดเหล่านี้
  3. วางโหนดบุตรหลาน: โหนดบุตรหลานแต่ละโหนดจะวางตามตำแหน่งของโหนดเอง

เมื่อสิ้นสุดระยะนี้ โหนดเลย์เอาต์แต่ละโหนดจะมีสิ่งต่อไปนี้

  • ความกว้างและความสูงที่กำหนด
  • พิกัด x, y ที่ควรวาด

โปรดนึกถึงแผนผัง UI จากส่วนก่อนหน้า

ข้อมูลโค้ดที่มี Composable 5 รายการและโครงสร้าง UI ที่ได้ โดยมีโหนดย่อยแยกสาขาจากโหนดหลัก

สำหรับแผนภูมินี้ อัลกอริทึมจะทำงานดังนี้

  1. Row จะวัดองค์ประกอบย่อย Image และ Column
  2. ระบบจะวัดค่า Image เนื่องจากไม่มีองค์ประกอบย่อย จึงกำหนดขนาดของตัวเองและรายงานขนาดกลับไปยัง Row
  3. จากนั้นระบบจะวัดColumn โดยจะวัดองค์ประกอบย่อยของตัวเอง (Text Composable 2 รายการ) ก่อน
  4. ระบบจะวัด Text แรก เนื่องจากไม่มีองค์ประกอบย่อย จึงกำหนดขนาดของตัวเองและรายงานขนาดกลับไปยัง Column
    1. ระบบจะวัดค่า Text ที่ 2 เนื่องจากไม่มีองค์ประกอบย่อย จึงกำหนดขนาดของตัวเองและรายงานกลับไปยัง Column
  5. Column จะใช้การวัดขนาดขององค์ประกอบย่อยเพื่อกำหนดขนาดของตัวเอง โดยจะใช้ความกว้างสูงสุดขององค์ประกอบย่อยและผลรวมความสูงขององค์ประกอบย่อย
  6. Column จะวางองค์ประกอบย่อยโดยอิงตามตัวมันเอง โดยวางองค์ประกอบย่อยไว้ใต้กันและกันในแนวตั้ง
  7. Row จะใช้การวัดขนาดขององค์ประกอบย่อยเพื่อกำหนดขนาดของตัวเอง โดยจะใช้ความสูงสูงสุดขององค์ประกอบย่อยและผลรวมของความกว้างขององค์ประกอบย่อย จากนั้นจึงวางองค์ประกอบย่อย

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

การวาด

ในระยะการวาด ระบบจะข้ามต้นไม้อีกครั้งจากบนลงล่าง และแต่ละโหนดจะวาดตัวเองบนหน้าจอตามลำดับ

รูปที่ 5 ระยะการวาดจะวาดพิกเซลบนหน้าจอ

จากตัวอย่างก่อนหน้านี้ เนื้อหาแบบต้นไม้จะแสดงในลักษณะต่อไปนี้

  1. Row จะวาดเนื้อหาที่อาจมี เช่น สีพื้นหลัง
  2. Image วาดตัวเอง
  3. Column วาดตัวเอง
  4. Text ตัวแรกและตัวที่ 2 จะวาดตัวเองตามลำดับ

รูปที่ 6 แผนผัง UI และการแสดงผลที่วาดขึ้น

การอ่านสถานะ

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

โดยทั่วไป คุณจะสร้างสถานะโดยใช้ mutableStateOf() แล้วเข้าถึงสถานะนั้นผ่านวิธีใดวิธีหนึ่งจาก 2 วิธี ได้แก่ การเข้าถึงพร็อพเพอร์ตี้ value โดยตรง หรือการใช้ผู้รับมอบสิทธิ์พร็อพเพอร์ตี้ Kotlin อ่านข้อมูลเพิ่มเติมเกี่ยวกับสถานะได้ในสถานะใน Composable สำหรับวัตถุประสงค์ของคู่มือนี้ "การอ่านสถานะ" หมายถึงวิธีการเข้าถึงที่เทียบเท่ากันอย่างใดอย่างหนึ่ง

// State read without property delegate.
val paddingState: MutableState<Dp> = remember { mutableStateOf(8.dp) }
Text(
    text = "Hello",
    modifier = Modifier.padding(paddingState.value)
)

// State read with property delegate.
var padding: Dp by remember { mutableStateOf(8.dp) }
Text(
    text = "Hello",
    modifier = Modifier.padding(padding)
)

เบื้องหลังของผู้รับมอบสิทธิ์พร็อพเพอร์ตี้ ฟังก์ชัน "getter" และ "setter" จะใช้เพื่อเข้าถึงและอัปเดต value ของ State ระบบจะเรียกใช้ฟังก์ชัน Getter และ Setter เหล่านี้เมื่อคุณอ้างอิงพร็อพเพอร์ตี้เป็นค่าเท่านั้น และจะไม่เรียกใช้เมื่อสร้างพร็อพเพอร์ตี้ ซึ่งเป็นสาเหตุที่ทำให้ทั้ง 2 วิธีที่อธิบายไว้ก่อนหน้านี้เทียบเท่ากัน

โค้ดแต่ละบล็อกที่เรียกใช้ซ้ำได้เมื่อสถานะการอ่านเปลี่ยนแปลงคือขอบเขตการรีสตาร์ท Compose จะติดตามการเปลี่ยนแปลงสถานะ value และรีสตาร์ทขอบเขตในระยะต่างๆ

การอ่านสถานะแบบแบ่งระยะ

ดังที่กล่าวไว้ก่อนหน้านี้ Compose มี 3 ระยะหลัก และ Compose จะติดตามสถานะที่อ่านภายในแต่ละระยะ ซึ่งจะช่วยให้ Compose แจ้งเตือนเฉพาะเฟสที่ต้องทำงานสำหรับองค์ประกอบที่ได้รับผลกระทบแต่ละรายการของ UI

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

ระยะที่ 1: การจัดองค์ประกอบ

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

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

var padding by remember { mutableStateOf(8.dp) }
Text(
    text = "Hello",
    // The `padding` state is read in the composition phase
    // when the modifier is constructed.
    // Changes in `padding` will invoke recomposition.
    modifier = Modifier.padding(padding)
)

ระยะที่ 2: เลย์เอาต์

ระยะเลย์เอาต์ประกอบด้วย 2 ขั้นตอน ได้แก่ การวัดและการวาง ขั้นตอนการวัดจะเรียกใช้ Lambda การวัดที่ส่งไปยัง Layout Composable, เมธอด MeasureScope.measure ของอินเทอร์เฟซ LayoutModifier และอื่นๆ ขั้นตอนการวางจะเรียกใช้บล็อกการวางของฟังก์ชัน layout, บล็อก Lambda ของ Modifier.offset { … } และฟังก์ชันที่คล้ายกัน

การอ่านสถานะในแต่ละขั้นตอนเหล่านี้จะส่งผลต่อเลย์เอาต์และอาจส่งผลต่อระยะการวาด เมื่อ value ของสถานะเปลี่ยนแปลง Compose UI จะกำหนดเวลาเฟสเลย์เอาต์ นอกจากนี้ยังเรียกใช้ระยะการวาดหากขนาดหรือตำแหน่งมีการเปลี่ยนแปลง

var offsetX by remember { mutableStateOf(8.dp) }
Text(
    text = "Hello",
    modifier = Modifier.offset {
        // The `offsetX` state is read in the placement step
        // of the layout phase when the offset is calculated.
        // Changes in `offsetX` restart the layout.
        IntOffset(offsetX.roundToPx(), 0)
    }
)

ระยะที่ 3: การจับรางวัล

การอ่านสถานะระหว่างโค้ดการวาดจะส่งผลต่อระยะการวาด ตัวอย่างที่พบบ่อย ได้แก่ Canvas(), Modifier.drawBehind และ Modifier.drawWithContent เมื่อ value ของสถานะเปลี่ยนแปลง Compose UI จะเรียกใช้เฉพาะระยะการวาดเท่านั้น

var color by remember { mutableStateOf(Color.Red) }
Canvas(modifier = modifier) {
    // The `color` state is read in the drawing phase
    // when the canvas is rendered.
    // Changes in `color` restart the drawing.
    drawRect(color)
}

แผนภาพแสดงให้เห็นว่าการอ่านสถานะระหว่างระยะการวาดจะทริกเกอร์ให้ระยะการวาดทำงานอีกครั้งเท่านั้น

เพิ่มประสิทธิภาพการอ่านสถานะ

เนื่องจาก Compose ทำการติดตามการอ่านสถานะที่แปลแล้ว คุณจึงลดปริมาณงานที่ทำได้โดยการอ่านแต่ละสถานะในเฟสที่เหมาะสม

ลองดูตัวอย่างต่อไปนี้ ตัวอย่างนี้มี Image() ซึ่งใช้ตัวแก้ไขออฟเซ็ตเพื่อออฟเซ็ตตำแหน่งเลย์เอาต์สุดท้าย ส่งผลให้เกิดเอฟเฟกต์พารัลแลกซ์เมื่อผู้ใช้เลื่อน

Box {
    val listState = rememberLazyListState()

    Image(
        // ...
        // Non-optimal implementation!
        Modifier.offset(
            with(LocalDensity.current) {
                // State read of firstVisibleItemScrollOffset in composition
                (listState.firstVisibleItemScrollOffset / 2).toDp()
            }
        )
    )

    LazyColumn(state = listState) {
        // ...
    }
}

โค้ดนี้ใช้งานได้ แต่ส่งผลให้ประสิทธิภาพไม่ดีเท่าที่ควร ตามที่เขียนไว้ โค้ดจะอ่าน value ของสถานะ firstVisibleItemScrollOffset และส่งไปยังฟังก์ชัน Modifier.offset(offset: Dp) เมื่อผู้ใช้เลื่อน value ของ firstVisibleItemScrollOffset จะเปลี่ยนไป ดังที่ได้เรียนรู้ไปแล้ว Compose จะติดตามการอ่านสถานะเพื่อให้สามารถรีสตาร์ท (เรียกใช้ซ้ำ) โค้ดการอ่านได้ ซึ่งในตัวอย่างนี้คือเนื้อหาของ Box

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

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

ออฟเซ็ตด้วยแลมบ์ดา

ตัวแก้ไขออฟเซ็ตยังมีอีกเวอร์ชันหนึ่ง ดังนี้ Modifier.offset(offset: Density.() -> IntOffset)

เวอร์ชันนี้ใช้พารามิเตอร์ Lambda โดยบล็อก Lambda จะแสดงผลออฟเซ็ตที่ได้ อัปเดตโค้ดเพื่อใช้โค้ดดังกล่าวโดยทำดังนี้

Box {
    val listState = rememberLazyListState()

    Image(
        // ...
        Modifier.offset {
            // State read of firstVisibleItemScrollOffset in Layout
            IntOffset(x = 0, y = listState.firstVisibleItemScrollOffset / 2)
        }
    )

    LazyColumn(state = listState) {
        // ...
    }
}

แล้วทำไมวิธีนี้จึงมีประสิทธิภาพมากกว่า ระบบจะเรียกใช้บล็อก Lambda ที่คุณระบุให้กับตัวแก้ไขในระหว่างระยะเลย์เอาต์ (โดยเฉพาะในขั้นตอนการวางของระยะเลย์เอาต์) ซึ่งหมายความว่าระบบจะไม่อ่านสถานะ firstVisibleItemScrollOffset ในระหว่างการจัดองค์ประกอบอีกต่อไป เนื่องจาก Compose จะติดตามเมื่อมีการอ่านสถานะ การเปลี่ยนแปลงนี้หมายความว่าหาก value ของ firstVisibleItemScrollOffset มีการเปลี่ยนแปลง Compose จะต้องรีสตาร์ทเฉพาะขั้นตอนเลย์เอาต์และการวาดเท่านั้น

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

ลูปการจัดองค์ประกอบใหม่ (การขึ้นต่อกันของระยะที่เป็นวงจร)

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

Box {
    var imageHeightPx by remember { mutableIntStateOf(0) }

    Image(
        painter = painterResource(R.drawable.rectangle),
        contentDescription = "I'm above the text",
        modifier = Modifier
            .fillMaxWidth()
            .onSizeChanged { size ->
                // Don't do this
                imageHeightPx = size.height
            }
    )

    Text(
        text = "I'm below the image",
        modifier = Modifier.padding(
            top = with(LocalDensity.current) { imageHeightPx.toDp() }
        )
    )
}

ตัวอย่างนี้ใช้คอลัมน์แนวตั้ง โดยมีรูปภาพอยู่ด้านบนและข้อความอยู่ด้านล่าง โดยใช้ Modifier.onSizeChanged() เพื่อรับขนาดที่แก้ไขแล้วของรูปภาพ จากนั้นใช้ Modifier.padding() กับข้อความเพื่อเลื่อนลง การแปลงที่ไม่เป็นธรรมชาติจาก Px กลับไปเป็น Dp บ่งชี้อยู่แล้วว่าโค้ดมีปัญหาบางอย่าง

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

องค์ประกอบเฟรมแรก

ในระหว่างระยะการจัดองค์ประกอบของเฟรมแรก imageHeightPx จะเป็น 0 ในตอนแรก ด้วยเหตุนี้ โค้ดจึงแสดงข้อความพร้อม Modifier.padding(top = 0) ระยะเลย์เอาต์ที่ตามมาจะเรียกใช้ Callback ของตัวแก้ไข onSizeChanged ซึ่งจะอัปเดต imageHeightPx เป็นความสูงจริงของรูปภาพ จากนั้น Compose จะกำหนดเวลาการจัดองค์ประกอบใหม่สำหรับเฟรมถัดไป อย่างไรก็ตาม ในระหว่างขั้นตอนการวาดปัจจุบัน ข้อความจะแสดงผลโดยมีระยะห่างจากขอบ 0 เนื่องจากค่า imageHeightPx ที่อัปเดตยังไม่แสดงผล

องค์ประกอบของเฟรมที่ 2

Compose จะเริ่มเฟรมที่ 2 ซึ่งทริกเกอร์โดยการเปลี่ยนแปลงค่าของ imageHeightPx ในระยะการจัดองค์ประกอบของเฟรมนี้ ระบบจะอ่านสถานะภายในบล็อกเนื้อหา Box ตอนนี้ข้อความมีระยะห่างจากขอบที่ตรงกับความสูงของรูปภาพอย่างถูกต้อง ในระหว่างระยะเลย์เอาต์ ระบบจะตั้งค่า imageHeightPx อีกครั้ง แต่จะไม่มีการกำหนดเวลาการจัดองค์ประกอบใหม่เพิ่มเติมเนื่องจากค่ายังคงสอดคล้องกัน

แผนภาพแสดงลูปการจัดองค์ประกอบใหม่ซึ่งการเปลี่ยนแปลงขนาดในระยะเลย์เอาต์จะทริกเกอร์การจัดองค์ประกอบใหม่ ซึ่งจะทำให้เลย์เอาต์เกิดขึ้นอีกครั้ง

ตัวอย่างนี้อาจดูเหมือนเป็นเรื่องที่แต่งขึ้น แต่โปรดระวังรูปแบบทั่วไปต่อไปนี้

  • Modifier.onSizeChanged(), onGloballyPositioned() หรือการดำเนินการอื่นๆ เกี่ยวกับเลย์เอาต์
  • อัปเดตสถานะบางอย่าง
  • ใช้สถานะนั้นเป็นอินพุตสำหรับตัวแก้ไขเลย์เอาต์ (padding(), height() หรือที่คล้ายกัน)
  • อาจทำซ้ำ

วิธีแก้ไขตัวอย่างก่อนหน้านี้คือการใช้ Layout Primitives ที่เหมาะสม ตัวอย่างก่อนหน้านี้สามารถใช้ Column() ได้ แต่คุณอาจมีตัวอย่างที่ซับซ้อนกว่าซึ่งต้องใช้สิ่งที่กำหนดเอง ซึ่งจะต้องเขียนเลย์เอาต์ที่กำหนดเอง ดูข้อมูลเพิ่มเติมได้ที่คู่มือเลย์เอาต์ที่กำหนดเอง

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

ดูข้อมูลเพิ่มเติมเกี่ยวกับลูปการจัดองค์ประกอบใหม่และวิธีหลีกเลี่ยงการเขียนไปยังสถานะในเฟสต่างๆ ได้ที่การเขียนย้อนกลับใน Compose