วงจรของ Composable

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

ภาพรวมวงจร

ดังที่กล่าวไว้ในเอกสารประกอบเกี่ยวกับการจัดการสถานะ องค์ประกอบ Composition จะอธิบาย 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 ที่ใช้ได้และจะเรียกใช้ LoginInput ที่ใช้ได้เสมอ การเรียกใช้แต่ละครั้งมีตำแหน่งการเรียกใช้และตำแหน่งต้นทางที่ไม่ซ้ำกัน ซึ่งคอมไพเลอร์จะใช้เพื่อระบุการเรียกใช้ดังกล่าวแบบไม่ซ้ำกัน

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

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

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

การเรียกใช้ Composable หลายครั้งจะเพิ่ม 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 ช่วยให้คุณบอกรันไทม์ได้ว่า ต้องการใช้ค่าใดเพื่อระบุส่วนที่ต้องการของทรี ซึ่งก็คือ คอมโพสابل 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 จะถือว่าการใช้งานทั้งหมดมีเสถียรภาพหากใช้อินเทอร์เฟซเป็นประเภทพารามิเตอร์