CompositionLocal เป็นเครื่องมือสำหรับส่งข้อมูลผ่าน Composition โดยนัย ในหน้านี้ คุณจะได้ทราบรายละเอียดเพิ่มเติมเกี่ยวกับ CompositionLocal, วิธีสร้าง CompositionLocal ของคุณเอง และดูว่า CompositionLocal เป็นโซลูชันที่เหมาะกับกรณีการใช้งานของคุณหรือไม่
ข้อมูลเบื้องต้นเกี่ยวกับ CompositionLocal
โดยปกติแล้วใน Compose ข้อมูลจะไหลลงผ่านแผนผัง UI เป็นพารามิเตอร์ไปยังฟังก์ชันที่ประกอบกันได้แต่ละฟังก์ชัน ซึ่งจะทำให้ทรัพยากร Dependency ของ Composable ชัดเจน อย่างไรก็ตาม วิธีนี้อาจยุ่งยากสำหรับข้อมูลที่มีการใช้งานบ่อยและแพร่หลาย เช่น สีหรือรูปแบบตัวอักษร โปรดดูตัวอย่างต่อไปนี้
@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 เป็นพารามิเตอร์ในฟังก์ชันที่ประกอบกันได้
คำสำคัญ: ในคู่มือนี้ เราใช้คำว่า Composition, UI Tree และลำดับชั้นของ UI แม้ว่าบางครั้งอาจใช้แทนกันได้ในคู่มืออื่นๆ แต่ก็มีความหมายที่แตกต่างกัน ดังนี้
- Composition คือบันทึกของกราฟการเรียกใช้ฟังก์ชันที่ประกอบกันได้
- แผนผัง UI หรือลำดับชั้น UI คือแผนผังของ
LayoutNodeที่สร้าง อัปเดต และดูแลรักษาโดยกระบวนการจัดองค์ประกอบ
CompositionLocal คือสิ่งที่ธีม Material ใช้เป็นส่วนประกอบพื้นฐานในการทำงาน
MaterialTheme เป็นออบเจ็กต์ที่ระบุอินสแตนซ์ CompositionLocal 3 รายการ ได้แก่ colorScheme, typography และ shapes ซึ่งช่วยให้คุณดึงข้อมูลอินสแตนซ์เหล่านี้ได้ในภายหลังในส่วนสืบทอดใดๆ ของ Composition
โดยเฉพาะพร็อพเพอร์ตี้ 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 จะกำหนดขอบเขตไว้ที่ส่วนหนึ่งของ Composition คุณจึงระบุค่าที่แตกต่างกันในระดับต่างๆ ของโครงสร้างได้ ค่า current ของ CompositionLocal จะสอดคล้องกับค่าที่ใกล้เคียงที่สุดที่ได้จากระดับบนสุดในส่วนนั้นของ Composition
หากต้องการระบุค่าใหม่ให้กับ CompositionLocal ให้ใช้
CompositionLocalProvider
และฟังก์ชัน infix provides
ที่เชื่อมโยงคีย์ CompositionLocal กับ value content lambda ของ CompositionLocalProvider จะได้รับค่าที่ระบุเมื่อเข้าถึงพร็อพเพอร์ตี้ current ของ CompositionLocal เมื่อมีการระบุค่าใหม่ 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ในตัวอย่างสุดท้าย มีการใช้อินสแตนซ์ CompositionLocal ภายในโดย Composable ของ Material หากต้องการเข้าถึงค่าปัจจุบันของ CompositionLocal ให้ใช้พร็อพเพอร์ตี้ current ในตัวอย่างต่อไปนี้ ระบบจะใช้ค่า Context ปัจจุบันของ CompositionLocal LocalContext ที่ใช้กันโดยทั่วไปในแอป 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 คือเมื่อพารามิเตอร์เป็นแบบข้ามส่วนและเลเยอร์ระดับกลางของการใช้งานไม่ควรทราบว่ามีอยู่ เนื่องจากการทำให้เลเยอร์ระดับกลางเหล่านั้นทราบจะจำกัดประโยชน์ของ Composable เช่น การค้นหาสิทธิ์ของ Android จะดำเนินการโดย CompositionLocal เบื้องหลัง คอมโพสิเบิลเครื่องมือเลือกสื่อสามารถเพิ่มฟังก์ชันการทำงานใหม่เพื่อเข้าถึงเนื้อหาที่ได้รับการคุ้มครองด้วยสิทธิ์ในอุปกรณ์โดยไม่ต้องเปลี่ยน API และกำหนดให้ผู้เรียกใช้เครื่องมือเลือกสื่อต้องทราบถึงบริบทที่เพิ่มเข้ามานี้ซึ่งใช้จากสภาพแวดล้อม
อย่างไรก็ตาม CompositionLocal ไม่ใช่วิธีแก้ปัญหาที่ดีที่สุดเสมอไป เรา
ไม่แนะนำให้ใช้ CompositionLocal มากเกินไปเนื่องจากมีข้อเสียบางประการดังนี้
CompositionLocal ทำให้พฤติกรรมของ Composable มีเหตุผลยากขึ้น เนื่องจากสร้างทรัพยากร Dependency โดยนัย ผู้เรียกใช้ Composable ที่ใช้ทรัพยากรเหล่านั้นจึงต้องตรวจสอบว่าค่าสำหรับ CompositionLocal ทุกค่าเป็นไปตามเงื่อนไข
นอกจากนี้ อาจไม่มีแหล่งข้อมูลที่เชื่อถือได้ที่ชัดเจนสำหรับทรัพยากร Dependency นี้ เนื่องจากสามารถเปลี่ยนแปลงได้ในส่วนใดก็ได้ของ Composition ดังนั้น การแก้ไขข้อบกพร่องของแอปเมื่อเกิดปัญหาอาจเป็นเรื่องที่ท้าทายมากขึ้น เนื่องจากคุณต้องไปยังส่วนต่างๆ ของ Composition เพื่อดูว่ามีการระบุค่า current ไว้ที่ใด เครื่องมือต่างๆ เช่น Find usages ใน IDE หรือเครื่องมือตรวจสอบเลย์เอาต์ Compose จะให้ข้อมูลที่เพียงพอเพื่อลดปัญหานี้
เลือกว่าจะใช้ CompositionLocal หรือไม่
มีเงื่อนไขบางอย่างที่ทำให้ CompositionLocal เป็นโซลูชันที่ดีสำหรับกรณีการใช้งานของคุณ ดังนี้
CompositionLocal ควรมีค่าเริ่มต้นที่ดี หากไม่มีค่าเริ่มต้น คุณต้องรับประกันว่านักพัฒนาแอปจะเข้าถึงสถานการณ์ที่ไม่มีการระบุค่าสำหรับ CompositionLocal ได้ยากมาก
การไม่ระบุค่าเริ่มต้นอาจทำให้เกิดปัญหาและความยุ่งยากเมื่อสร้างการทดสอบหรือแสดงตัวอย่าง Composable ที่ใช้ CompositionLocal นั้น ซึ่งจะต้องระบุอย่างชัดเจนเสมอ
หลีกเลี่ยง CompositionLocal สำหรับแนวคิดที่ไม่ได้มองว่าเป็นขอบเขตแบบต้นไม้หรือขอบเขตลำดับชั้นย่อย CompositionLocal จะเหมาะสมเมื่อสามารถใช้ได้โดยองค์ประกอบสืบทอดใดๆ ไม่ใช่เพียงบางองค์ประกอบ
หากกรณีการใช้งานของคุณไม่เป็นไปตามข้อกำหนดเหล่านี้ โปรดดูส่วนทางเลือกที่ควรพิจารณาก่อนสร้าง CompositionLocal
ตัวอย่างแนวทางปฏิบัติที่ไม่ดีคือการสร้าง CompositionLocal ที่เก็บ ViewModel ของหน้าจอหนึ่งๆ เพื่อให้ Composable ทั้งหมดในหน้าจอนั้นสามารถรับการอ้างอิงไปยัง ViewModel เพื่อดำเนินการตรรกะบางอย่างได้ ซึ่งเป็นแนวทางปฏิบัติที่ไม่ดีเนื่องจาก Composable บางรายการที่อยู่ใต้แผนผัง UI หนึ่งๆ ไม่จำเป็นต้องทราบเกี่ยวกับ ViewModel แนวทางปฏิบัติที่ดีคือการส่งข้อมูลที่จำเป็นไปยัง Composable เท่านั้น โดยทำตามรูปแบบที่สถานะไหลลงและเหตุการณ์ไหลขึ้น
แนวทางนี้จะช่วยให้คอมโพสิเบิลของคุณนำกลับมาใช้ใหม่ได้มากขึ้นและทดสอบได้ง่ายขึ้น
สร้าง CompositionLocal
มี API 2 รายการสำหรับสร้าง CompositionLocal ดังนี้
compositionLocalOf: การเปลี่ยนค่าที่ระบุระหว่างการจัดองค์ประกอบใหม่จะทำให้เฉพาะเนื้อหาที่อ่านค่าcurrentไม่ถูกต้องstaticCompositionLocalOf: Compose จะไม่ติดตามการอ่านstaticCompositionLocalOfซึ่งต่างจากcompositionLocalOfการเปลี่ยนค่าจะทำให้แลมบ์ดาcontentทั้งหมดที่มีCompositionLocalถูกจัดองค์ประกอบใหม่ แทนที่จะเป็นเฉพาะตำแหน่งที่มีการอ่านค่าcurrentใน Composition
หากค่าที่ระบุให้กับ CompositionLocal มีแนวโน้มต่ำมากที่จะเปลี่ยนแปลงหรือไม่เปลี่ยนแปลงเลย ให้ใช้ staticCompositionLocalOf เพื่อให้ได้ประโยชน์ด้านประสิทธิภาพ
เช่น ระบบการออกแบบของแอปอาจมีแนวคิดเฉพาะตัวเกี่ยวกับวิธีที่ Composable ได้รับการยกระดับโดยใช้เงาสำหรับคอมโพเนนต์ UI เนื่องจากระดับความสูงที่แตกต่างกันสำหรับแอปควรเผยแพร่ไปทั่วทั้ง UI Tree เราจึงใช้ 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 ให้ใช้ฟังก์ชัน infix 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 อย่างชัดเจนเป็นแนวทางปฏิบัติที่ดี เราขอแนะนำให้คุณส่ง Composable เฉพาะสิ่งที่จำเป็นเท่านั้น เพื่อส่งเสริมการแยกส่วนและการนำ 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 }
Inversion of control
อีกวิธีหนึ่งในการหลีกเลี่ยงการส่งทรัพยากร Dependency ที่ไม่จำเป็นไปยัง Composable คือการใช้การกลับด้านการควบคุม โดยคอมโพเนนต์หลักจะรับทรัพยากร 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