วงจรของ Composable

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

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

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

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

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

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

อินสแตนซ์ของ Composable ใน Composition จะระบุโดยตำแหน่งการเรียกใช้ คอมไพเลอร์ Compose จะพิจารณาว่า Call Site แต่ละรายการแตกต่างกัน การเรียกใช้ 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 จะเรียกใช้ Composable LoginError แบบมีเงื่อนไข และจะเรียกใช้ Composable LoginInput เสมอ การเรียกแต่ละครั้งจะมีตำแหน่งการเรียกและตำแหน่งต้นทางที่ไม่ซ้ำกัน ซึ่งคอมไพเลอร์จะใช้เพื่อระบุการเรียกนั้นๆ โดยไม่ซ้ำกัน

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

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

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

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

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

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

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

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

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

คอมโพสิเบิลบางรายการมีการรองรับในตัวสำหรับคอมโพสิเบิล key ตัวอย่างเช่น LazyColumn ยอมรับการระบุ key ที่กำหนดเองใน items DSL

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

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

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

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

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

มีโหมดคอมไพเลอร์ที่เรียกว่าการข้ามแบบเข้มงวด ซึ่งจะผ่อนปรนข้อกำหนดสุดท้าย

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

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

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

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

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

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