สร้างแอปแบบปรับเปลี่ยนได้ด้วย Jetpack Compose

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

  1. ในหน้าต่างยินดีต้อนรับสู่ Android Studio ให้เลือก c01826594f360d94.pngเปิดโปรเจ็กต์ที่มีอยู่
  2. เลือกโฟลเดอร์ <Download Location>/AdaptiveUiCodelab (ตรวจสอบว่าคุณได้เลือกไดเรกทอรี AdaptiveUiCodelab ที่มี build.gradle)
  3. เมื่อ Android Studio นำเข้าโปรเจ็กต์แล้ว ให้ทดสอบว่าคุณเรียกใช้สาขา main ได้

สํารวจโค้ดเริ่มต้น

รหัสสาขา main จะมีแพ็กเกจ ui คุณจะต้องทำงานกับไฟล์ต่อไปนี้ในแพ็กเกจดังกล่าว

  • MainActivity.kt - กิจกรรมจุดแรกเข้าที่คุณเริ่มแอป
  • ReplyApp.kt - มีคอมโพสิเบิล UI ของหน้าจอหลัก
  • ReplyHomeViewModel.kt - ระบุข้อมูลและสถานะ UI สำหรับเนื้อหาแอป
  • ReplyListContent.kt - มีคอมโพสิเบิลสสำหรับแสดงรายการและหน้าจอรายละเอียด

หากคุณเรียกใช้แอปนี้บนโปรแกรมจำลองที่ปรับขนาดได้และลองใช้กับอุปกรณ์ประเภทต่างๆ เช่น โทรศัพท์หรือแท็บเล็ต UI จะขยายไปยังพื้นที่ที่กำหนดเท่านั้น แทนที่จะใช้ประโยชน์จากพื้นที่หน้าจอหรือมอบประสบการณ์การใช้งานที่สะดวกสบาย

หน้าจอเริ่มต้นในโทรศัพท์

มุมมองแบบยืดเริ่มต้นในแท็บเล็ต

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

3. ทำให้แอปปรับเปลี่ยนได้

ส่วนนี้จะอธิบายความหมายของการทําให้แอปปรับเปลี่ยนได้ และคอมโพเนนต์ที่ Material 3 มีให้เพื่อทําให้การดำเนินการดังกล่าวง่ายขึ้น นอกจากนี้ยังครอบคลุมประเภทหน้าจอและสถานะที่คุณจะกำหนดเป้าหมาย ซึ่งรวมถึงโทรศัพท์ แท็บเล็ต แท็บเล็ตขนาดใหญ่ และอุปกรณ์พับได้

คุณจะเริ่มต้นด้วยการอธิบายพื้นฐานของขนาดหน้าต่าง ลักษณะการพับ และตัวเลือกการนำทางประเภทต่างๆ จากนั้น คุณจะใช้ API เหล่านี้ในแอปเพื่อให้แอปปรับเปลี่ยนได้มากขึ้น

ขนาดหน้าต่าง

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

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

WindowWidthSizeClass สำหรับความกว้างกะทัดรัด ปานกลาง และขยาย

WindowHeightSizeClass สําหรับความสูงแบบกะทัดรัด กลาง และขยาย

ทั้งความกว้างและความสูงจะจัดประเภทแยกกัน ดังนั้นแอปของคุณจะมี Window Size Class 2 รายการในทุกๆ ช่วงเวลา โดย 1 รายการสำหรับความกว้างและอีก 1 รายการสำหรับความสูง ความกว้างที่ใช้ได้มักจะสำคัญกว่าความสูงที่ใช้ได้เนื่องจากมีการเลื่อนในแนวตั้งอยู่ทั่วไป ดังนั้นในกรณีนี้ คุณจะใช้คลาสขนาดความกว้างด้วย

สถานะการพับ

อุปกรณ์แบบพับได้นำเสนอสถานการณ์เพิ่มเติมที่แอปสามารถปรับให้เข้ากับขนาดที่หลากหลายและข้อต่อของอุปกรณ์ บานพับอาจบดบังบางส่วนของจอแสดงผล ทำให้พื้นที่นั้นไม่เหมาะสมที่จะแสดงเนื้อหา และอาจเป็นการแยกออกจากกัน ซึ่งหมายความว่าจะมีจอแสดงผล 2 จอแยกกันเมื่อกางอุปกรณ์ออก

ลักษณะการพับ แบนและเปิดครึ่งหนึ่ง

นอกจากนี้ ผู้ใช้อาจกําลังดูที่จอแสดงผลด้านในขณะที่บานพับเปิดอยู่ครึ่งหนึ่ง ซึ่งส่งผลให้ท่าทางของร่างกายแตกต่างกันไปตามการวางแนวของรอยพับ ได้แก่ ท่าทางบนโต๊ะ (รอยพับแนวนอน ซึ่งแสดงอยู่ทางด้านขวาในรูปภาพด้านบน) และท่าทางแบบหนังสือ (รอยพับแนวตั้ง)

อ่านเพิ่มเติมเกี่ยวกับลักษณะการพับและบานพับ

ทั้งหมดนี้เป็นสิ่งที่ควรพิจารณาเมื่อใช้เลย์เอาต์แบบปรับเปลี่ยนได้ซึ่งรองรับอุปกรณ์แบบพับได้

รับข้อมูลที่ปรับเปลี่ยนได้

ไลบรารี Material3 adaptive ช่วยให้เข้าถึงข้อมูลเกี่ยวกับหน้าต่างที่แอปทำงานอยู่ได้อย่างสะดวก

  1. เพิ่มรายการสำหรับอาร์ติแฟกต์นี้และเวอร์ชันของอาร์ติแฟกต์ลงในไฟล์แคตตาล็อกเวอร์ชัน

gradle/libs.versions.toml

[versions]
material3Adaptive = "1.0.0"

[libraries]
androidx-material3-adaptive = { module = "androidx.compose.material3.adaptive:adaptive", version.ref = "material3Adaptive" }
  1. ในไฟล์บิลด์ของโมดูลแอป ให้เพิ่มทรัพยากร Dependency ของไลบรารีใหม่ แล้วทำการซิงค์ Gradle โดยทำดังนี้

app/build.gradle.kts

dependencies {

    implementation(libs.androidx.material3.adaptive)
}

ในตอนนี้ คุณสามารถใช้ currentWindowAdaptiveInfo() ในขอบเขตที่ประกอบกันได้ เพื่อรับออบเจ็กต์ WindowAdaptiveInfo ที่มีข้อมูล เช่น คลาสขนาดหน้าต่างปัจจุบัน และดูว่าอุปกรณ์อยู่ในลักษณะพับได้เหมือนอยู่บนโต๊ะหรือไม่

คุณลองดำเนินการนี้ได้แล้วใน MainActivity

  1. ใน onCreate() ภายในบล็อก ReplyTheme ให้รับข้อมูลการปรับหน้าต่างที่แสดงได้และแสดงคลาสขนาดใน Composable 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 มีคอมโพเนนต์หลายรายการที่ช่วยให้คุณนําการนําทางไปใช้ได้ โดยขึ้นอยู่กับคลาสขนาดหน้าต่างของอุปกรณ์

Bottom Navigation

การนําทางด้านล่างเหมาะสําหรับขนาดกะทัดรัด เนื่องจากเราจับอุปกรณ์ในลักษณะที่นิ้วโป้งสามารถเข้าถึงจุดสัมผัสทั้งหมดของการนําทางด้านล่างได้อย่างง่ายดาย ใช้เมื่ออุปกรณ์มีขนาดเล็กหรืออุปกรณ์แบบพับได้อยู่ในสถานะพับ

แถบนําทางด้านล่างพร้อมรายการ

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

แถบข้างสำหรับไปยังส่วนต่างๆ พร้อมรายการ

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

ลิ้นชักการนำทางแบบโมดัล

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

ลิ้นชักการนำทางแบบโมดัลที่มีรายการ

ลิ้นชักการนำทางแบบถาวร

คุณสามารถใช้ลิ้นชักการนำทางแบบถาวรสำหรับการนําทางแบบคงที่ในแท็บเล็ตขนาดใหญ่, Chromebook และเดสก์ท็อป

ลิ้นชักการนำทางแบบถาวรที่มีรายการ

ใช้การนําทางแบบไดนามิก

ตอนนี้คุณจะสลับระหว่างการนําทางประเภทต่างๆ ได้เมื่อสถานะและขนาดของอุปกรณ์เปลี่ยนแปลง

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

  1. เพิ่ม 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)
}
  1. ค้นหาฟังก์ชันคอมโพสิเบิล 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 ปัจจุบัน แต่คุณสามารถทำให้ดำเนินการดังกล่าวได้ด้วยการทําการเปลี่ยนแปลงเล็กน้อย

  1. เพิ่มโค้ดต่อไปนี้ก่อนการเรียก 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

  1. รับคอมโพเนนต์นี้โดยการเพิ่มการพึ่งพาต่อไปนี้และทำการซิงค์ 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)
}
  1. ค้นหาฟังก์ชันที่ประกอบกันได้ 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

  1. เปิด 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
)
  1. ในไฟล์เดียวกัน ReplyHomeViewModel มีฟังก์ชัน setSelectedEmail() ที่เรียกใช้เมื่อผู้ใช้แตะรายการในรายการ แก้ไขฟังก์ชันนี้เพื่อคัดลอกสถานะ UI และบันทึกอีเมลที่เลือก

ReplyHomeViewModel.kt

fun setSelectedEmail(email: Email) {
    _uiState.update {
        it.copy(selectedEmail = email)
    }
}

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

  1. แก้ไขฟังก์ชัน 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()
                )
            }
    }
}
  1. กลับไปที่ ReplyApp.kt และใช้อีเมลที่เลือก (หากมี) เพื่อป้อนข้อมูลในแผงรายละเอียด

ReplyApp.kt

ListDetailPaneScaffold(
    // ...
    detailPane = {
        if (replyHomeUIState.selectedEmail != null) {
            ReplyDetailPane(replyHomeUIState.selectedEmail)
        }
    }
)

เรียกใช้แอปอีกครั้งและเปลี่ยนโปรแกรมจำลองเป็นขนาดแท็บเล็ต แล้วดูว่าการแตะรายการในรายการจะอัปเดตเนื้อหาในแผงรายละเอียด

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

  1. ในการแก้ไขปัญหานี้ ให้แทรกโค้ดต่อไปนี้เมื่อ 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 แผงที่มองเห็น

  1. รองรับการนําทางกลับโดยเพิ่มโค้ดต่อไปนี้

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 อื่นๆ ในเส้นทางการเขียนโค้ด

แอปตัวอย่าง

เอกสารอ้างอิง