วงจรของ Composable

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

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

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

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

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

แผนภาพแสดงวงจรของคอมโพสิเบิล

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

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

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

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

แผนภาพแสดงการจัดเรียงตามลําดับชั้นขององค์ประกอบในข้อมูลโค้ดก่อนหน้า

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

องค์ประกอบของคอมโพสิเบิลในคอมโพสิชัน

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

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

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

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

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

แผนภาพแสดงวิธีคอมโพสิชันโค้ดก่อนหน้าอีกครั้งหากเปลี่ยน Flag showError เป็น &quot;จริง&quot; ระบบจะเพิ่มคอมโพสิชัน LoginError แต่จะไม่คอมโพสิชันคอมโพสิชันอื่นๆ อีกครั้ง

รูปที่ 3 การนําเสนอ LoginScreen ในองค์ประกอบเมื่อสถานะเปลี่ยนแปลงและเกิดการจัดองค์ประกอบใหม่ สีเดียวกันหมายความว่าไม่ได้จัดองค์ประกอบใหม่

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

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

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

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

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

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

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

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

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

@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 มีคีย์ที่ไม่ซ้ำกัน 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 เนื่องจากเป็นประเภทที่เปลี่ยนแปลงไม่ได้ เนื่องจากประเภทแบบคงที่จะไม่มีการเปลี่ยนแปลง จึงไม่จำเป็นต้องแจ้งการคอมโพสิชันของการเปลี่ยนแปลง ดังนั้นการปฏิบัติตามสัญญานี้จึงง่ายขึ้นมาก

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

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