การปรับขนาดแอป Android

1. บทนำ

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

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

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

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

คุณจะได้สำรวจผลกระทบของการปรับขนาดแบบอิสระและเพิ่มประสิทธิภาพแอป Android เพื่อแสดงแนวทางปฏิบัติแนะนำสำหรับการปรับขนาด แอปของคุณจะทำสิ่งต่อไปนี้

มีไฟล์ Manifest ที่เข้ากันได้

  • นำข้อจำกัดที่ทำให้แอปปรับขนาดได้อย่างอิสระออก

รักษาสถานะเมื่อปรับขนาด

  • คงสถานะ UI ไว้เมื่อปรับขนาดโดยใช้ rememberSaveable
  • หลีกเลี่ยงการทำซ้ำงานเบื้องหลังโดยไม่จำเป็นเพื่อเริ่มต้น UI

สิ่งที่คุณต้องมี

  1. มีความรู้ในการสร้างแอปพลิเคชัน Android พื้นฐาน
  2. ความรู้เกี่ยวกับ ViewModel และ State ใน Compose
  3. อุปกรณ์ทดสอบที่รองรับการปรับขนาดหน้าต่างรูปแบบอิสระ เช่น อุปกรณ์ต่อไปนี้

หากพบปัญหา (ข้อบกพร่องของโค้ด ข้อผิดพลาดทางไวยากรณ์ คำที่ไม่ชัดเจน ฯลฯ) ขณะทำตาม Codelab นี้ โปรดรายงานปัญหาผ่านลิงก์รายงานข้อผิดพลาดที่มุมซ้ายล่างของ Codelab

2. เริ่มต้นใช้งาน

โคลนที่เก็บจาก GitHub

git clone https://github.com/android/large-screen-codelabs/

...หรือดาวน์โหลดไฟล์ ZIP ของที่เก็บและแตกไฟล์

นำเข้าโปรเจ็กต์

  • เปิด Android Studio
  • เลือกนำเข้าโปรเจ็กต์หรือไฟล์->ใหม่->นำเข้าโปรเจ็กต์
  • ไปที่ตำแหน่งที่คุณโคลนหรือแยกโปรเจ็กต์
  • เปิดโฟลเดอร์ปรับขนาด
  • เปิดโปรเจ็กต์ในโฟลเดอร์ start ซึ่งมีโค้ดเริ่มต้น

ลองใช้แอป

  • สร้างและเรียกใช้แอป
  • ลองปรับขนาดแอป

คุณมีความคิดเห็นอย่างไร

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

ข้อจำกัดของไฟล์ Manifest

หากดูในไฟล์ AndroidManifest.xml ของแอป คุณจะเห็นว่ามีการเพิ่มข้อจำกัดบางอย่างซึ่งทำให้แอปของเราทำงานได้ไม่ดีในสภาพแวดล้อมการปรับขนาดหน้าต่างแบบอิสระ

AndroidManifest.xml

            android:maxAspectRatio="1.4"
            android:resizeableActivity="false"
            android:screenOrientation="portrait">

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

3. การเปลี่ยนแปลงการกำหนดค่าของการปรับขนาด

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

สังเกตการเปลี่ยนแปลงการกำหนดค่า

หากต้องการดูการเปลี่ยนแปลงเหล่านี้ด้วยตนเองในแอปที่สร้างด้วยระบบมุมมองของ Android คุณสามารถลบล้าง View.onConfigurationChanged ได้ ใน Jetpack Compose เรามีสิทธิ์เข้าถึง LocalConfiguration.current ซึ่งจะอัปเดตโดยอัตโนมัติเมื่อใดก็ตามที่มีการเรียกใช้ View.onConfigurationChanged

หากต้องการดูการเปลี่ยนแปลงการกำหนดค่าเหล่านี้ในแอปตัวอย่าง ให้เพิ่ม Composable ลงในแอปที่แสดงค่าจาก LocalConfiguration.current หรือสร้างโปรเจ็กต์ตัวอย่างใหม่ที่มี Composable ดังกล่าว ตัวอย่าง UI สำหรับดูข้อมูลเหล่านี้จะมีลักษณะดังนี้

val configuration = LocalConfiguration.current
val isPortrait = configuration.orientation ==
    Configuration.ORIENTATION_PORTRAIT
val screenLayoutSize =
        when (configuration.screenLayout and
                Configuration.SCREENLAYOUT_SIZE_MASK) {
            SCREENLAYOUT_SIZE_SMALL -> "SCREENLAYOUT_SIZE_SMALL"
            SCREENLAYOUT_SIZE_NORMAL -> "SCREENLAYOUT_SIZE_NORMAL"
            SCREENLAYOUT_SIZE_LARGE -> "SCREENLAYOUT_SIZE_LARGE"
            SCREENLAYOUT_SIZE_XLARGE -> "SCREENLAYOUT_SIZE_XLARGE"
            else -> "undefined value"
        }
Column(
    horizontalAlignment = Alignment.CenterHorizontally,
    modifier = Modifier.fillMaxWidth()
) {
    Text("screenWidthDp: ${configuration.screenWidthDp}")
    Text("screenHeightDp: ${configuration.screenHeightDp}")
    Text("smallestScreenWidthDp: ${configuration.smallestScreenWidthDp}")
    Text("orientation: ${if (isPortrait) "portrait" else "landscape"}")
    Text("screenLayout SIZE: $screenLayoutSize")
}

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

เมื่อปรับขนาดแอป ข้อมูลการกำหนดค่าที่เปลี่ยนแปลงจะแสดงในอินเทอร์เฟซของแอปแบบเรียลไทม์

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

4. การบันทึกเหตุการณ์ในวงจรกิจกรรม

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

lifecycle.addObserver(object : LifecycleEventObserver {
        override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
        Log.d("resizing-codelab-lifecycle", "$event was called")
    }
})

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

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

logcat แสดงเมธอดวงจรกิจกรรมที่เรียกใช้เมื่อปรับขนาด

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

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

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

หากต้องการลดความซับซ้อนบางอย่างที่เกี่ยวข้องกับการเปลี่ยนแปลงการกำหนดค่า ให้ใช้ API ระดับสูงกว่า เช่น WindowSizeClass เพื่อใช้ UI แบบปรับได้ (ดูเพิ่มเติมที่รองรับหน้าจอขนาดต่างๆ)

5. ความต่อเนื่อง - การรักษาสถานะภายในของ Composable เมื่อปรับขนาด

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

เริ่มต้นด้วยการทำให้NavigationDrawerHeaderฟังก์ชันที่ประกอบกันได้ (อยู่ใน ReplyHomeScreen.kt) ขยายเพื่อแสดงอีเมลเมื่อคลิก

@Composable
private fun NavigationDrawerHeader(
    modifier: Modifier = Modifier
) {
    var showDetails by remember { mutableStateOf(false) }
    Column(
        modifier = modifier.clickable {
                showDetails = !showDetails
            }
    ) {


        Row(
            horizontalArrangement = Arrangement.SpaceBetween,
            verticalAlignment = Alignment.CenterVertically
        ) {
            ReplyLogo(
                modifier = Modifier
                    .size(dimensionResource(R.dimen.reply_logo_size))
            )
            ReplyProfileImage(
                drawableResource = LocalAccountsDataProvider
                    .userAccount.avatar,
                description = stringResource(id = R.string.profile),
                modifier = Modifier
                    .size(dimensionResource(R.dimen.profile_image_size))
            )
        }
        AnimatedVisibility (showDetails) {
            Text(
                text = stringResource(id = LocalAccountsDataProvider
                        .userAccount.email),
                style = MaterialTheme.typography.labelMedium,
                modifier = Modifier
                    .padding(
                        start = dimensionResource(
                            R.dimen.drawer_padding_header),
                        end = dimensionResource(
                            R.dimen.drawer_padding_header),
                        bottom = dimensionResource(
                            R.dimen.drawer_padding_header)
                ),


            )
        }
    }
}

เมื่อเพิ่มส่วนหัวที่ขยายได้ลงในแอปแล้ว ให้ทำดังนี้

  1. เรียกใช้แอปบนอุปกรณ์ทดสอบ
  2. แตะส่วนหัวเพื่อขยาย
  3. ลองปรับขนาดหน้าต่าง

คุณจะเห็นว่าส่วนหัวจะสูญเสียสถานะเมื่อมีการปรับขนาดอย่างมาก

แตะส่วนหัวในลิ้นชักการนำทางของแอปแล้วขยาย แต่จะยุบหลังจากปรับขนาดแอป

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

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

6. หลีกเลี่ยงการทำซ้ำงานเบื้องหลังโดยไม่จำเป็น

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

หากต้องการดูตัวอย่างปัญหาที่คุณอาจพบ ให้เพิ่มคำสั่งบันทึกลงในเมธอด initializeUIState ใน ReplyViewModel

fun initializeUIState() {
    Log.d("resizing-codelab", "initializeUIState() called in the viewmodel")
    val mailboxes: Map<MailboxType, List<Email>> =
        LocalEmailsDataProvider.allEmails.groupBy { it.mailbox }
    _uiState.value =
        ReplyUiState(
            mailboxes = mailboxes,
            currentSelectedEmail = mailboxes[MailboxType.Inbox]?.get(0)
                ?: LocalEmailsDataProvider.defaultEmail
        )
}

ตอนนี้เรียกใช้แอปบนอุปกรณ์ทดสอบ แล้วลองปรับขนาดหน้าต่างของแอปหลายๆ ครั้ง

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

หากต้องการหลีกเลี่ยงการทำงานเบื้องหลังที่ไม่จำเป็น ให้นำการเรียกใช้ initializeUIState() ออกจากเมธอด onCreate() ของกิจกรรม แต่ให้เริ่มต้นข้อมูลในเมธอด init ของ ViewModel แทน ซึ่งจะช่วยให้มั่นใจได้ว่าเมธอดการเริ่มต้นจะทํางานเพียงครั้งเดียวเมื่อมีการสร้างอินสแตนซ์ ReplyViewModel เป็นครั้งแรก

init {
    initializeUIState()
}

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

7. ยินดีด้วย!

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

ตัวอย่างซอร์สโค้ด

โคลนที่เก็บจาก GitHub

git clone https://github.com/android/large-screen-codelabs/

...หรือดาวน์โหลดไฟล์ ZIP ของที่เก็บและแตกไฟล์