CompositionLocal
เป็นเครื่องมือสำหรับส่งผ่านข้อมูลไปยังองค์ประกอบโดยนัย ในหน้านี้ คุณจะได้เรียนรู้รายละเอียดเพิ่มเติมเกี่ยวกับCompositionLocal
วิธีสร้างCompositionLocal
ของคุณเอง และดูว่าCompositionLocal
เป็นโซลูชันที่ดีสำหรับกรณีการใช้งานของคุณหรือไม่
ขอแนะนำ 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
ซึ่งช่วยให้คุณเรียกข้อมูลอินสแตนซ์เหล่านั้นในภายหลังได้ในส่วนที่สืบทอดของคอมโพสิชัน
กล่าวโดยละเอียดคือ พร็อพเพอร์ตี้ 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
ของ CompositionLocalProvider
ซึ่งจะเชื่อมโยงคีย์ CompositionLocal
กับ value
content
แลมดาของ CompositionLocalProvider
จะได้รับค่าที่ระบุเมื่อเข้าถึงพร็อพเพอร์ตี้ current
ของ CompositionLocal
เมื่อระบุค่าใหม่ Compose จะจัดองค์ประกอบบางส่วนขององค์ประกอบที่อ่าน 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") }
รูปที่ 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
เป็นเครื่องมือสำหรับส่งข้อมูลผ่านองค์ประกอบโดยปริยาย
สัญญาณสำคัญอีกประการในการใช้ CompositionLocal
คือเมื่อพารามิเตอร์เป็นแบบตัดขวาง และเลเยอร์กลางของการใช้งานไม่ควรรับรู้ถึงพารามิเตอร์ดังกล่าว เนื่องจากการทำให้เลเยอร์กลางเหล่านั้นรับรู้จะจำกัดประโยชน์ของคอมโพสิเบิล เช่น การค้นหาสิทธิ์ของ Android นั้น CompositionLocal
จะเป็นผู้ดำเนินการ คอมโพสิชันเครื่องมือเลือกสื่อสามารถเพิ่มฟังก์ชันการทำงานใหม่เพื่อเข้าถึงเนื้อหาที่มีการป้องกันด้วยสิทธิ์ในอุปกรณ์ได้โดยไม่ต้องเปลี่ยน API และกำหนดให้ผู้เรียกใช้เครื่องมือเลือกสื่อต้องทราบถึงบริบทที่เพิ่มเข้ามานี้ซึ่งใช้จากสภาพแวดล้อม
อย่างไรก็ตาม CompositionLocal
ไม่ได้เป็นโซลูชันที่ดีที่สุดเสมอไป เราขอแนะนำว่าอย่าใช้ CompositionLocal
มากเกินไป เนื่องจากมีข้อเสียบางประการดังนี้
CompositionLocal
ทําให้คาดเดาลักษณะการทํางานของคอมโพสิเบิลได้ยากขึ้น เนื่องจากคอมโพสิเบิลเหล่านี้สร้างการพึ่งพาโดยนัย ผู้เรียกใช้คอมโพสิเบิลที่ใช้คอมโพสิเบิลเหล่านี้จึงต้องตรวจสอบว่าค่าสำหรับ CompositionLocal
ทุกรายการเป็นไปตามข้อกำหนด
นอกจากนี้ แหล่งข้อมูลที่เป็นความจริงของข้อกําหนดนี้อาจไม่ชัดเจน เนื่องจากอาจเปลี่ยนแปลงได้ในส่วนใดก็ได้ขององค์ประกอบ ดังนั้นการแก้ไขข้อบกพร่องของแอปเมื่อเกิดปัญหาจึงอาจทำได้ยากขึ้น เนื่องจากคุณต้องเลื่อนขึ้นไปยังส่วนประกอบเพื่อดูว่าป้อนค่า current
ไว้ที่ใด เครื่องมือต่างๆ เช่น ค้นหาการใช้งานใน IDE หรือเครื่องมือตรวจสอบเลย์เอาต์คอมโพสิต์จะให้ข้อมูลเพียงพอในการบรรเทาปัญหานี้
การเลือกว่าจะใช้ CompositionLocal
หรือไม่
เงื่อนไขบางอย่างที่ทําให้ CompositionLocal
เป็นโซลูชันที่ดีสําหรับกรณีการใช้งานของคุณมีดังนี้
CompositionLocal
ควรมีค่าเริ่มต้นที่ดี หากไม่มีค่าเริ่มต้น คุณต้องรับประกันว่านักพัฒนาแอปจะพบสถานการณ์ที่ไม่มีการระบุค่าสำหรับ CompositionLocal
นั้นยากมาก
การไม่ระบุค่าเริ่มต้นอาจทำให้เกิดปัญหาและความไม่สะดวกเมื่อสร้างการทดสอบหรือแสดงตัวอย่างคอมโพสิชันที่ใช้ CompositionLocal
นั้น จะต้องระบุค่าดังกล่าวอย่างชัดเจนเสมอ
หลีกเลี่ยงการใช้ CompositionLocal
สำหรับแนวคิดที่ไม่ได้คิดว่ามีขอบเขตระดับลําดับชั้นหรือมีขอบเขตระดับลําดับชั้นย่อย CompositionLocal
จะเหมาะสมเมื่ออาจมีการใช้โดยรายการที่สืบทอด ไม่ใช่เพียงบางรายการ
หาก Use Case ไม่เป็นไปตามข้อกำหนดเหล่านี้ โปรดดูส่วนทางเลือกอื่นๆ ที่ควรพิจารณาก่อนสร้างCompositionLocal
ตัวอย่างของแนวทางปฏิบัติที่ไม่ถูกต้องคือการสร้าง CompositionLocal
ที่มี ViewModel
ของหน้าจอหนึ่งๆ เพื่อให้คอมโพสิเบิลทั้งหมดในหน้าจอนั้นอ้างอิง ViewModel
เพื่อทำตรรกะบางอย่างได้ แนวทางนี้ไม่ถูกต้องเนื่องจากคอมโพสิเบิลบางรายการที่อยู่ใต้ต้นไม้ UI บางต้นไม่จำเป็นต้องทราบเกี่ยวกับViewModel
แนวทางปฏิบัติแนะนำคือส่งเฉพาะข้อมูลที่คอมโพสิเบิลต้องการตามรูปแบบที่สถานะไหลลงและเหตุการณ์ไหลขึ้น วิธีนี้จะทำให้คอมโพสิเบิลของคุณนํากลับมาใช้ซ้ำได้มากขึ้นและทดสอบได้ง่ายขึ้น
การสร้าง CompositionLocal
มี API 2 รายการสําหรับสร้าง CompositionLocal
compositionLocalOf
: การเปลี่ยนแปลงค่าที่ระบุระหว่างการจัดเรียงใหม่จะลบล้างเฉพาะเนื้อหาที่อ่านค่าของcurrent
ค่านั้นstaticCompositionLocalOf
: Compose จะไม่ติดตามการอ่านstaticCompositionLocalOf
ต่างจากcompositionLocalOf
การเปลี่ยนค่าจะทำให้ทั้งcontent
lambda ที่มีCompositionLocal
อยู่ต้องคอมโพสิชันใหม่ทั้งหมดแทนที่จะคอมโพสิชันเฉพาะตำแหน่งที่อ่านค่าcurrent
ใน Composition
หากค่าที่ระบุใน CompositionLocal
มีแนวโน้มที่จะเปลี่ยนแปลงน้อยมากหรือจะไม่มีการเปลี่ยนแปลงเลย ให้ใช้ staticCompositionLocalOf
เพื่อรับประโยชน์ด้านประสิทธิภาพ
เช่น ระบบการออกแบบของแอปอาจกำหนดแนวทางในการแสดงคอมโพสิเบิลโดยใช้เงาสำหรับคอมโพเนนต์ 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
จะเชื่อมโยงค่ากับอินสแตนซ์ CompositionLocal
สำหรับลําดับชั้นที่ระบุ หากต้องการระบุค่าใหม่ให้กับ CompositionLocal
ให้ใช้ 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 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
นำมาใช้ซ้ำได้น้อยลงเนื่องจากตอนนี้มีการเชื่อมโยงกัน ลองพิจารณาทางเลือกที่ไม่ส่งค่าพึ่งพาไปยังรายการที่สืบทอดและใช้หลักการผนวกการควบคุมซึ่งทําให้บรรพบุรุษมีหน้าที่รับผิดชอบในการใช้ตรรกะ
@Composable fun MyComposable(myViewModel: MyViewModel = viewModel()) { // ... ReusableLoadDataButton( onLoadClick = { myViewModel.loadData() } ) } @Composable fun ReusableLoadDataButton(onLoadClick: () -> Unit) { Button(onClick = onLoadClick) { Text("Load data") } }
แนวทางนี้อาจเหมาะกับบางกรณีการใช้งานมากกว่า เนื่องจากแยกรายการย่อยออกจากบรรพบุรุษโดยตรง คอมโพสิเบิลหลักมักจะมีความซับซ้อนมากขึ้นเพื่อให้คอมโพสิเบิลระดับล่างมีความยืดหยุ่นมากขึ้น
ในทํานองเดียวกัน คุณสามารถใช้ @Composable
Content Lambda ในลักษณะเดียวกันเพื่อรับผลประโยชน์เดียวกัน
@Composable fun MyComposable(myViewModel: MyViewModel = viewModel()) { // ... ReusablePartOfTheScreen( content = { Button( onClick = { myViewModel.loadData() } ) { Text("Confirm") } } ) } @Composable fun ReusablePartOfTheScreen(content: @Composable () -> Unit) { Column { // ... content() } }
แนะนำสำหรับคุณ
- หมายเหตุ: ข้อความลิงก์จะแสดงเมื่อ JavaScript ปิดอยู่
- องค์ประกอบของธีมในเครื่องมือเขียน
- การใช้มุมมองในโหมดเขียน
- Kotlin สำหรับ Jetpack Compose