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

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

ข้อมูลเบื้องต้นเกี่ยวกับ 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 เป็นพารามิเตอร์ในฟังก์ชันที่ประกอบกันได้

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

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

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

@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 และฟังก์ชัน provides infix ซึ่งเชื่อมโยงคีย์ CompositionLocal กับ value แลมบ์ดาของ CompositionLocalProvider จะได้รับค่าที่ระบุเมื่อเข้าถึงพร็อพเพอร์ตี้ CompositionLocalcontentcurrent เมื่อมีการระบุค่าใหม่ Compose จะทำการ Recompose ส่วนต่างๆ ของ 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")
}

ตัวอย่างของ Composable CompositionLocalExample
รูปที่ 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 เป็นเครื่องมือสำหรับส่งข้อมูลลงใน Composition โดยนัย

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

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

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

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

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

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

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

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

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

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

สร้าง CompositionLocal

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

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

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

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

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

// 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 อาจเป็นโซลูชันที่มากเกินไปสำหรับ Use Case บางรายการ หาก Use Case ของคุณไม่เป็นไปตามเกณฑ์ที่ระบุไว้ในส่วนการตัดสินใจว่าจะใช้ CompositionLocal หรือไม่ โซลูชันอื่นอาจเหมาะกับ Use Case ของคุณมากกว่า

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

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

@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
fun MyComposable(myViewModel: MyViewModel = viewModel()) {
    // ...
    MyDescendant(myViewModel)
}

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

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

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