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 เป็นพารามิเตอร์ในฟังก์ชันที่ประกอบกันได้
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 value
ของ CompositionLocal จะสอดคล้องกับค่าที่ใกล้ที่สุดซึ่งระบุโดย
ระดับบนในส่วนนั้นของ Composition
หากต้องการระบุค่าใหม่ให้กับ CompositionLocal ให้ใช้
CompositionLocalProvider
และฟังก์ชัน provides
infix ซึ่งเชื่อมโยงคีย์ CompositionLocal กับ value แลมบ์ดาของ CompositionLocalProvider จะได้รับค่าที่ระบุเมื่อเข้าถึงพร็อพเพอร์ตี้ CompositionLocalcontentcurrent เมื่อมีการระบุค่าใหม่ Compose จะทำการจัดองค์ประกอบใหม่ในส่วนต่างๆ ของ 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") }
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 ทุกรายการ
นอกจากนี้ อาจไม่มีแหล่งข้อมูลที่เชื่อถือได้ที่ชัดเจนสำหรับการขึ้นอยู่กันนี้ เนื่องจากค่าอาจเปลี่ยนแปลงได้ในส่วนใดก็ได้ของ Composition ดังนั้น การแก้ไขข้อบกพร่องของแอปเมื่อเกิดปัญหาจึงอาจทำได้ยากขึ้น เนื่องจากคุณต้องไปยังส่วนบนของ Composition เพื่อดูว่ามีการระบุค่า current ไว้ที่ใด เครื่องมือต่างๆ เช่น Find
usages ใน 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ได้รับการจัดองค์ประกอบใหม่ แทนที่จะเป็นเพียงตำแหน่งที่มีการอ่านค่า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 }
การผกผันการควบคุม
อีกวิธีหนึ่งในการหลีกเลี่ยงการส่งการขึ้นอยู่ที่ไม่จำเป็นไปยังฟังก์ชันที่ประกอบกันได้คือการใช้ การผกผันการควบคุม แทนที่องค์ประกอบสืบทอดจะรับทรัพยากร 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 นำกลับมาใช้ซ้ำได้น้อยลงเนื่องจากตอนนี้ฟังก์ชันทั้ง 2 เชื่อมโยงกัน ลองพิจารณาทางเลือกที่ไม่ส่งทรัพยากร Dependency ไปยังองค์ประกอบสืบทอดและใช้หลักการผกผันการควบคุมซึ่งทำให้ระดับบนมีหน้าที่รับผิดชอบในการดำเนินการตรรกะ
@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() } }
แนะนำสำหรับคุณ
- หมายเหตุ: ข้อความลิงก์จะแสดงเมื่อ JavaScript ปิดอยู่
- องค์ประกอบของธีมใน Compose
- การใช้ View ใน Compose
- Kotlin สำหรับ Jetpack Compose