วงจรของ 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

โดยปกติแล้ว การประกอบใหม่จะเกิดขึ้นเมื่อมีการเปลี่ยนแปลงออบเจ็กต์ a 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 หมายความว่ามีการประกอบMovieOverview ComposableMovieOverview อีกครั้ง

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

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