CompositionLocal เป็นเครื่องมือสำหรับ
ส่งข้อมูลผ่าน Composition โดยนัย ในหน้านี้ คุณจะได้เรียนรู้รายละเอียดเพิ่มเติมเกี่ยวกับCompositionLocal วิธีสร้างCompositionLocalของคุณเอง และดูว่าCompositionLocalเป็นโซลูชันที่ดีสำหรับกรณีการใช้งานของคุณหรือไม่
ข้อมูลเบื้องต้นเกี่ยวกับ CompositionLocal
โดยปกติใน Compose ข้อมูลจะไหลลงผ่าน แผนผัง UI เป็นพารามิเตอร์ไปยังฟังก์ชันที่ประกอบกันได้แต่ละฟังก์ชัน ซึ่งทำให้การอ้างอิงของ 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 ) }
Compose มี CompositionLocal ซึ่งช่วยให้คุณสร้างออบเจ็กต์ที่มีชื่อซึ่งกำหนดขอบเขตแบบทรีได้ ซึ่งสามารถใช้เป็นวิธีโดยนัยในการทำให้โฟลว์ข้อมูลไหลผ่านทรี UI เพื่อรองรับการไม่ต้องส่งสีเป็นพารามิเตอร์ที่ขึ้นกับทรัพยากร Dependency อย่างชัดเจนไปยัง Composable ส่วนใหญ่
โดยปกติแล้ว องค์ประกอบ CompositionLocal จะมีค่าในโหนดหนึ่งๆ
ของทรี UI ลูกหลานที่ประกอบกันได้จะใช้ค่าดังกล่าวได้โดยไม่ต้องประกาศ CompositionLocal เป็นพารามิเตอร์ในฟังก์ชันที่ประกอบกันได้
คำศัพท์สำคัญ: ในคู่มือนี้ เราใช้คำว่าการจัดองค์ประกอบ แผนผัง UI และลำดับชั้น UI แม้ว่าในคำแนะนำอื่นๆ จะใช้คำเหล่านี้แทนกันได้ แต่คำเหล่านี้มีความหมายแตกต่างกัน ดังนี้
- การเรียบเรียงคือบันทึกของกราฟการเรียกใช้ฟังก์ชันที่สามารถเรียบเรียงได้
- แผนผัง UI หรือลำดับชั้น UI คือแผนผังของ
LayoutNodeที่สร้าง อัปเดต และดูแลรักษาโดยกระบวนการจัดองค์ประกอบ
CompositionLocal คือสิ่งที่ธีม Material ใช้เบื้องหลัง
MaterialTheme คือ
ออบเจ็กต์ที่มีอินสแตนซ์ 3 รายการของ CompositionLocal ได้แก่ 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
infix ที่เชื่อมโยงคีย์ CompositionLocal กับ value
contentLambda ของ CompositionLocalProvider จะได้รับค่า
ที่ระบุเมื่อเข้าถึงพร็อพเพอร์ตี้ current ของ CompositionLocal เมื่อมีการระบุค่าใหม่ Compose จะสร้างส่วนต่างๆ ของ Composition ที่อ่าน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") }
CompositionLocalExample ที่เขียนด้วย Compose ได้ในตัวอย่างสุดท้าย CompositionLocal อินสแตนซ์ถูกใช้ภายใน
โดย Composable ของ Material หากต้องการเข้าถึงค่าปัจจุบันของ 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 คือเมื่อพารามิเตอร์เป็น
เลเยอร์การใช้งานที่ตัดขวางและระดับกลางไม่ควรทราบ
ว่ามีอยู่ เนื่องจากหากทำให้เลเยอร์ระดับกลางเหล่านั้นทราบจะจำกัด
ประโยชน์ของ Composable เช่น การค้นหาสิทธิ์ของ Android จะดำเนินการโดย CompositionLocal เบื้องหลัง Composable ของเครื่องมือเลือกสื่อ
สามารถเพิ่มฟังก์ชันใหม่เพื่อเข้าถึงเนื้อหาที่ได้รับการปกป้องด้วยสิทธิ์ใน
อุปกรณ์โดยไม่ต้องเปลี่ยน API และกำหนดให้ผู้เรียกใช้เครื่องมือเลือกสื่อ
ต้องทราบบริบทเพิ่มเติมนี้ที่ใช้จากสภาพแวดล้อม
อย่างไรก็ตาม CompositionLocal ไม่ใช่โซลูชันที่ดีที่สุดเสมอไป เรา
ไม่แนะนำให้ใช้ CompositionLocal มากเกินไปเนื่องจากมีข้อเสียบางประการดังนี้
CompositionLocal ทำให้คาดเดาพฤติกรรมของ Composable ได้ยากขึ้น เนื่องจาก
ค่าเหล่านี้สร้างการอ้างอิงโดยนัย ผู้เรียกใช้ Composable ที่ใช้ค่าเหล่านี้จึงต้อง
ตรวจสอบว่ามีค่าสำหรับ CompositionLocal ทุกรายการ
นอกจากนี้ อาจไม่มีแหล่งข้อมูลที่ชัดเจนสำหรับ Dependency นี้เนื่องจากสามารถเปลี่ยนแปลงได้ในส่วนใดก็ได้ของ Composition ดังนั้น การแก้ไขข้อบกพร่องของแอปเมื่อเกิดปัญหาจึงอาจทำได้ยากขึ้น เนื่องจากคุณต้องไปยังส่วนต่างๆ ของ
การจัดองค์ประกอบเพื่อดูว่ามีการระบุค่า current ไว้ที่ใด เครื่องมือต่างๆ เช่น Find
usages ใน 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การอ่านstaticCompositionLocalOfจะไม่ได้รับการติดตามโดย Compose ซึ่งแตกต่างจากcompositionLocalOfการเปลี่ยนค่าจะทําให้contentLambda ทั้งหมดที่มี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 MyCard(elevation = LocalElevations.current.card) { // Content } }
ทางเลือกที่ควรพิจารณา
CompositionLocalอาจเป็นโซลูชันที่มากเกินไปสำหรับกรณีการใช้งานบางอย่าง หากกรณีการใช้งานของคุณไม่เป็นไปตามเกณฑ์ที่ระบุไว้ในส่วนการตัดสินใจว่าจะใช้ CompositionLocal หรือไม่ โซลูชันอื่นอาจเหมาะกับกรณีการใช้งานของคุณมากกว่า
ส่งพารามิเตอร์ที่ชัดเจน
การระบุการขึ้นต่อกันของ 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 }
การผกผันของการควบคุม
อีกวิธีในการหลีกเลี่ยงการส่งการขึ้นต่อกันที่ไม่จำเป็นไปยัง 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") } }
วิธีนี้อาจเหมาะกับกรณีการใช้งานบางอย่างมากกว่า เนื่องจากแยกองค์ประกอบย่อยออกจากองค์ประกอบหลักที่อยู่ติดกัน Composable ระดับบนมักจะมีความซับซ้อนมากขึ้น เพื่อรองรับ Composable ระดับล่างที่มีความยืดหยุ่นมากขึ้น
ในทำนองเดียวกัน คุณสามารถใช้ Lambda ของเนื้อหา @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