วงจรของ Composable

ในหน้านี้ คุณจะได้เรียนรู้เกี่ยวกับวงจรของ Composable และ วิธีที่ Compose ตัดสินว่า Composable ต้องมีการ Recomposition หรือไม่

ภาพรวมวงจร

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

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

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

แผนภาพแสดงวงจรการใช้งานของ Composable
รูปที่ 1 วงจรของ Composable ใน Composition โดยจะเข้าสู่ Composition, ได้รับการจัดองค์ประกอบใหม่ 0 ครั้งขึ้นไป และออกจาก Composition

โดยปกติแล้ว การประกอบใหม่จะเกิดขึ้นเมื่อมีการเปลี่ยนแปลงออบเจ็กต์ State<T> Compose จะติดตามสิ่งเหล่านี้และเรียกใช้ Composable ทั้งหมดใน Composition ที่อ่านState<T>นั้นๆ และ Composable ใดๆ ที่เรียกใช้ Composable นั้นซึ่งข้ามไม่ได้

หากมีการเรียกใช้ Composable หลายครั้ง ระบบจะวางอินสแตนซ์หลายรายการไว้ใน Composition การเรียกใช้แต่ละครั้งจะมีวงจรของตัวเองในการเรียบเรียง

@Composable
fun MyComposable() {
    Column {
        Text("Hello")
        Text("World")
    }
}

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

โครงสร้างของ Composable ใน Composition

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

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

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

ลองดูตัวอย่างต่อไปนี้

@Composable
fun LoginScreen(showError: Boolean) {
    if (showError) {
        LoginError()
    }
    LoginInput() // This call site affects where LoginInput is placed in Composition
}

@Composable
fun LoginInput() { /* ... */ }

@Composable
fun LoginError() { /* ... */ }

ในข้อมูลโค้ดด้านบน LoginScreen จะเรียกใช้ LoginError Composable แบบมีเงื่อนไข และจะเรียกใช้ LoginInput Composable เสมอ การเรียกใช้แต่ละครั้งมีตำแหน่งการเรียกใช้และตำแหน่งต้นทางที่ไม่ซ้ำกัน ซึ่งคอมไพเลอร์จะใช้เพื่อระบุการเรียกใช้ดังกล่าวแบบไม่ซ้ำกัน

แผนภาพแสดงวิธีประกอบโค้ดก่อนหน้าใหม่หากเปลี่ยนแฟล็ก showError เป็น true ระบบจะเพิ่ม Composable LoginError แต่จะไม่ทำการ Recompose Composable อื่นๆ
รูปที่ 3 การแสดง LoginScreen ใน Composition เมื่อสถานะ เปลี่ยนและเกิดการจัดองค์ประกอบใหม่ สีเดียวกันหมายความว่ายังไม่ได้จัดองค์ประกอบใหม่

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

เพิ่มข้อมูลเพิ่มเติมเพื่อช่วยในการจัดองค์ประกอบใหม่แบบอัจฉริยะ

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

@Composable
fun MoviesScreen(movies: List<Movie>) {
    Column {
        for (movie in movies) {
            // MovieOverview composables are placed in Composition given its
            // index position in the for loop
            MovieOverview(movie)
        }
    }
}

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

แผนภาพแสดงวิธีที่โค้ดก่อนหน้าจะได้รับการจัดองค์ประกอบใหม่หากมีการเพิ่มองค์ประกอบใหม่ที่ด้านล่างของรายการ ส่วนรายการอื่นๆ ในลิสต์จะไม่มีการเปลี่ยนตำแหน่งและจะไม่ได้รับการจัดเรียงใหม่
รูปที่ 4 การแสดง MoviesScreen ในการจัดองค์ประกอบเมื่อเพิ่มองค์ประกอบใหม่ที่ด้านล่างของรายการ MovieOverview ที่ใช้ใน Composition สามารถนำกลับมาใช้ใหม่ได้ สีเดียวกันใน MovieOverview หมายความว่า Composable ยังไม่ได้ทำการ Recompose

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

@Composable
fun MovieOverview(movie: Movie) {
    Column {
        // Side effect explained later in the docs. If MovieOverview
        // recomposes, while fetching the image is in progress,
        // it is cancelled and restarted.
        val image = loadNetworkImage(movie.url)
        MovieHeader(image)

        /* ... */
    }
}

แผนภาพแสดงวิธีที่โค้ดก่อนหน้าจะได้รับการจัดองค์ประกอบใหม่หากมีการเพิ่มองค์ประกอบใหม่ที่ด้านบนของรายการ รายการอื่นๆ ทั้งหมดในลิสต์จะเปลี่ยนตำแหน่งและต้องจัดเรียงใหม่
รูปที่ 5 การแสดง MoviesScreen ในการจัดองค์ประกอบเมื่อมีการเพิ่มองค์ประกอบใหม่ลงในรายการ MovieOverview Composable จะนำกลับมาใช้ใหม่ไม่ได้และ ผลข้างเคียงทั้งหมดจะรีสตาร์ท สีที่แตกต่างใน MovieOverview หมายความว่า Composable ได้รับการประกอบใหม่

ในอุดมคติ เราต้องการพิจารณาตัวตนของMovieOverviewอินสแตนซ์เป็น ลิงก์กับตัวตนของmovieที่ส่งไปยังอินสแตนซ์ หากเราจัดลำดับรายการภาพยนตร์ใหม่ เราควรจัดลำดับอินสแตนซ์ใน Composition tree ใหม่ในลักษณะเดียวกันแทนที่จะจัดองค์ประกอบใหม่สำหรับแต่ละ MovieOverview composable ด้วยอินสแตนซ์ภาพยนตร์ที่ แตกต่างกัน Compose ช่วยให้คุณบอกรันไทม์ได้ว่าต้องการใช้ค่าใดเพื่อระบุส่วนที่ต้องการของทรี ซึ่งก็คือ Composable key

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

@Composable
fun MoviesScreenWithKey(movies: List<Movie>) {
    Column {
        for (movie in movies) {
            key(movie.id) { // Unique ID for this movie
                MovieOverview(movie)
            }
        }
    }
}

ด้วยข้อมูลข้างต้น แม้ว่าองค์ประกอบในรายการจะเปลี่ยนแปลง แต่ Compose ก็จะจดจำการเรียกแต่ละรายการไปยัง MovieOverview และนำกลับมาใช้ใหม่ได้

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

Composable บางรายการมีการรองรับ Composable key ในตัว เช่น LazyColumn ยอมรับการระบุ key ที่กำหนดเองใน DSL ของ items

@Composable
fun MoviesScreenLazy(movies: List<Movie>) {
    LazyColumn {
        items(movies, key = { movie -> movie.id }) { movie ->
            MovieOverview(movie)
        }
    }
}

ข้ามหากอินพุตไม่มีการเปลี่ยนแปลง

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

ฟังก์ชันที่ประกอบกันได้จะมีสิทธิ์ข้ามเว้นแต่

  • ฟังก์ชันมีประเภทการคืนค่าที่���ม่ใช่ Unit
  • ฟังก์ชันมีคำอธิบายประกอบด้วย @NonRestartableComposable หรือ @NonSkippableComposable
  • พารามิเตอร์ที่จำเป็นมีประเภทที่ไม่เสถียร

มีโหมดคอมไพเลอร์เวอร์ชันทดลอง Strong Skipping ซึ่งผ่อนปรนข้อกำหนดสุดท้าย

ประเภทต้องเป็นไปตามสัญญาต่อไปนี้จึงจะถือว่าเสถียร

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

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

  • ประเภทค่าดั้งเดิมทั้งหมด: Boolean, Int, Long, Float, Char ฯลฯ
  • เครื่องสาย
  • ฟังก์ชันทุกประเภท (Lambda)

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

ประเภทที่น่าสังเกตประเภทหนึ่งซึ่งมีความเสถียรแต่แก้ไขได้คือประเภท MutableState ของ Compose หากค่าอยู่ใน MutableState ระบบจะถือว่าออบเจ็กต์สถานะโดยรวมมีความเสถียร เนื่องจาก Compose จะได้รับการแจ้งเตือนเกี่ยวกับการเปลี่ยนแปลงใดๆ ในพร็อพเพอร์ตี้ .value ของ State

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

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

หาก Compose ไม่สามารถสรุปได้ว่าประเภทใดมีเสถียรภาพ แต่คุณต้องการบังคับให้ Compose ถือว่าประเภทนั้นมีเสถียรภาพ ให้ทำเครื่องหมายด้วยคำอธิบายประกอบ @Stable

// Marking the type as stable to favor skipping and smart recompositions.
@Stable
interface UiState<T : Result<T>> {
    val value: T?
    val exception: Throwable?

    val hasError: Boolean
        get() = exception != null
}

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