1. บทนำ
ในโค้ดแล็บนี้ คุณจะได้เรียนรู้วิธีสร้างแอปแบบปรับอัตโนมัติสำหรับโทรศัพท์ แท็บเล็ต และอุปกรณ์แบบพับได้ รวมถึงวิธีที่แอปเหล่านี้ช่วยเพิ่มการเข้าถึงด้วย Jetpack Compose นอกจากนี้ คุณยังจะได้เรียนรู้แนวทางปฏิบัติแนะนำในการใช้คอมโพเนนต์และการกำหนดธีม Material 3 ด้วย
ก่อนจะเริ่มใช้งาน คุณควรทำความเข้าใจความหมายของความสามารถในการปรับตัว
ความสามารถในการปรับตัว
UI ของแอปควรตอบสนองต่อขนาดหน้าต่าง การวางแนว และรูปแบบของอุปกรณ์ต่างๆ เลย์เอาต์แบบปรับอัตโนมัติจะเปลี่ยนแปลงตามพื้นที่ว่างบนหน้าจอ การเปลี่ยนแปลงเหล่านี้มีตั้งแต่การปรับเลย์เอาต์อย่างง่ายเพื่อเติมเต็มพื้นที่ การเลือกรูปแบบการนำทางที่เกี่ยวข้อง ไปจนถึงการเปลี่ยนเลย์เอาต์ทั้งหมดเพื่อใช้พื้นที่เพิ่มเติม
ดูข้อมูลเพิ่มเติมได้ที่การออกแบบแบบปรับอัตโนมัติ
ในโค้ดแล็บนี้ คุณจะได้สำรวจวิธีใช้และความคิดเกี่ยวกับความสามารถในการปรับตัวเมื่อใช้ Jetpack Compose คุณสร้างแอปพลิเคชันชื่อ Reply ซึ่งแสดงวิธีใช้การปรับเปลี่ยนตามอุปกรณ์สำหรับหน้าจอทุกประเภท รวมถึงวิธีที่การปรับเปลี่ยนตามอุปกรณ์และการเข้าถึงทำงานร่วมกันเพื่อให้ผู้ใช้ได้รับประสบการณ์ที่ดีที่สุด
สิ่งที่คุณจะได้เรียนรู้
- วิธีออกแบบแอปให้กำหนดเป้าหมายขนาดหน้าต่างทั้งหมดด้วย Jetpack Compose
- วิธีกำหนดเป้าหมายแอปสำหรับอุปกรณ์พับได้รุ่นต่างๆ
- วิธีใช้การนำทางประเภทต่างๆ เพื่อการเข้าถึงและการช่วยเหลือพิเศษที่ดีขึ้น
- วิธีใช้คอมโพเนนต์ Material 3 เพื่อมอบประสบการณ์ที่ดีที่สุดสำหรับขนาดหน้าต่างทุกขนาด
สิ่งที่คุณต้องมี
- Android Studio เวอร์ชันเสถียรล่าสุด
- อุปกรณ์เสมือนที่ปรับขนาดได้ของ Android 13
- มีความรู้เกี่ยวกับ Kotlin
- ความเข้าใจพื้นฐานเกี่ยวกับ Compose (เช่น คำอธิบายประกอบ
@Composable) - มีความคุ้นเคยพื้นฐานกับเลย์เอาต์ Compose (เช่น
RowและColumn) - คุ้นเคยกับตัวแก้ไขในระดับพื้นฐาน (เช่น
Modifier.padding())
คุณจะใช้โปรแกรมจำลองที่ปรับขนาดได้สำหรับโค้ดแล็บนี้ ซึ่งช่วยให้คุณสลับระหว่างอุปกรณ์ประเภทต่างๆ และขนาดหน้าต่างได้

หากคุณไม่คุ้นเคยกับ Compose โปรดลองทำ Codelab พื้นฐานของ Jetpack Compose ก่อนทำ Codelab นี้
สิ่งที่คุณจะสร้าง
- แอปไคลเอ็นต์อีเมลแบบอินเทอร์แอกทีฟชื่อ 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
- ในหน้าต่าง Welcome to Android Studio ให้เลือก
Open an Existing Project - เลือกโฟลเดอร์
<Download Location>/AdaptiveUiCodelab(ตรวจสอบว่าคุณเลือกไดเรกทอรีAdaptiveUiCodelabที่มีbuild.gradle) - เมื่อ Android Studio นำเข้าโปรเจ็กต์แล้ว ให้ทดสอบว่าคุณเรียกใช้กิ่ง
mainได้
สำรวจโค้ดเริ่มต้น
โค้ดสาขาหลักมีแพ็กเกจ ui คุณจะทำงานกับไฟล์ต่อไปนี้ในแพ็กเกจนั้น
MainActivity.kt- กิจกรรมจุดแรกเข้าที่คุณเริ่มแอปReplyApp.kt- มี Composable ของ UI หน้าจอหลักReplyHomeViewModel.kt- จัดเตรียมข้อมูลและสถานะ UI สำหรับเนื้อหาแอปReplyListContent.kt- มี Composable สำหรับแสดงรายการและหน้าจอรายละเอียด
หากคุณเรียกใช้แอปนี้ในโปรแกรมจำลองที่ปรับขนาดได้และลองใช้อุปกรณ์ประเภทต่างๆ เช่น โทรศัพท์หรือแท็บเล็ต UI จะขยายไปยังพื้นที่ที่กำหนดแทนที่จะใช้ประโยชน์จากพื้นที่หน้าจอหรือให้การยศาสตร์ในการเข้าถึง


คุณจะอัปเดตเพื่อใช้ประโยชน์จากพื้นที่หน้าจอ เพิ่มความสามารถในการใช้งาน และปรับปรุงประสบการณ์โดยรวมของผู้ใช้
3. ทำให้แอปปรับเปลี่ยนได้
ส่วนนี้จะแนะนำความหมายของการทำให้อัปปรับเปลี่ยนได้ และคอมโพเนนต์ที่ Material 3 มีเพื่อให้การปรับเปลี่ยนง่ายขึ้น นอกจากนี้ ยังครอบคลุมประเภทหน้าจอและสถานะที่คุณจะกำหนดเป้าหมาย ซึ่งรวมถึงโทรศัพท์ แท็บเล็ต แท็บเล็ตขนาดใหญ่ และอุปกรณ์พับได้
โดยคุณจะเริ่มจากการเรียนรู้พื้นฐานเกี่ยวกับขนาดหน้าต่าง ท่าพับ และตัวเลือกการนำทางประเภทต่างๆ จากนั้นคุณสามารถใช้ API เหล่านี้ในแอปเพื่อทำให้แอปปรับเปลี่ยนได้มากขึ้น
ขนาดหน้าต่าง
อุปกรณ์ Android มีหลากหลายรูปแบบและขนาด ตั้งแต่โทรศัพท์ไปจนถึงอุปกรณ์พับได้ แท็บเล็ต และอุปกรณ์ ChromeOS UI ต้องตอบสนองและปรับเปลี่ยนได้เพื่อรองรับขนาดหน้าต่างให้ได้มากที่สุด เราได้กำหนดค่าเบรกพอยต์ที่จะช่วยจัดประเภทอุปกรณ์เป็นคลาสขนาดที่กำหนดไว้ล่วงหน้า (กะทัดรัด ปานกลาง และขยาย) ซึ่งเรียกว่าคลาสขนาดหน้าต่าง เพื่อช่วยให้คุณพบเกณฑ์ที่เหมาะสมในการเปลี่ยน UI ของแอป ซึ่งเป็นชุดเบรกพอยต์ของวิวพอร์ตที่กำหนดไว้ล่วงหน้าเพื่อช่วยคุณออกแบบ พัฒนา และทดสอบเลย์เอาต์แอปพลิเคชันที่ปรับเปลี่ยนตามพื้นที่โฆษณาและปรับเปลี่ยนตามอุปกรณ์
เราเลือกหมวดหมู่เหล่านี้มาโดยเฉพาะเพื่อสร้างความสมดุลระหว่างความเรียบง่ายของเลย์เอาต์กับความยืดหยุ่นในการเพิ่มประสิทธิภาพแอปสำหรับกรณีที่ไม่เหมือนใคร คลาสขนาดหน้าต่างจะกำหนดโดยพื้นที่หน้าจอที่แอปใช้ได้เสมอ ซึ่งอาจไม่ใช่ทั้งหน้าจอจริงสำหรับการทำงานแบบมัลติทาสก์หรือการแบ่งส่วนอื่นๆ


ทั้งความกว้างและความสูงจะได้รับการจัดประเภทแยกกัน ดังนั้นในทุกช่วงเวลา แอปจะมี Window Size Classes 2 รายการ ได้แก่ รายการหนึ่งสำหรับความกว้างและอีกรายการหนึ่งสำหรับความสูง โดยปกติแล้ว ความกว้างที่ใช้ได้จะมีความสำคัญมากกว่าความสูงที่ใช้ได้เนื่องจากการเลื่อนแนวตั้งเป็นเรื่องปกติ ดังนั้นในกรณีนี้ คุณจะต้องใช้คลาสขนาดความกว้างด้วย
สถานะการพับ
อุปกรณ์แบบพับได้ทำให้แอปของคุณต้องปรับตัวให้เข้ากับสถานการณ์ต่างๆ มากยิ่งขึ้นเนื่องจากมีขนาดที่แตกต่างกันและมีบานพับ บานพับอาจบดบังส่วนหนึ่งของจอแสดงผล ทำให้พื้นที่ดังกล่าวไม่เหมาะที่จะแสดงเนื้อหา หรืออาจแยกออกจากกัน ซึ่งหมายความว่าจะมีจอแสดงผลจริง 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ให้รับข้อมูลการปรับหน้าต่างและแสดงคลาสขนาดในTextที่ใช้ร่วมกันได้ คุณเพิ่มข้อความนี้หลังองค์ประกอบ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 มีคอมโพเนนต์หลายอย่างที่จะช่วยคุณนำการนำทางไปใช้ได้ ทั้งนี้ขึ้นอยู่กับคลาสขนาดหน้าต่างของอุปกรณ์
การนำทางด้านล่าง
แถบนำทางด้านล่างเหมาะสำหรับขนาดกะทัดรัด เนื่องจากเรามักจะถืออุปกรณ์ในลักษณะที่นิ้วโป้งแตะจุดสัมผัสทั้งหมดของแถบนำทางด้านล่างได้ง่าย ใช้เมื่อใดก็ตามที่คุณมีอุปกรณ์ขนาดกะทัดรัดหรืออุปกรณ์พับได้ในสถานะพับแบบกะทัดรัด

แถบข้างสำหรับไปยังส่วนต่างๆ
สำหรับขนาดหน้าต่างปานกลาง แถบนำทางเหมาะอย่างยิ่งสำหรับการเข้าถึง เนื่องจากนิ้วหัวแม่มือจะอยู่ด้านข้างของอุปกรณ์โดยธรรมชาติ นอกจากนี้ คุณยังรวมแถบนำทางกับลิ้นชักนำทางเพื่อแสดงข้อมูลเพิ่มเติมได้ด้วย

ลิ้นชักการนำทาง
ลิ้นชักการนำทางช่วยให้คุณดูข้อมูลโดยละเอียดสำหรับแท็บการนำทางได้อย่างง่ายดาย และเข้าถึงได้ง่ายเมื่อใช้แท็บเล็ตหรืออุปกรณ์ขนาดใหญ่ ลิ้นชักการนำทางมี 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 Classes แบบกะทัดรัด ปานกลาง และขยาย เลย์เอาต์ Canonical ของรายละเอียดรายการเหมาะอย่างยิ่งสำหรับกรณีการใช้งานนี้ และพร้อมใช้งานใน Compose เป็น ListDetailPaneScaffold
- รับคอมโพเนนต์นี้โดยเพิ่มทรัพยากร Dependency ต่อไปนี้และทำการซิงค์ 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())
}
)
}
โค้ดนี้จะสร้าง Navigator ก่อนโดยใช้ rememberListDetailPaneNavigator แถบนำทางช่วยให้คุณควบคุมได้ว่าจะแสดงแผงใดและควรแสดงเนื้อหาใดในแผงนั้น ซึ่งจะแสดงให้ดูในภายหลัง
ListDetailPaneScaffold จะแสดง 2 บานหน้าต่างเมื่อขยายคลาสขนาดความกว้างของหน้าต่าง มิเช่นนั้น ระบบจะแสดงบานหน้าต่างใดบานหน้าต่างหนึ่งตามค่าที่ระบุสำหรับพารามิเตอร์ 2 รายการ ได้แก่ คำสั่ง Scaffold และค่า Scaffold หากต้องการรับลักษณะการทำงานเริ่มต้น โค้ดนี้จะใช้คำสั่ง Scaffold และค่า Scaffold ที่ระบุโดย Navigator
พารามิเตอร์ที่เหลือที่จำเป็นคือ Composable Lambdas สำหรับบานหน้าต่าง 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 ในหน้าต่างขนาดเล็ก การดำเนินการนี้จะทำให้ดูเหมือนว่าแอปได้ไปยังหน้าถัดไปแล้ว
นอกจากนี้ แอปยังต้องจัดการสิ่งที่เกิดขึ้นเมื่อผู้ใช้กดปุ่มย้อนกลับจากแผงรายละเอียดด้วย และลักษณะการทำงานนี้จะแตกต่างกันไปขึ้นอยู่กับว่ามีแผงที่มองเห็นได้ 1 หรือ 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)
}
}
}
)
}
Navigator รู้สถานะทั้งหมดของ ListDetailPaneScaffold ไม่ว่าจะย้อนกลับได้หรือไม่ และสิ่งที่ต้องทำในสถานการณ์ทั้งหมดนี้ โค้ดนี้จะสร้าง BackHandler ที่เปิดใช้เมื่อใดก็ตามที่ Navigator สามารถย้อนกลับได้ และภายใน Lambda จะเรียก navigateBack() นอกจากนี้ เพื่อให้การเปลี่ยนผ่านระหว่างบานหน้าต่างราบรื่นยิ่งขึ้น เราจึงห่อหุ้มแต่ละบานหน้าต่างด้วย AnimatedPane() composable
เรียกใช้แอปอีกครั้งในโปรแกรมจำลองที่ปรับขนาดได้สำหรับอุปกรณ์ประเภทต่างๆ และสังเกตว่าเมื่อใดก็ตามที่การกำหนดค่าหน้าจอเปลี่ยนแปลง หรือคุณกางอุปกรณ์พับได้ เนื้อหาการนำทางและหน้าจอจะเปลี่ยนแปลงแบบไดนามิกเพื่อตอบสนองต่อการเปลี่ยนแปลงสถานะของอุปกรณ์ นอกจากนี้ ให้ลองแตะอีเมลในแผงรายการและดูว่าเลย์เอาต์ทำงานอย่างไรบนหน้าจอต่างๆ โดยแสดงทั้ง 2 แผงแบบคู่กันหรือเคลื่อนไหวระหว่างแผงอย่างราบรื่น

ยินดีด้วย คุณปรับแอปให้เข้ากับสถานะและขนาดของอุปกรณ์ทุกประเภทได้สำเร็จแล้ว ลองเล่นแอปในอุปกรณ์พับได้ แท็บเล็ต หรืออุปกรณ์เคลื่อนที่อื่นๆ
6. ขอแสดงความยินดี
ยินดีด้วย คุณทำ Codelab นี้เสร็จสมบูรณ์แล้ว และได้เรียนรู้วิธีสร้างแอปที่ปรับเปลี่ยนตามอุปกรณ์ต่างๆ ด้วย Jetpack Compose
คุณได้เรียนรู้วิธีตรวจสอบขนาดและสถานะการพับของอุปกรณ์ รวมถึงวิธีอัปเดต UI, การนำทาง และฟังก์ชันอื่นๆ ของแอปตามนั้น นอกจากนี้ คุณยังได้เรียนรู้วิธีที่ความสามารถในการปรับตัวช่วยเพิ่มการเข้าถึงและปรับปรุงประสบการณ์ของผู้ใช้
ขั้นตอนต่อไปคืออะไร
ดู Codelab อื่นๆ ในเส้นทาง Compose
แอปตัวอย่าง
- ตัวอย่าง Compose คือชุดแอปจำนวนมากที่รวมแนวทางปฏิบัติแนะนำที่อธิบายไว้ใน Codelab