วงจรของ Composable

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

ภาพรวมวงจรลูกค้า

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

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

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 เพื่อให้ดำเนินการเสร็จสมบูรณ์ได้แทนที่จะต้องรีสตาร์ททุกครั้ง ที่มีการจัดองค์ประกอบใหม่

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

@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 ช่วยให้คุณบอกรันไทม์ได้ว่า ต้องการใช้ค่าใดเพื่อระบุส่วนที่ต้องการของทรี ซึ่งก็คือ key Composable

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