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
เป็นพารามิเตอร์ในฟังก์ชันแบบคอมโพสิเบิล
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
Composable
ในตัวอย่างล่าสุด คอมโพสิชัน 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
คือเมื่อพารามิเตอร์
ไม่ควรคํานึงถึงการใช้งานแบบข้ามคัตและระดับกลาง
มีอยู่จริง เนื่องจากการทำให้เลเยอร์กลางเหล่านั้นรับรู้ได้นั้นจะจำกัด
ของ Composable เช่น การค้นหาสิทธิ์ของ Android คือ
ในราคา CompositionLocal
ที่ซ่อนอยู่ คอมโพสิชันเครื่องมือเลือกสื่อสามารถเพิ่มฟังก์ชันการทำงานใหม่เพื่อเข้าถึงเนื้อหาที่มีการป้องกันด้วยสิทธิ์ในอุปกรณ์ได้โดยไม่ต้องเปลี่ยน API และกำหนดให้ผู้เรียกใช้เครื่องมือเลือกสื่อต้องทราบถึงบริบทที่เพิ่มเข้ามานี้ซึ่งใช้จากสภาพแวดล้อม
อย่างไรก็ตาม CompositionLocal
อาจไม่ใช่วิธีแก้ปัญหาที่ดีที่สุดเสมอไป เราขอแนะนำว่าอย่าใช้ CompositionLocal
มากเกินไป เนื่องจากมีข้อเสียบางประการดังนี้
CompositionLocal
ทำให้ลักษณะการทำงานของ Composable ยากต่อการให้เหตุผล เนื่องจากคอมโพสิเบิลเหล่านี้สร้างการพึ่งพาโดยนัย ผู้เรียกใช้คอมโพสิเบิลที่ใช้คอมโพสิเบิลเหล่านี้จึงต้องตรวจสอบว่าค่าสำหรับ CompositionLocal
ทุกรายการเป็นไปตามข้อกำหนด
นอกจากนี้ ทรัพยากร Dependency นี้อาจไม่มีแหล่งที่มาที่ชัดเจนแน่นอน
ซึ่งอาจเปลี่ยนแปลงในส่วนใดก็ได้ของการเรียบเรียง ดังนั้น การแก้ไขข้อบกพร่องของแอปเมื่อ
ก็เป็นเรื่องที่ท้าทายยิ่งขึ้น เพราะคุณต้องสำรวจ
องค์ประกอบเพื่อดูตำแหน่งที่ระบุค่า current
เครื่องมือต่างๆ เช่น ค้นหาการใช้งานใน IDE หรือเครื่องมือตรวจสอบเลย์เอาต์คอมโพสิต์จะให้ข้อมูลเพียงพอในการบรรเทาปัญหานี้
กำลังตัดสินใจว่าจะใช้ CompositionLocal
หรือไม่
เงื่อนไขบางอย่างที่ทําให้ CompositionLocal
เป็นโซลูชันที่ดีสําหรับกรณีการใช้งานของคุณมีดังนี้
CompositionLocal
ควรมีค่าเริ่มต้นที่ดี หากไม่มีค่าเริ่มต้น คุณต้องรับประกันว่านักพัฒนาแอปจะพบสถานการณ์ที่ไม่มีการระบุค่าสำหรับ CompositionLocal
นั้นยากมาก
การไม่ระบุค่าเริ่มต้นอาจทำให้เกิดปัญหาและความไม่สะดวกเมื่อสร้างการทดสอบหรือแสดงตัวอย่างคอมโพสิชันที่ใช้ CompositionLocal
นั้น จะต้องระบุค่าดังกล่าวอย่างชัดเจนเสมอ
หลีกเลี่ยงการใช้ CompositionLocal
สำหรับแนวคิดที่ไม่ได้คิดว่ามีขอบเขตระดับลําดับชั้นหรือมีขอบเขตระดับลําดับชั้นย่อย CompositionLocal
จะเหมาะสมเมื่ออาจมีการใช้โดยรายการที่สืบทอด ไม่ใช่เพียงบางรายการ
หาก Use Case ไม่เป็นไปตามข้อกำหนดเหล่านี้ โปรดดูส่วนทางเลือกอื่นๆ ที่ควรพิจารณาก่อนสร้างCompositionLocal
ตัวอย่างของแนวทางปฏิบัติที่ไม่ถูกต้องคือการสร้าง CompositionLocal
ที่มี ViewModel
ของหน้าจอหนึ่งๆ เพื่อให้คอมโพสิเบิลทั้งหมดในหน้าจอนั้นอ้างอิง ViewModel
เพื่อทำตรรกะบางอย่างได้ การกระทำนี้ไม่เหมาะสม
เนื่องจาก Composable บางรายการด้านล่างโครงสร้าง UI บางต้นไม่จำเป็นต้องทราบเกี่ยวกับ
ViewModel
แนวทางปฏิบัติที่ดีคือการส่งผ่านไปยัง Composable เฉพาะข้อมูล
ที่พวกเขาต้องการตามรูปแบบที่สถานะไหลลงและเหตุการณ์ไหลขึ้น วิธีนี้จะทำให้คอมโพสิเบิลของคุณนํากลับมาใช้ซ้ำได้มากขึ้นและทดสอบได้ง่ายขึ้น
กำลังสร้าง CompositionLocal
มี API 2 รายการในการสร้าง CompositionLocal
ดังนี้
compositionLocalOf
การเปลี่ยนค่าที่ระบุระหว่างการจัดองค์ประกอบใหม่จะเป็นโมฆะเท่านั้น เนื้อหาที่อ่านcurrent
staticCompositionLocalOf
สิ่งที่ต่างจากcompositionLocalOf
ตรงที่การอ่านstaticCompositionLocalOf
ไม่ต่างจาก Compose ติดตามอยู่ การเปลี่ยนค่าจะทำให้ทั้ง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
Composable จะเชื่อมโยงค่ากับอินสแตนซ์ 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 นั้นเป็นลักษณะนิสัยที่ดี คำแนะนำจากเรา คุณส่งผ่าน 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 }
การกลับการควบคุม
อีกวิธีในการหลีกเลี่ยงการส่งพา dependencies ที่ไม่จำเป็นไปยังคอมโพสิเบิลคือการเปลี่ยนการควบคุม องค์ประกอบที่สืบทอดจะรับข้อมูลพึ่งพาเพื่อดำเนินการตรรกะบางอย่างแทนที่องค์ประกอบหลัก
ดูตัวอย่างต่อไปนี้ซึ่งองค์ประกอบสืบทอดต้องทริกเกอร์คำขอ โหลดข้อมูลบางส่วน:
@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
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 ปิดอยู่
- องค์ประกอบของธีมใน Compose
- การใช้มุมมองในโหมดเขียน
- Kotlin สำหรับ Jetpack Compose