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

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

@Composable
fun MyApp() {
    // Provides a Theme whose values are propagated down its `content`
    MaterialTheme {
        // New values for colors, 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.colors.primary
    )
}

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

หากต้องการระบุค่าใหม่ให้กับ CompositionLocal ให้ใช้พารามิเตอร์ CompositionLocalProvider และ provides ฟังก์ชัน infix ที่เชื่อมโยงคีย์ CompositionLocal กับ value แลมบ์ดา content จาก CompositionLocalProvider จะได้รับข้อมูล เมื่อเข้าถึงคุณสมบัติ current ของ CompositionLocal เมื่อ มีการระบุค่าใหม่ "เขียน" จะจัดองค์ประกอบส่วนต่างๆ ของการเรียบเรียงที่อ่าน CompositionLocal

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

@Composable
fun CompositionLocalExample() {
    MaterialTheme { // MaterialTheme sets ContentAlpha.high as default
        Column {
            Text("Uses MaterialTheme's provided alpha")
            CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) {
                Text("Medium value provided for LocalContentAlpha")
                Text("This Text also uses the medium value")
                CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.disabled) {
                    DescendantExample()
                }
            }
        }
    }
}

@Composable
fun DescendantExample() {
    // CompositionLocalProviders also work across composable functions
    Text("This Text uses the disabled alpha now")
}

รูปที่ 1 ตัวอย่างของ CompositionLocalExample Composable

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

นอกจากนี้ ทรัพยากร Dependency นี้อาจไม่มีแหล่งที่มาที่ชัดเจนแน่นอน ซึ่งอาจเปลี่ยนแปลงในส่วนใดก็ได้ของการเรียบเรียง ดังนั้น การแก้ไขข้อบกพร่องของแอปเมื่อ ก็เป็นเรื่องที่ท้าทายยิ่งขึ้น เพราะคุณต้องสำรวจ องค์ประกอบเพื่อดูตำแหน่งที่ระบุค่า current เครื่องมือต่างๆ เช่น Find ใน 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: สิ่งที่ต่างจาก compositionLocalOf ตรงที่การอ่าน staticCompositionLocalOf ไม่ต่างจาก Compose ติดตามอยู่ การเปลี่ยนค่าจะส่งผลให้เกิด 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
    Card(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
}

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

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

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

@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 lambda ก็สามารถใช้ในลักษณะเดียวกันได้ สิทธิประโยชน์เดียวกัน

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

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