เฟสของ Jetpack Compose

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

เราได้อธิบายการคอมโพสิชันไว้ในเอกสาร Compose ซึ่งรวมถึงการคิดใน Compose และState และ Jetpack Compose

เฟรมแบ่งออกเป็น 3 ช่วง

การสร้างมี 3 ระยะหลักดังนี้

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

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

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

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

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

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

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

รูปที่ 2 ต้นไม้ที่แสดง UI ที่สร้างขึ้นในขั้นตอนการคอมโพสิชัน

ส่วนย่อยของโค้ดและต้นไม้ UI มีลักษณะดังต่อไปนี้

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

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

เลย์เอาต์

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

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

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

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

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

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

โปรดจำโครงสร้าง UI จากส่วนก่อนหน้า

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

สําหรับต้นไม้นี้ อัลกอริทึมจะทํางานดังนี้

  1. Row จะวัดค่าของ Image และ Column
  2. ระบบจะวัด Image โหนดนี้ไม่มีโหนดย่อย จึงตัดสินใจเลือกขนาดของตนเองและรายงานขนาดกลับไปยัง Row
  3. ระบบจะวัด Column ในลำดับถัดไป โดยจะวัดค่าองค์ประกอบของตัวเอง (Text คอมโพสิเบิล 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 และการแสดงภาพ

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

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

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

// 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)
)

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

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

การอ่านสถานะแบบเป็นระยะ

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

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

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

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

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

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 { … } และอื่นๆ

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

กล่าวอย่างละเอียดคือ ขั้นตอนการวัดและขั้นตอนตําแหน่งมีขอบเขตการเริ่มใหม่แยกกัน ซึ่งหมายความว่าการอ่านสถานะในขั้นตอนตําแหน่งจะไม่เรียกใช้ขั้นตอนการวัดก่อนหน้านั้นอีกครั้ง อย่างไรก็ตาม ขั้นตอนทั้ง 2 ขั้นตอนนี้มักจะเกี่ยวพันกัน สถานะที่อ่านในขั้นตอนตําแหน่งจึงอาจส่งผลต่อขอบเขตการเริ่มใหม่อื่นๆ ที่อยู่ในขั้นตอนการวัด

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 เมื่อค่าสถานะเปลี่ยนแปลง 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) {
        // ...
    }
}

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

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

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

ตัวแก้ไขระยะห่างอีกเวอร์ชันหนึ่งมีดังนี้ 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 จะติดตามเมื่อมีการอ่านสถานะ การเปลี่ยนแปลงนี้จึงหมายความว่าหากค่า firstVisibleItemScrollOffset เปลี่ยนแปลง Compose จะต้องเริ่มระยะการจัดวางและการวาดใหม่เท่านั้น

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

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

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

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

Box {
    var imageHeightPx by remember { mutableStateOf(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() ในข้อความเพื่อเลื่อนข้อความลง Conversion ที่ผิดปกติจาก Px กลับไปเป็น Dp บ่งชี้ว่าโค้ดมีปัญหา

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

มาดูแต่ละเฟรมเพื่อดูสิ่งที่เกิดขึ้นกัน

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

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

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

ตัวอย่างนี้อาจดูจงใจ แต่โปรดระวังรูปแบบทั่วไปนี้

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

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

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