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

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

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

โดยปกติใน Compose ข้อมูลจะไหลลงผ่าน แผนผัง UI เป็นพารามิเตอร์ไปยังฟังก์ชันที่ประกอบกันได้แต่ละรายการ ซึ่งทำให้การอ้างอิงของ 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
    )
}

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

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

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 มีขอบเขตเป็นส่วนหนึ่งขององค์ประกอบ คุณจึงระบุค่าที่แตกต่างกันในระดับต่างๆ ของโครงสร้างแบบต้นไม้ได้ ค่า current ของ CompositionLocal จะสอดคล้องกับค่าที่ใกล้ที่สุดซึ่งระบุโดย องค์ประกอบระดับบนในส่วนนั้นขององค์ประกอบ

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

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

@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")
}

ตัวอย่างของคอมโพเนนต์ที่เขียนได้ด้วย Compose CompositionLocalExample
รูปที่ 1 ตัวอย่างของคอมโพเนนต์ CompositionLocalExample ที่เขียนด้วย Compose ได้

ในตัวอย่างสุดท้าย CompositionLocal อินสแตนซ์ถูกใช้ภายใน โดย Composable ของ Material หากต้องการเข้าถึงค่าปัจจุบันของ CompositionLocal ให้ใช้พร็อพเพอร์ตี้ current ในตัวอย่างต่อไปนี้ ค่า Context ปัจจุบันของ LocalContext CompositionLocal ที่ใช้กันทั่วไปในแอป 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 เบื้องหลัง Composables ของเครื่องมือเลือกสื่อ สามารถเพิ่มฟังก์ชันใหม่เพื่อเข้าถึงเนื้อหาที่ได้รับการปกป้องด้วยสิทธิ์ใน อุปกรณ์ได้โดยไม่ต้องเปลี่ยน API และกำหนดให้ผู้เรียกใช้เครื่องมือเลือกสื่อ ต้องทราบบริบทเพิ่มเติมนี้ที่ใช้จากสภาพแวดล้อม

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

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

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

สร้าง CompositionLocal

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

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

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

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

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

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

การระบุการขึ้นต่อกันของ 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
}

การผกผันของการควบคุม

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

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

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

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

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

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

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

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

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

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

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