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

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

ขอแนะนำ CompositionLocal

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

@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 เป็นพารามิเตอร์ในฟังก์ชันแบบคอมโพสิเบิล

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 Composable

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

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

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

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

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

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

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

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

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

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

กำลังสร้าง CompositionLocal

มี API 2 รายการในการสร้าง CompositionLocal ดังนี้

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

  • staticCompositionLocalOf สิ่งที่ต่างจาก compositionLocalOf ตรงที่การอ่าน staticCompositionLocalOf ไม่ต่างจาก Compose ติดตามอยู่ การเปลี่ยนค่าจะทำให้ทั้ง 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 Composable จะเชื่อมโยงค่ากับอินสแตนซ์ 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 นั้นเป็นลักษณะนิสัยที่ดี คำแนะนำจากเรา คุณส่งผ่าน Composables เฉพาะส่วนที่จำเป็น เพื่อกระตุ้นการแยกออกจากกัน และ 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
}

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

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

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

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

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

ในทํานองเดียวกัน คุณสามารถใช้ @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()
    }
}