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

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

ขอแนะนำ CompositionLocal

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

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

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

CompositionLocal คือสิ่งที่ธีม Material ใช้อยู่เบื้องหลัง MaterialTheme คือออบเจ็กต์ที่มีอินสแตนซ์ CompositionLocal 3 รายการ ได้แก่ colorScheme, typography และ shapes ซึ่งช่วยให้คุณเรียกข้อมูลอินสแตนซ์เหล่านั้นในภายหลังได้ในส่วนที่สืบทอดของคอมโพสิชัน กล่าวโดยละเอียดคือ พร็อพเพอร์ตี้ 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 ของ CompositionLocalProvider ซึ่งจะเชื่อมโยงคีย์ CompositionLocal กับ value content แลมดาของ CompositionLocalProvider จะได้รับค่าที่ระบุเมื่อเข้าถึงพร็อพเพอร์ตี้ current ของ CompositionLocal เมื่อระบุค่าใหม่ Compose จะจัดองค์ประกอบบางส่วนขององค์ประกอบที่อ่าน 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")
}

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

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

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

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

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

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

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

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

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

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

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

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

การสร้าง CompositionLocal

มี API 2 รายการสําหรับสร้าง CompositionLocal

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

  • staticCompositionLocalOf: Compose จะไม่ติดตามการอ่าน staticCompositionLocalOf ต่างจาก compositionLocalOf การเปลี่ยนค่าจะทำให้ทั้ง content lambda ที่มี CompositionLocal อยู่ต้องคอมโพสิชันใหม่ทั้งหมดแทนที่จะคอมโพสิชันเฉพาะตำแหน่งที่อ่านค่า current ใน Composition

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

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

การกลับการควบคุม

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

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

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

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

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

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

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