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

1. เกริ่นนำ

ใน Codelab นี้ คุณจะได้เรียนรู้วิธีสร้างแอปที่ปรับขนาดได้สำหรับโทรศัพท์ แท็บเล็ต และอุปกรณ์แบบพับได้ รวมถึงวิธีปรับปรุงความสามารถในการเข้าถึงด้วย Jetpack Compose นอกจากนี้ คุณจะได้เรียนรู้แนวทางปฏิบัติแนะนำในการใช้คอมโพเนนต์และธีมของ Material 3 ด้วย

ก่อนที่จะเจาะลึกรายละเอียด คุณควรเข้าใจว่าความสามารถในการปรับตัวหมายถึงอะไร

ความสามารถในการปรับตัว

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

ดูข้อมูลเพิ่มเติมได้ที่การออกแบบที่ปรับอัตโนมัติ

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

สิ่งที่คุณจะได้เรียนรู้

  • วิธีออกแบบแอปเพื่อกำหนดเป้าหมายหน้าต่างทุกขนาดด้วย Jetpack Compose
  • วิธีกำหนดเป้าหมายแอปของคุณสำหรับอุปกรณ์แบบพับได้แบบต่างๆ
  • วิธีใช้การนำทางประเภทต่างๆ เพื่อการเข้าถึงและการช่วยเหลือพิเศษที่ดียิ่งขึ้น
  • วิธีใช้คอมโพเนนต์ Material 3 เพื่อมอบประสบการณ์ที่ดีที่สุดสำหรับหน้าต่างทุกขนาด

สิ่งที่ต้องมี

  • Android Studio JellyFish ขึ้นไป
  • อุปกรณ์เสมือน Android 13 ที่ปรับขนาดได้
  • ความรู้เกี่ยวกับ Kotlin
  • ความเข้าใจเบื้องต้นเกี่ยวกับการเขียน (เช่น คำอธิบายประกอบ @Composable)
  • ความคุ้นเคยกับเลย์เอาต์การเขียน (เช่น Row และ Column) ในระดับเบื้องต้น
  • มีความคุ้นเคยกับตัวแก้ไขเบื้องต้น (เช่น Modifier.padding())

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

โปรแกรมจำลองที่ปรับขนาดได้พร้อมทั้งโทรศัพท์ กางออก แท็บเล็ต และเดสก์ท็อป

หากคุณไม่คุ้นเคยกับ Compose ให้ลองใช้ Codelab พื้นฐานสำหรับ Jetpack Compose ก่อนที่จะดำเนินการ Codelab นี้

สิ่งที่คุณจะสร้าง

  • แอปโปรแกรมรับส่งอีเมลแบบอินเทอร์แอกทีฟที่ใช้แนวทางปฏิบัติแนะนำสำหรับการออกแบบที่ปรับเปลี่ยนได้ การนำทางใน Material แบบต่างๆ และการใช้พื้นที่หน้าจออย่างเหมาะสม

การแสดงการสนับสนุนอุปกรณ์ที่หลากหลายที่คุณจะได้รับใน Codelab นี้

2. ตั้งค่า

หากต้องการรับโค้ดสำหรับ Codelab ให้โคลนที่เก็บ GitHub จากบรรทัดคำสั่งนี้

git clone https://github.com/android/codelab-android-compose.git
cd codelab-android-compose/AdaptiveUiCodelab

หรือจะดาวน์โหลดที่เก็บเป็นไฟล์ ZIP ก็ได้ โดยทำดังนี้

เราขอแนะนำให้คุณเริ่มจากโค้ดใน Branch หลัก แล้วทำตาม Codelab ทีละขั้นตอนได้ตามต้องการ

เปิดโปรเจ็กต์ใน Android Studio

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

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

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

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

คุณจะเน้นที่ MainActivity.kt ก่อน

MainActivity.kt

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)

    setContent {
        ReplyTheme {
            val uiState by viewModel.uiState.collectAsStateWithLifecycle()
            ReplyApp(
                replyHomeUIState = uiState,
                onEmailClick = viewModel::setSelectedEmail
            )
        }
    }
}

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

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

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

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

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

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

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

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

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

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

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

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

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

พับสถานะ

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

ท่าแบบพับได้ แบนและกางครึ่งออก

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

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

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

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

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

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

gradle/libs.versions.toml

[versions]
material3Adaptive = "1.0.0-beta01"

[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() ในขอบเขต Composable ใดก็ได้เพื่อรับออบเจ็กต์ 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(20.dp)
            )
        }
    }
}

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

4. การนำทางแบบไดนามิก

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

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

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

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

Bottom Navigation

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

แถบนำทางด้านล่างที่มีรายการ

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

แถบนำทางพร้อมรายการ

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

ลิ้นชักการนำทางสำหรับคำสั่ง

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

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

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

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

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

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

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

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

  1. เพิ่มการอ้างอิง Gradle เพื่อรับคอมโพเนนต์นี้โดยการอัปเดตแคตตาล็อกเวอร์ชันและสคริปต์บิลด์ของแอป จากนั้นทำการซิงค์ Gradle ตามขั้นตอนต่อไปนี้

gradle/libs.versions.toml

[versions]
material3AdaptiveNavSuite = "1.3.0-beta01"

[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. ค้นหาฟังก์ชัน Composable 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. การใช้พื้นที่หน้าจอ

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

วัสดุ 3 จะกำหนดรูปแบบตามรูปแบบบัญญัติ 3 รูปแบบ ซึ่งแต่ละรายการมีการกำหนดค่าสำหรับคลาสขนาดหน้าต่างที่กะทัดรัด ปานกลาง และขยาย รูปแบบตามรูปแบบบัญญัติรายละเอียดรายการเหมาะสำหรับกรณีการใช้งานนี้ และสามารถใช้ได้ในการเขียนเป็น ListDetailPaneScaffold

  1. รับคอมโพเนนต์นี้ด้วยการเพิ่มทรัพยากร Dependency ต่อไปนี้และทำการซิงค์ Gradle

gradle/libs.versions.toml

[versions]
material3Adaptive = "1.0.0-beta01"

[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. ค้นหาฟังก์ชัน Composable ReplyAppContent() ใน ReplyApp.kt ซึ่งปัจจุบันแสดงเฉพาะแผงรายการโดยเรียกใช้ ReplyListPane() แทนที่การใช้งานนี้ด้วย ListDetailPaneScaffold ด้วยการแทรกโค้ดต่อไปนี้ เนื่องจาก API นี้เป็น 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 ช่องเมื่อขยายคลาสขนาดความกว้างของหน้าต่าง มิฉะนั้นจะแสดง 1 แผงหรืออีกแผงตามค่าที่ระบุสำหรับพารามิเตอร์ 2 รายการ ได้แก่ คำสั่ง Scaffold และค่า Scaffold ในการรับลักษณะการทำงานเริ่มต้น โค้ดนี้ใช้คำสั่ง Scaffold และค่า Scaffold ที่ตัวนำทางระบุไว้

พารามิเตอร์ที่จําเป็นที่เหลือคือ 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 นี้ใช้ตัวนำทางที่สร้างขึ้นก่อนหน้านี้เพื่อเพิ่มลักษณะการทำงานเพิ่มเติมเมื่อมีการคลิกรายการ โดยจะเรียก 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 อื่นๆ ในเส้นทางการเขียน

แอปตัวอย่าง

  • ตัวอย่างการเขียนคือคอลเล็กชันของแอปจำนวนมากที่มีแนวทางปฏิบัติแนะนำที่อธิบายไว้ใน Codelab

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