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

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

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

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

กำลังตัดสินใจว่าจะใช้ CompositionLocal หรือไม่

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

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

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

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

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

การสร้าง CompositionLocal

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

  • compositionLocalOf: การเปลี่ยนแปลงค่าที่ระบุระหว่างการจัดเรียงใหม่จะทำให้เนื้อหาที่อ่านค่านั้นๆ current เป็นโมฆะเท่านั้น

  • staticCompositionLocalOf: Compose ต่างจาก compositionLocalOf ตรงที่การอ่าน staticCompositionLocalOf จะไม่ติดตาม การเปลี่ยนค่าจะทำให้ทั้ง 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 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 หรือไม่ โซลูชันอื่นอาจเหมาะกับกรณีการใช้งานของคุณมากกว่า

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

การระบุอย่างชัดแจ้งเกี่ยวกับทรัพยากร 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
}

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

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

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

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

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

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

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

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