ข้อมูลที่กำหนดขอบเขตเฉพาะเครื่องด้วย CompositionLocal

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

ข้อมูลเบื้องต้นเกี่ยวกับ CompositionLocal

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

@Composable
fun MyApp() {
    // Theme information tends to be defined near the root of the application
    val colors = colors()
}

// Some composable deep in the hierarchy
@Composable
fun SomeTextLabel(labelText: String) {
    Text(
        text = labelText,
        color = colors.onPrimary // ← need to access colors here
    )
}

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

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

คำสำคัญ: ในคู่มือนี้ เราใช้คำว่า Composition, UI Tree และลำดับชั้นของ UI แม้ว่าบางครั้งอาจใช้แทนกันได้ในคู่มืออื่นๆ แต่ก็มีความหมายที่แตกต่างกัน ดังนี้

  • Composition คือบันทึกของกราฟการเรียกใช้ฟังก์ชันที่ประกอบกันได้
  • แผนผัง UI หรือลำดับชั้น UI คือแผนผังของ LayoutNode ที่สร้าง อัปเดต และดูแลรักษาโดยกระบวนการจัดองค์ประกอบ

CompositionLocal คือสิ่งที่ธีม Material ใช้เป็นส่วนประกอบพื้นฐานในการทำงาน MaterialTheme เป็นออบเจ็กต์ที่ระบุอินสแตนซ์ CompositionLocal 3 รายการ ได้แก่ colorScheme, typography และ shapes ซึ่งช่วยให้คุณดึงข้อมูลอินสแตนซ์เหล่านี้ได้ในภายหลังในส่วนสืบทอดใดๆ ของ Composition โดยเฉพาะพร็อพเพอร์ตี้ LocalColorScheme, LocalShapes และ LocalTypography ที่คุณเข้าถึงได้ผ่านแอตทริบิวต์ MaterialTheme, colorScheme, shapes และ typography

@Composable
fun MyApp() {
    // Provides a Theme whose values are propagated down its `content`
    MaterialTheme {
        // New values for colorScheme, typography, and shapes are available
        // in MaterialTheme's content lambda.

        // ... content here ...
    }
}

// Some composable deep in the hierarchy of MaterialTheme
@Composable
fun SomeTextLabel(labelText: String) {
    Text(
        text = labelText,
        // `primary` is obtained from MaterialTheme's
        // LocalColors CompositionLocal
        color = MaterialTheme.colorScheme.primary
    )
}

อินสแตนซ์ CompositionLocal จะกำหนดขอบเขตไว้ที่ส่วนหนึ่งของ Composition คุณจึงระบุค่าที่แตกต่างกันในระดับต่างๆ ของโครงสร้างได้ ค่า current ของ CompositionLocal จะสอดคล้องกับค่าที่ใกล้เคียงที่สุดที่ได้จากระดับบนสุดในส่วนนั้นของ Composition

หากต้องการระบุค่าใหม่ให้กับ CompositionLocal ให้ใช้ CompositionLocalProvider และฟังก์ชัน infix provides ที่เชื่อมโยงคีย์ CompositionLocal กับ value content lambda ของ CompositionLocalProvider จะได้รับค่าที่ระบุเมื่อเข้าถึงพร็อพเพอร์ตี้ current ของ CompositionLocal เมื่อมีการระบุค่าใหม่ Compose จะจัดองค์ประกอบส่วนต่างๆ ของ Composition ที่อ่าน CompositionLocal ใหม่

ตัวอย่างเช่น LocalContentColor CompositionLocal มีสีเนื้อหาที่ต้องการซึ่งใช้สำหรับข้อความและระบบการตีความสัญลักษณ์เพื่อให้แน่ใจว่าตัดกับสีพื้นหลังปัจจุบัน ในตัวอย่างต่อไปนี้ มีการใช้ CompositionLocalProvider เพื่อระบุค่าที่แตกต่างกันสำหรับส่วนต่างๆ ของ Composition

@Composable
fun CompositionLocalExample() {
    MaterialTheme {
        // Surface provides contentColorFor(MaterialTheme.colorScheme.surface) by default
        // This is to automatically make text and other content contrast to the background
        // correctly.
        Surface {
            Column {
                Text("Uses Surface's provided content color")
                CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.primary) {
                    Text("Primary color provided by LocalContentColor")
                    Text("This Text also uses primary as textColor")
                    CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.error) {
                        DescendantExample()
                    }
                }
            }
        }
    }
}

@Composable
fun DescendantExample() {
    // CompositionLocalProviders also work across composable functions
    Text("This Text uses the error color now")
}

ตัวอย่างของคอมโพสิเบิล CompositionLocalExample
รูปที่ 1 ตัวอย่างของคอมโพสิเบิล CompositionLocalExample

ในตัวอย่างสุดท้าย มีการใช้อินสแตนซ์ CompositionLocal ภายในโดย Composable ของ Material หากต้องการเข้าถึงค่าปัจจุบันของ CompositionLocal ให้ใช้พร็อพเพอร์ตี้ current ในตัวอย่างต่อไปนี้ ระบบจะใช้ค่า Context ปัจจุบันของ CompositionLocal LocalContext ที่ใช้กันโดยทั่วไปในแอป Android เพื่อจัดรูปแบบข้อความ

@Composable
fun FruitText(fruitSize: Int) {
    // Get `resources` from the current value of LocalContext
    val resources = LocalContext.current.resources
    val fruitText = remember(resources, fruitSize) {
        resources.getQuantityString(R.plurals.fruit_title, fruitSize)
    }
    Text(text = fruitText)
}

สร้าง CompositionLocal ของคุณเอง

CompositionLocal เป็นเครื่องมือสำหรับส่งข้อมูลผ่าน Composition โดยนัย

สัญญาณสำคัญอีกอย่างหนึ่งสำหรับการใช้ CompositionLocal คือเมื่อพารามิเตอร์เป็นแบบข้ามส่วนและเลเยอร์ระดับกลางของการใช้งานไม่ควรทราบว่ามีอยู่ เนื่องจากการทำให้เลเยอร์ระดับกลางเหล่านั้นทราบจะจำกัดประโยชน์ของ Composable เช่น การค้นหาสิทธิ์ของ Android จะดำเนินการโดย CompositionLocal เบื้องหลัง คอมโพสิเบิลเครื่องมือเลือกสื่อสามารถเพิ่มฟังก์ชันการทำงานใหม่เพื่อเข้าถึงเนื้อหาที่ได้รับการคุ้มครองด้วยสิทธิ์ในอุปกรณ์โดยไม่ต้องเปลี่ยน API และกำหนดให้ผู้เรียกใช้เครื่องมือเลือกสื่อต้องทราบถึงบริบทที่เพิ่มเข้ามานี้ซึ่งใช้จากสภาพแวดล้อม

อย่างไรก็ตาม CompositionLocal ไม่ใช่วิธีแก้ปัญหาที่ดีที่สุดเสมอไป เรา ไม่แนะนำให้ใช้ CompositionLocal มากเกินไปเนื่องจากมีข้อเสียบางประการดังนี้

CompositionLocal ทำให้พฤติกรรมของ Composable มีเหตุผลยากขึ้น เนื่องจากสร้างทรัพยากร Dependency โดยนัย ผู้เรียกใช้ Composable ที่ใช้ทรัพยากรเหล่านั้นจึงต้องตรวจสอบว่าค่าสำหรับ CompositionLocal ทุกค่าเป็นไปตามเงื่อนไข

นอกจากนี้ อาจไม่มีแหล่งข้อมูลที่เชื่อถือได้ที่ชัดเจนสำหรับทรัพยากร Dependency นี้ เนื่องจากสามารถเปลี่ยนแปลงได้ในส่วนใดก็ได้ของ Composition ดังนั้น การแก้ไขข้อบกพร่องของแอปเมื่อเกิดปัญหาอาจเป็นเรื่องที่ท้าทายมากขึ้น เนื่องจากคุณต้องไปยังส่วนต่างๆ ของ Composition เพื่อดูว่ามีการระบุค่า current ไว้ที่ใด เครื่องมือต่างๆ เช่น Find usages ใน IDE หรือเครื่องมือตรวจสอบเลย์เอาต์ Compose จะให้ข้อมูลที่เพียงพอเพื่อลดปัญหานี้

เลือกว่าจะใช้ CompositionLocal หรือไม่

มีเงื่อนไขบางอย่างที่ทำให้ CompositionLocal เป็นโซลูชันที่ดีสำหรับกรณีการใช้งานของคุณ ดังนี้

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

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

หากกรณีการใช้งานของคุณไม่เป็นไปตามข้อกำหนดเหล่านี้ โปรดดูส่วนทางเลือกที่ควรพิจารณาก่อนสร้าง CompositionLocal

ตัวอย่างแนวทางปฏิบัติที่ไม่ดีคือการสร้าง CompositionLocal ที่เก็บ ViewModel ของหน้าจอหนึ่งๆ เพื่อให้ Composable ทั้งหมดในหน้าจอนั้นสามารถรับการอ้างอิงไปยัง ViewModel เพื่อดำเนินการตรรกะบางอย่างได้ ซึ่งเป็นแนวทางปฏิบัติที่ไม่ดีเนื่องจาก Composable บางรายการที่อยู่ใต้แผนผัง UI หนึ่งๆ ไม่จำเป็นต้องทราบเกี่ยวกับ ViewModel แนวทางปฏิบัติที่ดีคือการส่งข้อมูลที่จำเป็นไปยัง Composable เท่านั้น โดยทำตามรูปแบบที่สถานะไหลลงและเหตุการณ์ไหลขึ้น แนวทางนี้จะช่วยให้คอมโพสิเบิลของคุณนำกลับมาใช้ใหม่ได้มากขึ้นและทดสอบได้ง่ายขึ้น

สร้าง CompositionLocal

มี API 2 รายการสำหรับสร้าง CompositionLocal ดังนี้

  • compositionLocalOf: การเปลี่ยนค่าที่ระบุระหว่างการจัดองค์ประกอบใหม่จะทำให้เฉพาะเนื้อหาที่อ่านค่า current ไม่ถูกต้อง

  • staticCompositionLocalOf: Compose จะไม่ติดตามการอ่าน staticCompositionLocalOf ซึ่งต่างจาก compositionLocalOf การเปลี่ยนค่าจะทำให้แลมบ์ดา content ทั้งหมดที่มี CompositionLocal ถูกจัดองค์ประกอบใหม่ แทนที่จะเป็นเฉพาะตำแหน่งที่มีการอ่านค่า current ใน Composition

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

เช่น ระบบการออกแบบของแอปอาจมีแนวคิดเฉพาะตัวเกี่ยวกับวิธีที่ Composable ได้รับการยกระดับโดยใช้เงาสำหรับคอมโพเนนต์ UI เนื่องจากระดับความสูงที่แตกต่างกันสำหรับแอปควรเผยแพร่ไปทั่วทั้ง UI Tree เราจึงใช้ CompositionLocal เนื่องจากค่า CompositionLocal ได้มาตามเงื่อนไขโดยอิงตามธีมของระบบ เราจึงใช้ compositionLocalOf API ดังนี้

// LocalElevations.kt file

data class Elevations(val card: Dp = 0.dp, val default: Dp = 0.dp)

// Define a CompositionLocal global object with a default
// This instance can be accessed by all composables in the app
val LocalElevations = compositionLocalOf { Elevations() }

ระบุค่าให้กับ CompositionLocal

CompositionLocalProvider Composable จะเชื่อมโยงค่ากับอินสแตนซ์ CompositionLocal สำหรับลำดับชั้นที่ระบุ หากต้องการระบุค่าใหม่ให้กับ CompositionLocal ให้ใช้ฟังก์ชัน infix provides ที่เชื่อมโยงคีย์ CompositionLocal กับ value ดังนี้

// MyActivity.kt file

class MyActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setContent {
            // Calculate elevations based on the system theme
            val elevations = if (isSystemInDarkTheme()) {
                Elevations(card = 1.dp, default = 1.dp)
            } else {
                Elevations(card = 0.dp, default = 0.dp)
            }

            // Bind elevation as the value for LocalElevations
            CompositionLocalProvider(LocalElevations provides elevations) {
                // ... Content goes here ...
                // This part of Composition will see the `elevations` instance
                // when accessing LocalElevations.current
            }
        }
    }
}

การใช้ CompositionLocal

CompositionLocal.current จะแสดงผลค่าที่ได้จาก CompositionLocalProvider ที่ใกล้ที่สุดซึ่งให้ค่าแก่ CompositionLocal ดังกล่าว

@Composable
fun SomeComposable() {
    // Access the globally defined LocalElevations variable to get the
    // current Elevations in this part of the Composition
    MyCard(elevation = LocalElevations.current.card) {
        // Content
    }
}

ทางเลือกที่ควรพิจารณา

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

ส่งพารามิเตอร์ที่ชัดเจน

การระบุทรัพยากร Dependency ของ Composable อย่างชัดเจนเป็นแนวทางปฏิบัติที่ดี เราขอแนะนำให้คุณส่ง Composable เฉพาะสิ่งที่จำเป็นเท่านั้น เพื่อส่งเสริมการแยกส่วนและการนำ Composable กลับมาใช้ใหม่ Composable แต่ละรายการควรเก็บข้อมูลให้น้อยที่สุดเท่าที่จะเป็นไปได้

@Composable
fun MyComposable(myViewModel: MyViewModel = viewModel()) {
    // ...
    MyDescendant(myViewModel.data)
}

// Don't pass the whole object! Just what the descendant needs.
// Also, don't  pass the ViewModel as an implicit dependency using
// a CompositionLocal.
@Composable
fun MyDescendant(myViewModel: MyViewModel) { /* ... */ }

// Pass only what the descendant needs
@Composable
fun MyDescendant(data: DataToDisplay) {
    // Display data
}

Inversion of control

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

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

@Composable
fun MyComposable(myViewModel: MyViewModel = viewModel()) {
    // ...
    MyDescendant(myViewModel)
}

@Composable
fun MyDescendant(myViewModel: MyViewModel) {
    Button(onClick = { myViewModel.loadData() }) {
        Text("Load data")
    }
}

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

@Composable
fun MyComposable(myViewModel: MyViewModel = viewModel()) {
    // ...
    ReusableLoadDataButton(
        onLoadClick = {
            myViewModel.loadData()
        }
    )
}

@Composable
fun ReusableLoadDataButton(onLoadClick: () -> Unit) {
    Button(onClick = onLoadClick) {
        Text("Load data")
    }
}

วิธีนี้อาจเหมาะกับ Use Case บางกรณีมากกว่า เนื่องจากแยกองค์ประกอบย่อยออกจากระดับบนสุดที่อยู่ติดกัน คอมโพสิเบิลระดับบนมักจะมีความซับซ้อนมากขึ้นเพื่อให้มีคอมโพสิเบิลระดับล่างที่ยืดหยุ่นมากขึ้น

ในทำนองเดียวกัน คุณสามารถใช้แลมบ์ดาเนื้อหา @Composable ในลักษณะเดียวกันเพื่อให้ได้รับประโยชน์เหมือนกัน ดังนี้

@Composable
fun MyComposable(myViewModel: MyViewModel = viewModel()) {
    // ...
    ReusablePartOfTheScreen(
        content = {
            Button(
                onClick = {
                    myViewModel.loadData()
                }
            ) {
                Text("Confirm")
            }
        }
    )
}

@Composable
fun ReusablePartOfTheScreen(content: @Composable () -> Unit) {
    Column {
        // ...
        content()
    }
}