1. บทนำ
ในโค้ดแล็บนี้ คุณจะได้เรียนรู้วิธีสร้างแอปที่ปรับให้เหมาะกับอุปกรณ์ต่างๆ เช่น โทรศัพท์ แท็บเล็ต และอุปกรณ์แบบพับได้ รวมถึงวิธีเพิ่มการเข้าถึงด้วย Jetpack Compose นอกจากนี้ คุณจะได้เรียนรู้แนวทางปฏิบัติแนะนำในการใช้คอมโพเนนต์และธีม Material 3 ด้วย
ก่อนเจาะลึกเรื่องนี้ เราขออธิบายความหมายของ "ความสามารถในการปรับตัว"
ความสามารถในการปรับตัว
UI ของแอปควรปรับเปลี่ยนตามขนาด การวางแนว และรูปแบบของหน้าต่างที่แตกต่างกัน เลย์เอาต์แบบปรับเปลี่ยนจะเปลี่ยนแปลงตามพื้นที่หน้าจอที่มีอยู่ การเปลี่ยนแปลงเหล่านี้มีตั้งแต่การปรับเลย์เอาต์แบบง่ายเพื่อเติมเต็มพื้นที่ การเลือกสไตล์การนําทางที่เกี่ยวข้อง ไปจนถึงการเปลี่ยนเลย์เอาต์ทั้งหมดเพื่อใช้ประโยชน์จากพื้นที่เพิ่มเติม
ดูข้อมูลเพิ่มเติมได้ที่การออกแบบที่ปรับให้เหมาะสม
ในโค้ดแล็บนี้ คุณจะได้สำรวจวิธีใช้และพิจารณาความยืดหยุ่นเมื่อใช้ Jetpack Compose คุณสร้างแอปพลิเคชันชื่อ Reply ซึ่งแสดงวิธีใช้ความสามารถในการปรับตัวสําหรับหน้าจอทุกประเภท และวิธีที่ความสามารถในการปรับตัวและการเข้าถึงทํางานร่วมกันเพื่อให้ผู้ใช้ได้รับประสบการณ์การใช้งานที่ดีที่สุด
สิ่งที่คุณจะได้เรียนรู้
- วิธีออกแบบแอปให้กำหนดเป้าหมายไปยังหน้าต่างทุกขนาดด้วย Jetpack Compose
- วิธีกำหนดเป้าหมายแอปสำหรับอุปกรณ์แบบพับได้ต่างๆ
- วิธีใช้การนําทางประเภทต่างๆ เพื่อให้เข้าถึงได้ง่ายขึ้นและเข้าถึงได้ดีขึ้น
- วิธีใช้คอมโพเนนต์ Material 3 เพื่อมอบประสบการณ์การใช้งานที่ดีที่สุดสำหรับทุกขนาดหน้าต่าง
สิ่งที่คุณต้องมี
- Android Studio เวอร์ชันเสถียรล่าสุด
- อุปกรณ์เสมือนที่ปรับขนาดได้ของ Android 13
- ความรู้เกี่ยวกับ Kotlin
- ความเข้าใจเบื้องต้นเกี่ยวกับการเขียน (เช่น คำอธิบายประกอบ
@Composable
) - ความคุ้นเคยเบื้องต้นเกี่ยวกับเลย์เอาต์การเขียน (เช่น
Row
และColumn
) - ความคุ้นเคยขั้นพื้นฐานเกี่ยวกับตัวแก้ไข (เช่น
Modifier.padding()
)
คุณจะใช้โปรแกรมจำลองที่ปรับขนาดได้ในโค้ดแล็บนี้ ซึ่งช่วยให้คุณสลับระหว่างอุปกรณ์ประเภทต่างๆ และขนาดหน้าต่างได้
หากคุณไม่คุ้นเคยกับ Compose ให้ลองทำโค้ดแล็บพื้นฐานของ Jetpack Compose ก่อนทำโค้ดแล็บนี้
สิ่งที่คุณจะสร้าง
- แอปไคลเอ็นต์อีเมลแบบอินเทอร์แอกทีฟชื่อ Reply ซึ่งใช้แนวทางปฏิบัติแนะนำสำหรับการออกแบบที่ปรับเปลี่ยนได้ การนำทางแบบต่างๆ ของ Material และการใช้พื้นที่หน้าจออย่างคุ้มค่าที่สุด
2. ตั้งค่า
หากต้องการดูโค้ดสําหรับ Codelab นี้ ให้โคลนที่เก็บ GitHub จากบรรทัดคําสั่ง
git clone https://github.com/android/codelab-android-compose.git cd codelab-android-compose/AdaptiveUiCodelab
หรือจะดาวน์โหลดที่เก็บเป็นไฟล์ ZIP ก็ได้ โดยทำดังนี้
เราขอแนะนำให้คุณเริ่มต้นด้วยโค้ดในสาขา main แล้วทําตามขั้นตอนใน Codelab ทีละขั้นตอนตามเวลาที่คุณสะดวก
เปิดโปรเจ็กต์ใน Android Studio
- ในหน้าต่างยินดีต้อนรับสู่ Android Studio ให้เลือก เปิดโปรเจ็กต์ที่มีอยู่
- เลือกโฟลเดอร์
<Download Location>/AdaptiveUiCodelab
(ตรวจสอบว่าคุณได้เลือกไดเรกทอรีAdaptiveUiCodelab
ที่มีbuild.gradle
) - เมื่อ Android Studio นำเข้าโปรเจ็กต์แล้ว ให้ทดสอบว่าคุณเรียกใช้สาขา
main
ได้
สํารวจโค้ดเริ่มต้น
รหัสสาขา main จะมีแพ็กเกจ ui
คุณจะต้องทำงานกับไฟล์ต่อไปนี้ในแพ็กเกจดังกล่าว
MainActivity.kt
- กิจกรรมจุดแรกเข้าที่คุณเริ่มแอปReplyApp.kt
- มีคอมโพสิเบิล UI ของหน้าจอหลักReplyHomeViewModel.kt
- ระบุข้อมูลและสถานะ UI สำหรับเนื้อหาแอปReplyListContent.kt
- มีคอมโพสิเบิลสสำหรับแสดงรายการและหน้าจอรายละเอียด
หากคุณเรียกใช้แอปนี้บนโปรแกรมจำลองที่ปรับขนาดได้และลองใช้กับอุปกรณ์ประเภทต่างๆ เช่น โทรศัพท์หรือแท็บเล็ต UI จะขยายไปยังพื้นที่ที่กำหนดเท่านั้น แทนที่จะใช้ประโยชน์จากพื้นที่หน้าจอหรือมอบประสบการณ์การใช้งานที่สะดวกสบาย
คุณจะอัปเดตแอปเพื่อใช้ประโยชน์จากพื้นที่หน้าจอ เพิ่มความสะดวกในการใช้งาน และปรับปรุงประสบการณ์โดยรวมของผู้ใช้
3. ทำให้แอปปรับเปลี่ยนได้
ส่วนนี้จะอธิบายความหมายของการทําให้แอปปรับเปลี่ยนได้ และคอมโพเนนต์ที่ Material 3 มีให้เพื่อทําให้การดำเนินการดังกล่าวง่ายขึ้น นอกจากนี้ยังครอบคลุมประเภทหน้าจอและสถานะที่คุณจะกำหนดเป้าหมาย ซึ่งรวมถึงโทรศัพท์ แท็บเล็ต แท็บเล็ตขนาดใหญ่ และอุปกรณ์พับได้
คุณจะเริ่มต้นด้วยการอธิบายพื้นฐานของขนาดหน้าต่าง ลักษณะการพับ และตัวเลือกการนำทางประเภทต่างๆ จากนั้น คุณจะใช้ API เหล่านี้ในแอปเพื่อให้แอปปรับเปลี่ยนได้มากขึ้น
ขนาดหน้าต่าง
อุปกรณ์ Android มีทุกรูปร่างและขนาด ตั้งแต่โทรศัพท์ไปจนถึงอุปกรณ์แบบพับได้ แท็บเล็ต และ ChromeOS UI ของคุณต้องปรับเปลี่ยนตามอุปกรณ์และตอบสนองต่อผู้ใช้เพื่อให้รองรับขนาดหน้าต่างได้มากที่สุด เราได้กําหนดค่าจุดหยุดพักซึ่งช่วยจัดประเภทอุปกรณ์ออกเป็นคลาสขนาดที่กําหนดไว้ล่วงหน้า (กะทัดรัด ปานกลาง และขยาย) ที่เรียกว่าคลาสขนาดหน้าต่าง เพื่อช่วยคุณค้นหาเกณฑ์ที่เหมาะสมสําหรับเปลี่ยน UI ของแอป เหล่านี้คือชุดจุดพักของวิวพอร์ตตามความคิดเห็นที่จะช่วยคุณออกแบบ พัฒนา และทดสอบเลย์เอาต์แอปพลิเคชันที่ปรับเปลี่ยนตามอุปกรณ์และตอบสนอง
หมวดหมู่เหล่านี้ได้รับการเลือกมาโดยเฉพาะเพื่อรักษาสมดุลระหว่างความเรียบง่ายของเลย์เอาต์กับความยืดหยุ่นในการเพิ่มประสิทธิภาพแอปสำหรับกรณีที่ไม่ซ้ำกัน คลาสขนาดหน้าต่างจะกำหนดโดยพื้นที่หน้าจอที่พร้อมใช้งานสำหรับแอปเสมอ ซึ่งอาจไม่ใช่พื้นที่หน้าจอทั้งหมดสำหรับการทำงานแบบหลายงานหรือแบ่งกลุ่มอื่นๆ
ทั้งความกว้างและความสูงจะจัดประเภทแยกกัน ดังนั้นแอปของคุณจะมี Window Size Class 2 รายการในทุกๆ ช่วงเวลา โดย 1 รายการสำหรับความกว้างและอีก 1 รายการสำหรับความสูง ความกว้างที่ใช้ได้มักจะสำคัญกว่าความสูงที่ใช้ได้เนื่องจากมีการเลื่อนในแนวตั้งอยู่ทั่วไป ดังนั้นในกรณีนี้ คุณจะใช้คลาสขนาดความกว้างด้วย
สถานะการพับ
อุปกรณ์แบบพับได้นำเสนอสถานการณ์เพิ่มเติมที่แอปสามารถปรับให้เข้ากับขนาดที่หลากหลายและข้อต่อของอุปกรณ์ บานพับอาจบดบังบางส่วนของจอแสดงผล ทำให้พื้นที่นั้นไม่เหมาะสมที่จะแสดงเนื้อหา และอาจเป็นการแยกออกจากกัน ซึ่งหมายความว่าจะมีจอแสดงผล 2 จอแยกกันเมื่อกางอุปกรณ์ออก
นอกจากนี้ ผู้ใช้อาจกําลังดูที่จอแสดงผลด้านในขณะที่บานพับเปิดอยู่ครึ่งหนึ่ง ซึ่งส่งผลให้ท่าทางของร่างกายแตกต่างกันไปตามการวางแนวของรอยพับ ได้แก่ ท่าทางบนโต๊ะ (รอยพับแนวนอน ซึ่งแสดงอยู่ทางด้านขวาในรูปภาพด้านบน) และท่าทางแบบหนังสือ (รอยพับแนวตั้ง)
อ่านเพิ่มเติมเกี่ยวกับลักษณะการพับและบานพับ
ทั้งหมดนี้เป็นสิ่งที่ควรพิจารณาเมื่อใช้เลย์เอาต์แบบปรับเปลี่ยนได้ซึ่งรองรับอุปกรณ์แบบพับได้
รับข้อมูลที่ปรับเปลี่ยนได้
ไลบรารี Material3 adaptive
ช่วยให้เข้าถึงข้อมูลเกี่ยวกับหน้าต่างที่แอปทำงานอยู่ได้อย่างสะดวก
- เพิ่มรายการสำหรับอาร์ติแฟกต์นี้และเวอร์ชันของอาร์ติแฟกต์ลงในไฟล์แคตตาล็อกเวอร์ชัน
gradle/libs.versions.toml
[versions]
material3Adaptive = "1.0.0"
[libraries]
androidx-material3-adaptive = { module = "androidx.compose.material3.adaptive:adaptive", version.ref = "material3Adaptive" }
- ในไฟล์บิลด์ของโมดูลแอป ให้เพิ่มทรัพยากร Dependency ของไลบรารีใหม่ แล้วทำการซิงค์ Gradle โดยทำดังนี้
app/build.gradle.kts
dependencies {
implementation(libs.androidx.material3.adaptive)
}
ในตอนนี้ คุณสามารถใช้ currentWindowAdaptiveInfo()
ในขอบเขตที่ประกอบกันได้ เพื่อรับออบเจ็กต์ WindowAdaptiveInfo
ที่มีข้อมูล เช่น คลาสขนาดหน้าต่างปัจจุบัน และดูว่าอุปกรณ์อยู่ในลักษณะพับได้เหมือนอยู่บนโต๊ะหรือไม่
คุณลองดำเนินการนี้ได้แล้วใน MainActivity
- ใน
onCreate()
ภายในบล็อกReplyTheme
ให้รับข้อมูลการปรับหน้าต่างที่แสดงได้และแสดงคลาสขนาดใน ComposableText
คุณสามารถเพิ่มข้อมูลนี้ต่อจากองค์ประกอบReplyApp()
MainActivity.kt
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
ReplyTheme {
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
ReplyApp(
replyHomeUIState = uiState,
onEmailClick = viewModel::setSelectedEmail
)
val adaptiveInfo = currentWindowAdaptiveInfo()
val sizeClassText =
"${adaptiveInfo.windowSizeClass.windowWidthSizeClass}\n" +
"${adaptiveInfo.windowSizeClass.windowHeightSizeClass}"
Text(
text = sizeClassText,
color = Color.Magenta,
modifier = Modifier.padding(
WindowInsets.safeDrawing.asPaddingValues()
)
)
}
}
}
การเรียกใช้แอปตอนนี้จะแสดงคลาสขนาดหน้าต่างที่พิมพ์ทับเนื้อหาแอป คุณสามารถดูข้อมูลอื่นๆ ที่มีให้ในข้อมูลการปรับหน้าต่างได้ หลังจากนั้น คุณสามารถนำ Text
นี้ออกได้เนื่องจากครอบคลุมเนื้อหาแอปและไม่จำเป็นต้องใช้ในขั้นตอนถัดไป
4. การนำทางแบบไดนามิก
ตอนนี้คุณปรับการนําทางของแอปเมื่อสถานะและขนาดของอุปกรณ์เปลี่ยนแปลงเพื่อให้แอปใช้งานได้ง่ายขึ้น
เมื่อผู้ใช้ถือโทรศัพท์ นิ้วของผู้ใช้มักจะอยู่ที่ด้านล่างของหน้าจอ เมื่อผู้ใช้ถืออุปกรณ์แบบพับได้หรือแท็บเล็ตที่กางออก นิ้วของผู้ใช้มักจะอยู่ใกล้กับด้านข้าง ผู้ใช้ควรไปยังส่วนต่างๆ หรือเริ่มโต้ตอบกับแอปได้โดยไม่ต้องใช้มือในลักษณะที่ผิดปกติหรือเปลี่ยนตำแหน่งมือ
ขณะที่คุณออกแบบแอปและตัดสินใจว่าจะวางองค์ประกอบ UI แบบอินเทอร์แอกทีฟไว้ที่ใดในเลย์เอาต์ ให้พิจารณาผลกระทบด้านสรีรศาสตร์ของแต่ละพื้นที่ของหน้าจอ
- พื้นที่ใดที่เข้าถึงได้สะดวกขณะถืออุปกรณ์
- พื้นที่ใดที่เข้าถึงได้โดยการยืดนิ้วเท่านั้น ซึ่งอาจไม่สะดวก
- บริเวณใดที่เข้าถึงได้ยากหรืออยู่ห่างจากจุดที่ผู้ใช้ถืออุปกรณ์
การนําทางเป็นสิ่งแรกที่ผู้ใช้โต้ตอบด้วย และมีการดำเนินการที่สําคัญสูงซึ่งเกี่ยวข้องกับเส้นทางของผู้ใช้ที่สําคัญ จึงควรวางไว้ในพื้นที่ที่เข้าถึงได้ง่ายที่สุด ไลบรารีแบบปรับเปลี่ยนได้ของ Material มีคอมโพเนนต์หลายรายการที่ช่วยให้คุณนําการนําทางไปใช้ได้ โดยขึ้นอยู่กับคลาสขนาดหน้าต่างของอุปกรณ์
Bottom Navigation
การนําทางด้านล่างเหมาะสําหรับขนาดกะทัดรัด เนื่องจากเราจับอุปกรณ์ในลักษณะที่นิ้วโป้งสามารถเข้าถึงจุดสัมผัสทั้งหมดของการนําทางด้านล่างได้อย่างง่ายดาย ใช้เมื่ออุปกรณ์มีขนาดเล็กหรืออุปกรณ์แบบพับได้อยู่ในสถานะพับ
แถบข้างสำหรับไปยังส่วนต่างๆ
สำหรับหน้าต่างที่มีความกว้างปานกลาง รางนำทางเหมาะสำหรับความสามารถในการเข้าถึงเนื่องจากนิ้วหัวแม่มือของเราวางอยู่ข้างๆ อุปกรณ์อย่างเป็นธรรมชาติ นอกจากนี้ คุณยังรวมแถบนําทางเข้ากับลิ้นชักการนําทางเพื่อแสดงข้อมูลเพิ่มเติมได้ด้วย
ลิ้นชักการนำทาง
ลิ้นชักการนำทางเป็นวิธีที่ง่ายในการดูข้อมูลโดยละเอียดสำหรับแท็บการนำทาง และเข้าถึงได้ง่ายเมื่อใช้แท็บเล็ตหรืออุปกรณ์ขนาดใหญ่ กล่องโต้ตอบการนำทางมี 2 ประเภท ได้แก่ กล่องโต้ตอบการนำทางแบบโมดัลและกล่องโต้ตอบการนำทางแบบถาวร
ลิ้นชักการนำทางแบบโมดัล
คุณสามารถใช้ลิ้นชักการนำทางแบบโมดัลสำหรับโทรศัพท์และแท็บเล็ตขนาดกะทัดรัดถึงกลางได้ เนื่องจากสามารถขยายหรือซ่อนเป็นการวางซ้อนบนเนื้อหา ซึ่งบางครั้งอาจใช้ร่วมกับรางนำทางได้
ลิ้นชักการนำทางแบบถาวร
คุณสามารถใช้ลิ้นชักการนำทางแบบถาวรสำหรับการนําทางแบบคงที่ในแท็บเล็ตขนาดใหญ่, Chromebook และเดสก์ท็อป
ใช้การนําทางแบบไดนามิก
ตอนนี้คุณจะสลับระหว่างการนําทางประเภทต่างๆ ได้เมื่อสถานะและขนาดของอุปกรณ์เปลี่ยนแปลง
ปัจจุบันแอปจะแสดง NavigationBar
ใต้เนื้อหาหน้าจอเสมอ ไม่ว่าอุปกรณ์จะอยู่ในสถานะใดก็ตาม แต่คุณใช้คอมโพเนนต์ Material NavigationSuiteScaffold
เพื่อสลับไปมาระหว่างคอมโพเนนต์การนำทางต่างๆ โดยอัตโนมัติตามข้อมูล เช่น คลาสขนาดหน้าต่างปัจจุบันได้
- เพิ่ม Dependency ของ Gradle เพื่อรับคอมโพเนนต์นี้โดยอัปเดตแคตตาล็อกเวอร์ชันและสคริปต์การสร้างของแอป จากนั้นทำการซิงค์ Gradle
gradle/libs.versions.toml
[versions]
material3AdaptiveNavSuite = "1.3.0"
[libraries]
androidx-material3-adaptive-navigation-suite = { module = "androidx.compose.material3:material3-adaptive-navigation-suite", version.ref = "material3AdaptiveNavSuite" }
app/build.gradle.kts
dependencies {
implementation(libs.androidx.material3.adaptive.navigation.suite)
}
- ค้นหาฟังก์ชันคอมโพสิเบิล
ReplyNavigationWrapper()
ในReplyApp.kt
และแทนที่Column
และเนื้อหาด้วยNavigationSuiteScaffold
ดังนี้
ReplyApp.kt
@Composable
private fun ReplyNavigationWrapperUI(
content: @Composable () -> Unit = {}
) {
var selectedDestination: ReplyDestination by remember {
mutableStateOf(ReplyDestination.Inbox)
}
NavigationSuiteScaffold(
navigationSuiteItems = {
ReplyDestination.entries.forEach {
item(
selected = it == selectedDestination,
onClick = { /*TODO update selection*/ },
icon = {
Icon(
imageVector = it.icon,
contentDescription = stringResource(it.labelRes)
)
},
label = {
Text(text = stringResource(it.labelRes))
},
)
}
}
) {
content()
}
}
อาร์กิวเมนต์ navigationSuiteItems
คือบล็อกที่ช่วยให้คุณเพิ่มรายการโดยใช้ฟังก์ชัน item()
ซึ่งคล้ายกับการเพิ่มรายการใน LazyColumn
ภายใน lambda ต่อท้าย โค้ดนี้จะเรียก content()
ที่ส่งผ่านเป็นอาร์กิวเมนต์ไปยัง ReplyNavigationWrapperUI()
เรียกใช้แอปบนโปรแกรมจำลองและลองเปลี่ยนขนาดระหว่างโทรศัพท์ อุปกรณ์แบบพับได้ และแท็บเล็ต แล้วคุณจะเห็นแถบนําทางเปลี่ยนเป็นแถบนําทางแนวนอนและกลับเป็นแถบนําทางแนวตั้ง
ในหน้าต่างที่กว้างมาก เช่น ในแท็บเล็ตในแนวนอน คุณอาจต้องการแสดงลิ้นชักการนำทางแบบถาวร NavigationSuiteScaffold
รองรับการแสดงลิ้นชักถาวร แต่จะไม่แสดงในค่า WindowWidthSizeClass
ปัจจุบัน แต่คุณสามารถทำให้ดำเนินการดังกล่าวได้ด้วยการทําการเปลี่ยนแปลงเล็กน้อย
- เพิ่มโค้ดต่อไปนี้ก่อนการเรียก
NavigationSuiteScaffold
ReplyApp.kt
@Composable
private fun ReplyNavigationWrapperUI(
content: @Composable () -> Unit = {}
) {
var selectedDestination: ReplyDestination by remember {
mutableStateOf(ReplyDestination.Inbox)
}
val windowSize = with(LocalDensity.current) {
currentWindowSize().toSize().toDpSize()
}
val layoutType = if (windowSize.width >= 1200.dp) {
NavigationSuiteType.NavigationDrawer
} else {
NavigationSuiteScaffoldDefaults.calculateFromAdaptiveInfo(
currentWindowAdaptiveInfo()
)
}
NavigationSuiteScaffold(
layoutType = layoutType,
...
) {
content()
}
}
โค้ดนี้จะรับขนาดหน้าต่างก่อนแล้วแปลงเป็นหน่วย DP โดยใช้ currentWindowSize()
และ LocalDensity.current
จากนั้นเปรียบเทียบความกว้างของหน้าต่างเพื่อเลือกประเภทเลย์เอาต์ของ UI การนําทาง หากความกว้างของหน้าต่างอย่างน้อย 1200.dp
ระบบจะใช้ NavigationSuiteType.NavigationDrawer
ไม่เช่นนั้น ระบบจะใช้การคํานวณเริ่มต้น
เมื่อคุณเรียกใช้แอปอีกครั้งในโปรแกรมจำลองที่ปรับขนาดได้และลองใช้ประเภทอื่น ให้สังเกตว่าเมื่อการกำหนดค่าหน้าจอมีการเปลี่ยนแปลงหรือคุณกางอุปกรณ์พับ การนำทางจะเปลี่ยนไปเป็นประเภทที่เหมาะสมสำหรับขนาดดังกล่าว
ยินดีด้วย คุณได้เรียนรู้เกี่ยวกับการนําทางประเภทต่างๆ เพื่อรองรับขนาดและสถานะหน้าต่างประเภทต่างๆ แล้ว
ในส่วนถัดไป คุณจะได้ดูวิธีใช้ประโยชน์จากพื้นที่หน้าจอที่เหลืออยู่แทนการยืดรายการในรายการเดียวกันจากขอบหนึ่งไปอีกขอบหนึ่ง
5. การใช้พื้นที่หน้าจอ
ไม่ว่าคุณจะเรียกใช้แอปในแท็บเล็ตขนาดเล็ก อุปกรณ์แบบกางไม่ได้ หรือแท็บเล็ตขนาดใหญ่ หน้าจอจะขยายให้เต็มพื้นที่ที่เหลือ คุณต้องตรวจสอบว่าสามารถใช้ประโยชน์จากพื้นที่หน้าจอนั้นเพื่อแสดงข้อมูลเพิ่มเติม เช่น สำหรับแอปนี้ การแสดงอีเมลและชุดข้อความต่อผู้ใช้ในหน้าเดียวกัน
Material 3 กําหนดเลย์เอาต์ตามแบบฉบับ 3 รายการ ซึ่งแต่ละรายการมีการกําหนดค่าสําหรับ Window Size Class แบบกะทัดรัด ปานกลาง และขยาย เลย์เอาต์ Canonical รายละเอียดรายการเหมาะสําหรับ Use Case นี้อย่างยิ่ง และพร้อมใช้งานในโหมดเขียนเป็น ListDetailPaneScaffold
- รับคอมโพเนนต์นี้โดยการเพิ่มการพึ่งพาต่อไปนี้และทำการซิงค์ Gradle
gradle/libs.versions.toml
[libraries]
androidx-material3-adaptive-layout = { module = "androidx.compose.material3.adaptive:adaptive-layout", version.ref = "material3Adaptive" }
androidx-material3-adaptive-navigation = { module = "androidx.compose.material3.adaptive:adaptive-navigation", version.ref = "material3Adaptive" }
app/build.gradle.kts
dependencies {
implementation(libs.androidx.material3.adaptive.layout)
implementation(libs.androidx.material3.adaptive.navigation)
}
- ค้นหาฟังก์ชันที่ประกอบกันได้
ReplyAppContent()
ในReplyApp.kt
ซึ่งปัจจุบันจะแสดงเฉพาะแผงรายการโดยการเรียกใช้ReplyListPane()
แทนที่การใช้งานนี้ด้วยListDetailPaneScaffold
โดยแทรกโค้ดต่อไปนี้ เนื่องจากเป็น API เวอร์ชันทดลอง คุณจะต้องเพิ่มคำอธิบายประกอบ@OptIn
ในฟังก์ชันReplyAppContent()
ด้วย
ReplyApp.kt
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
@Composable
fun ReplyAppContent(
replyHomeUIState: ReplyHomeUIState,
onEmailClick: (Email) -> Unit,
) {
val navigator = rememberListDetailPaneScaffoldNavigator<Long>()
ListDetailPaneScaffold(
directive = navigator.scaffoldDirective,
value = navigator.scaffoldValue,
listPane = {
ReplyListPane(replyHomeUIState, onEmailClick)
},
detailPane = {
ReplyDetailPane(replyHomeUIState.emails.first())
}
)
}
โค้ดนี้จะสร้างตัวนำทางโดยใช้ rememberListDetailPaneNavigator
ก่อน เครื่องมือนำทางช่วยให้คุณควบคุมได้บางส่วนเกี่ยวกับแผงที่จะแสดงและเนื้อหาที่ควรแสดงในแผงนั้น ซึ่งเราจะสาธิตให้ดูในภายหลัง
ListDetailPaneScaffold
จะแสดง 2 แผงเมื่อขยาย Window Size Class ของความกว้างของหน้าต่าง มิเช่นนั้น ระบบจะแสดงแผงหนึ่งหรืออีกแผงหนึ่งตามค่าที่ระบุสำหรับพารามิเตอร์ 2 รายการ ได้แก่ คำสั่งสคาฟเฟิลและค่าสคาฟเฟิล หากต้องการดูลักษณะการทำงานเริ่มต้น โค้ดนี้จะใช้คำสั่งสแคฟเฟิลด์และค่าสแคฟเฟิลด์ที่ได้จากโปรแกรมนำทาง
พารามิเตอร์ที่จําเป็นที่เหลือคือ lambda ที่ประกอบกันได้สําหรับแผง ReplyListPane()
และ ReplyDetailPane()
(อยู่ใน ReplyListContent.kt
) ใช้เพื่อกรอกบทบาทของแผงรายการและแผงรายละเอียดตามลำดับ ReplyDetailPane()
ต้องการอาร์กิวเมนต์อีเมล ดังนั้นตอนนี้โค้ดนี้จะใช้อีเมลแรกจากรายการอีเมลใน ReplyHomeUIState
เรียกใช้แอปและเปลี่ยนมุมมองโปรแกรมจำลองเป็นแบบพับได้หรือแท็บเล็ต (คุณอาจต้องเปลี่ยนการวางแนวด้วย) เพื่อดูเลย์เอาต์แบบ 2 แผง ดูดีกว่าก่อนหน้านี้มากเลย!
มาพูดถึงลักษณะการทำงานที่ต้องการของหน้าจอนี้กัน เมื่อผู้ใช้แตะอีเมลในแผงรายการ อีเมลดังกล่าวควรแสดงในแผงรายละเอียดพร้อมกับการตอบกลับทั้งหมด ขณะนี้ แอปจะไม่ติดตามว่าได้เลือกอีเมลใดไว้ และแตะที่รายการจะไม่ส่งผลใดๆ ตำแหน่งที่ดีที่สุดในการเก็บข้อมูลนี้คือสถานะ UI ที่เหลือใน ReplyHomeUIState
- เปิด
ReplyHomeViewModel.kt
และค้นหาคลาสข้อมูลReplyHomeUIState
เพิ่มพร็อพเพอร์ตี้สำหรับอีเมลที่เลือก โดยมีค่าเริ่มต้นเป็นnull
ดังนี้
ReplyHomeViewModel.kt
data class ReplyHomeUIState(
val emails : List<Email> = emptyList(),
val selectedEmail: Email? = null,
val loading: Boolean = false,
val error: String? = null
)
- ในไฟล์เดียวกัน
ReplyHomeViewModel
มีฟังก์ชันsetSelectedEmail()
ที่เรียกใช้เมื่อผู้ใช้แตะรายการในรายการ แก้ไขฟังก์ชันนี้เพื่อคัดลอกสถานะ UI และบันทึกอีเมลที่เลือก
ReplyHomeViewModel.kt
fun setSelectedEmail(email: Email) {
_uiState.update {
it.copy(selectedEmail = email)
}
}
สิ่งที่ควรพิจารณาคือสิ่งที่เกิดขึ้นก่อนผู้ใช้แตะรายการใดๆ และอีเมลที่เลือกคือ null
ควรแสดงอะไรในแผงรายละเอียด การจัดการเคสนี้ทำได้หลายวิธี เช่น การแสดงรายการแรกในรายการโดยค่าเริ่มต้น
- แก้ไขฟังก์ชัน
observeEmails()
ในไฟล์เดียวกัน เมื่อโหลดรายการอีเมลแล้ว หากสถานะ UI ก่อนหน้าไม่มีอีเมลที่เลือกไว้ ให้ตั้งค่าเป็นรายการแรกโดยทำดังนี้
ReplyHomeViewModel.kt
private fun observeEmails() {
viewModelScope.launch {
emailsRepository.getAllEmails()
.catch { ex ->
_uiState.value = ReplyHomeUIState(error = ex.message)
}
.collect { emails ->
val currentSelection = _uiState.value.selectedEmail
_uiState.value = ReplyHomeUIState(
emails = emails,
selectedEmail = currentSelection ?: emails.first()
)
}
}
}
- กลับไปที่
ReplyApp.kt
และใช้อีเมลที่เลือก (หากมี) เพื่อป้อนข้อมูลในแผงรายละเอียด
ReplyApp.kt
ListDetailPaneScaffold(
// ...
detailPane = {
if (replyHomeUIState.selectedEmail != null) {
ReplyDetailPane(replyHomeUIState.selectedEmail)
}
}
)
เรียกใช้แอปอีกครั้งและเปลี่ยนโปรแกรมจำลองเป็นขนาดแท็บเล็ต แล้วดูว่าการแตะรายการในรายการจะอัปเดตเนื้อหาในแผงรายละเอียด
วิธีนี้จะทำงานได้ดีเมื่อมองเห็นแผงทั้ง 2 ช่องได้ แต่เมื่อหน้าต่างมีพื้นที่ให้แสดงเพียงแผงเดียวเท่านั้น จึงดูเหมือนว่าไม่มีอะไรเกิดขึ้นเมื่อคุณแตะรายการ ลองเปลี่ยนมุมมองโปรแกรมจำลองเป็นโทรศัพท์หรืออุปกรณ์แบบพับได้ในแนวตั้ง และสังเกตเห็นว่ามีเพียงแผงรายการที่มองเห็นได้แม้หลังจากแตะรายการแล้ว นั่นเป็นเพราะแม้ว่าอีเมลที่เลือกจะได้รับการอัปเดต แต่ ListDetailPaneScaffold
จะยังคงโฟกัสอยู่ที่แผงรายการในการกําหนดค่าเหล่านี้
- ในการแก้ไขปัญหานี้ ให้แทรกโค้ดต่อไปนี้เมื่อ lambda ที่ส่งไปยัง
ReplyListPane
ReplyApp.kt
ListDetailPaneScaffold(
// ...
listPane = {
ReplyListPane(
replyHomeUIState = replyHomeUIState,
onEmailClick = { email ->
onEmailClick(email)
navigator.navigateTo(ListDetailPaneScaffoldRole.Detail, email.id)
}
)
},
// ...
)
Lambda นี้ใช้ Navigator ที่สร้างขึ้นก่อนหน้านี้เพื่อเพิ่มลักษณะการทํางานเพิ่มเติมเมื่อมีการคลิกรายการ ระบบจะเรียกใช้ Lambda เดิมที่ส่งไปยังฟังก์ชันนี้ จากนั้นจะเรียกใช้ navigator.navigateTo()
เพื่อระบุแผงที่จะแสดง แต่ละแผงในสคาฟเฟิลด์มีบทบาทที่เชื่อมโยงอยู่ และสำหรับแผงรายละเอียด บทบาทคือ ListDetailPaneScaffoldRole.Detail
ในหน้าต่างขนาดเล็ก การดำเนินการนี้จะดูเหมือนว่าแอปเลื่อนไปข้างหน้า
นอกจากนี้ แอปยังต้องจัดการสิ่งที่จะเกิดขึ้นเมื่อผู้ใช้กดปุ่มย้อนกลับจากแผงรายละเอียดด้วย และลักษณะการทํางานนี้จะแตกต่างกันไปโดยขึ้นอยู่กับว่ามีแผงเดียวหรือ 2 แผงที่มองเห็น
- รองรับการนําทางกลับโดยเพิ่มโค้ดต่อไปนี้
ReplyApp.kt
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
@Composable
fun ReplyAppContent(
replyHomeUIState: ReplyHomeUIState,
onEmailClick: (Email) -> Unit,
) {
val navigator = rememberListDetailPaneScaffoldNavigator<Long>()
BackHandler(navigator.canNavigateBack()) {
navigator.navigateBack()
}
ListDetailPaneScaffold(
directive = navigator.scaffoldDirective,
value = navigator.scaffoldValue,
listPane = {
AnimatedPane {
ReplyListPane(
replyHomeUIState = replyHomeUIState,
onEmailClick = { email ->
onEmailClick(email)
navigator.navigateTo(ListDetailPaneScaffoldRole.Detail, email.id)
}
)
}
},
detailPane = {
AnimatedPane {
if (replyHomeUIState.selectedEmail != null) {
ReplyDetailPane(replyHomeUIState.selectedEmail)
}
}
}
)
}
โปรแกรมนำทางจะทราบสถานะทั้งหมดของ ListDetailPaneScaffold
, รู้ว่าการนําทางกลับเป็นไปได้หรือไม่ และควรทําอย่างไรในสถานการณ์เหล่านี้ทั้งหมด โค้ดนี้จะสร้าง BackHandler
ที่เปิดใช้ทุกครั้งที่โปรแกรมนำทางสามารถไปยังส่วนหลังได้ และภายในการเรียก Lambda จะเรียก navigateBack()
นอกจากนี้ แต่ละแผงยังได้รับการรวมไว้ใน AnimatedPane()
composable เพื่อให้การเปลี่ยนระหว่างแผงเป็นไปอย่างราบรื่นยิ่งขึ้น
เรียกใช้แอปอีกครั้งบนโปรแกรมจำลองที่ปรับขนาดได้สำหรับอุปกรณ์ทุกประเภท และสังเกตว่าเมื่อใดก็ตามที่มีการเปลี่ยนแปลงการกำหนดค่าหน้าจอหรือคุณกางอุปกรณ์แบบพับออก การนำทางและเนื้อหาหน้าจอจะเปลี่ยนแปลงแบบไดนามิกตามการเปลี่ยนแปลงสถานะของอุปกรณ์ นอกจากนี้ ให้ลองแตะอีเมลในแผงรายการและดูว่าเลย์เอาต์ทำงานอย่างไรในหน้าจอต่างๆ โดยแสดงทั้ง 2 แผงพร้อมกันหรือแสดงภาพเคลื่อนไหวระหว่างแผงอย่างราบรื่น
ขอแสดงความยินดี คุณได้ทําให้แอปปรับเปลี่ยนให้เหมาะกับสถานะและขนาดอุปกรณ์ทุกประเภทเรียบร้อยแล้ว ลองใช้แอปในอุปกรณ์แบบพับได้ แท็บเล็ต หรืออุปกรณ์เคลื่อนที่อื่นๆ
6. ขอแสดงความยินดี
ยินดีด้วย คุณศึกษา Codelab นี้จบแล้ว และได้เรียนรู้วิธีทำให้แอปปรับตัวได้ด้วย Jetpack Compose
คุณได้เรียนรู้วิธีตรวจสอบขนาดและสถานะการพับของอุปกรณ์ รวมถึงอัปเดต UI, การนําทาง และฟังก์ชันอื่นๆ ของแอปตามความเหมาะสมแล้ว นอกจากนี้ คุณยังได้เรียนรู้ว่าการปรับให้เข้ากับบริบทต่างๆ ช่วยเพิ่มการเข้าถึงและปรับปรุงประสบการณ์ของผู้ใช้ได้อย่างไร
ขั้นตอนถัดไปคือ
ดู Codelab อื่นๆ ในเส้นทางการเขียนโค้ด
แอปตัวอย่าง
- ตัวอย่างการคอมไพล์คือคอลเล็กชันแอปจำนวนมากที่ใช้แนวทางปฏิบัติแนะนำที่อธิบายไว้ในโค้ดแล็บ