Thay đổi kích thước ứng dụng Android

1. Giới thiệu

Hệ sinh thái thiết bị Android không ngừng phát triển. Từ buổi ban đầu khi bàn phím phần cứng tích hợp mới ra mắt cho đến hiện tại với sự góp mặt của các thiết bị lật, thiết bị gập, máy tính bảng và cửa sổ có thể đổi kích thước tuỳ ý — chưa bao giờ ứng dụng Android lại chạy trên nhiều thiết bị đa dạng như ngày nay.

Mặc dù đây là tin vui cho nhà phát triển, nhưng họ cần phải triển khai một số điểm tối ưu hoá nhất định cho ứng dụng để đáp ứng kỳ vọng về khả năng hữu dụng và mang lại trải nghiệm chất lượng cao cho người dùng trên các kích thước màn hình khác nhau. Thay vì phải nhắm đến từng thiết bị mới, một giao diện người dùng thích ứng/thích nghi cùng kiến trúc linh hoạt có thể giúp ứng dụng của bạn trông đẹp mắt và hoạt động mượt mà trên mọi thiết bị mà người dùng hiện tại và tương lai sử dụng — tức là trên các thiết bị với đủ loại kích thước và hình dạng!

Với sự ra mắt của những môi trường Android có thể thay đổi kích thước tuỳ ý, thì vấn đề kiểm thử giao diện người dùng thích ứng/thích nghi để sẵn sàng cho mọi thiết bị lại càng trở nên quan trọng hơn bao giờ hết. Lớp học lập trình này sẽ hướng dẫn bạn nắm bắt được tác động của việc thay đổi kích thước cũng như triển khai một số phương pháp hay nhất để giúp ứng dụng thay đổi kích thước một cách triệt để và dễ dàng.

Sản phẩm bạn sẽ tạo ra

Bạn sẽ khám phá tác động của việc thay đổi kích thước tuỳ ý và tối ưu hoá một ứng dụng Android để thể hiện các phương pháp thay đổi kích thước hay nhất. Ứng dụng này sẽ:

Có một tệp kê khai tương thích

  • Gỡ bỏ những quy tắc hạn chế ngăn ứng dụng tự do thay đổi kích thước

Duy trì trạng thái khi thay đổi kích thước

  • Duy trì trạng thái giao diện người dùng khi thay đổi kích thước bằng rememberSaveable
  • Tránh trùng lặp công việc ở chế độ nền một cách không cần thiết trong quá trình khởi tạo giao diện người dùng

Bạn cần có

  1. Kiến thức về cách tạo ứng dụng Android cơ bản
  2. Kiến thức về ViewModel và State trong Compose
  3. Một thiết bị kiểm thử có hỗ trợ thay đổi kích thước cửa sổ tuỳ ý, chẳng hạn như một trong những thiết bị sau:
  • Thiết bị Chromebook có thiết lập ADB
  • Máy tính bảng có hỗ trợ Chế độ Samsung DeX hoặc Chế độ sản xuất (Productivity Mode)
  • Trình mô phỏng The Desktop Android Virtual Device (Thiết bị Android ảo trên máy tính) trong Android Studio

Nếu bạn gặp vấn đề (lỗi trong đoạn mã, lỗi ngữ pháp, từ ngữ không rõ ràng, v.v.) khi thực hành theo lớp học lập trình này, vui lòng báo cáo vấn đề thông qua đường liên kết Báo cáo lỗi ở góc dưới bên trái lớp học lập trình.

2. Bắt đầu

Sao chép kho lưu trữ trên GitHub.

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

...hoặc tải tệp ZIP của kho lưu trữ xuống rồi giải nén tệp đó

Nhập dự án

  • Mở Android Studio
  • Chọn Import Project (Nhập dự án) hoặc File->New->Import Project (Tệp->Mới->Nhập dự án)
  • Chuyển đến vị trí bạn nhân bản hoặc giải nén dự án
  • Mở thư mục resizing.
  • Mở dự án trong thư mục start. Đây là thư mục chứa đoạn mã khởi đầu.

Dùng thử ứng dụng

  • Tạo bản dựng rồi chạy ứng dụng
  • Thử đổi kích thước ứng dụng

Bạn thấy như thế nào?

Tuỳ thuộc vào khả năng tương thích mà thiết bị kiểm thử hỗ trợ, có thể bạn sẽ nhận thấy rằng trải nghiệm người dùng chưa được lý tưởng cho lắm. Ứng dụng không thể thay đổi kích thước và mắc kẹt ở tỷ lệ khung hình ban đầu. Điều gì đang xảy ra?

Quy tắc hạn chế trong tệp kê khai

Nếu nhìn vào tệp AndroidManifest.xml của ứng dụng, bạn có thể thấy rằng có một số quy tắc hạn chế được thêm vào đang ngăn ứng dụng của chúng ta hoạt động hiệu quả trong môi trường cửa sổ thay đổi kích thước tuỳ ý.

AndroidManifest.xml

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

Hãy thử xoá 3 dòng có vấn đề này khỏi tệp kê khai của bạn, dựng lại ứng dụng rồi thử lại trên thiết bị kiểm thử. Bạn sẽ nhận thấy rằng ứng dụng không còn bị hạn chế thay đổi kích thước tuỳ ý nữa. Việc xoá các quy tắc hạn chế như vậy khỏi tệp kê khai của bạn là một bước quan trọng trong việc tối ưu hoá ứng dụng để có thể thay đổi kích thước cửa sổ tuỳ ý.

3. Các thay đổi về cấu hình khi đổi kích thước

Khi cửa sổ ứng dụng thay đổi kích thước, Cấu hình của ứng dụng sẽ được cập nhật. Việc cập nhật này có tác động đối với ứng dụng của bạn. Hãy nắm rõ và dự đoán những thay đổi để có thể mang lại cho người dùng của bạn trải nghiệm chất lượng cao. Những thay đổi rõ ràng nhất là về chiều rộng và chiều cao của cửa sổ ứng dụng, nhưng những thay đổi này cũng có tác động đến cả tỷ lệ khung hình và hướng.

Quan sát các thay đổi về cấu hình

Để xem những thay đổi này diễn ra trong một ứng dụng được xây dựng bằng hệ thống thành phần hiển thị Android, bạn có thể ghi đè View.onConfigurationChanged. Trong Jetpack Compose, chúng ta có thể truy cập vào LocalConfiguration.current (tự động cập nhật mỗi khi View.onConfigurationChanged được gọi).

Để xem những thay đổi về cấu hình này trong ứng dụng mẫu của bạn, hãy thêm vào ứng dụng một thành phần kết hợp dùng để hiển thị các giá trị từ LocalConfiguration.current hoặc tạo một dự án mẫu mới với một thành phần kết hợp như vậy. Một giao diện người dùng mẫu để xem những thay đổi này sẽ trông như sau:

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")
}

Bạn có thể xem ví dụ triển khai trong thư mục dự án observing-configuration-changes. Hãy thử thêm phương thức triển khai này vào giao diện người dùng của ứng dụng, rồi chạy nó trên thiết bị kiểm thử và xem giao diện người dùng cập nhật khi cấu hình ứng dụng của bạn thay đổi.

khi ứng dụng được thay đổi kích thước, thông tin cấu hình thay đổi sẽ được thể hiện trên giao diện của ứng dụng theo thời gian thực

Những thay đổi này đối với cấu hình ứng dụng của bạn cho phép bạn mô phỏng sự chuyển đổi nhanh chóng từ các điểm cực biên mà chúng ta dự tính từ một màn hình chia đôi trên một thiết bị cầm tay nhỏ sang toàn màn hình trên một chiếc máy tính bảng hoặc máy tính Đây không chỉ là một cách hay để kiểm thử bố cục ứng dụng của bạn trên nhiều màn hình mà còn cho phép bạn kiểm tra xem ứng dụng của mình có thể xử lý các sự kiện thay đổi cấu hình nhanh chóng hiệu quả đến mức nào.

4. Ghi nhật ký các sự kiện trong vòng đời Hoạt động

Một tác động khác của việc thay đổi kích thước cửa sổ tuỳ ý là sẽ có nhiều thay đổi về vòng đời Activity xảy ra đối với ứng dụng. Để xem những thay đổi này theo thời gian thực, hãy thêm một trình quan sát vòng đời vào phương thức onCreate, rồi ghi nhật ký từng sự kiện vòng đời mới bằng cách ghi đè onStateChanged.

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

Sau khi chuẩn bị xong việc ghi nhật ký này, hãy chạy lại ứng dụng của bạn trên thiết bị kiểm thử rồi xem logcat khi bạn tìm cách thu nhỏ ứng dụng của mình rồi đưa ứng dụng đó lên nền trước một lần nữa.

Để ý rằng ứng dụng của bạn bị tạm dừng khi thu nhỏ rồi tiếp tục lại khi được đưa lên nền trước. Điều này có tác động với ứng dụng của bạn và bạn sẽ tìm hiểu những tác động đó trong phần sắp tới của lớp học lập trình này (phần này sẽ tập trung vào tính liên tục).

logcat cho thấy các phương thức vòng đời hoạt động đang được gọi khi thay đổi kích thước

Bây giờ, hãy xem Logcat để biết phương thức gọi lại trong vòng đời hoạt động nào được gọi khi bạn đổi kích thước ứng dụng của mình từ kích thước nhỏ nhất có thể đến kích thước lớn nhất có thể

Tuỳ thuộc vào thiết bị kiểm thử mà bạn có thể quan sát thấy các hành vi khác nhau, nhưng có thể bạn sẽ nhận thấy rằng hoạt động của mình bị huỷ và được tạo lại khi kích thước cửa sổ ứng dụng thay đổi đáng kể, còn khi kích thước cửa sổ thay đổi một chút thì không. Là bởi vì trên API 24 trở lên, chỉ những thay đổi đáng kể về kích thước mới dẫn đến việc tạo lại Activity.

Vậy là bạn đã thấy một số thay đổi thường gặp về cấu hình mà bạn có thể lường trước trong một môi trường cửa sổ kích thước tuỳ ý, nhưng vẫn có những thay đổi khác cần lưu ý. Ví dụ: nếu có một màn hình ngoài được kết nối với thiết bị kiểm thử, bạn có thể thấy Activity của mình bị huỷ và được tạo lại do những thay đổi về cấu hình như mật độ hiển thị.

Để rút ra hoá một số vấn đề phức tạp liên quan đến những thay đổi về cấu hình, hãy sử dụng các API cấp cao hơn như WindowSizeClass để triển khai giao diện người dùng thích ứng. (Xem thêm nội dung về Hỗ trợ nhiều kích thước màn hình.)

5. Tính liên tục – Duy trì trạng thái bên trong của thành phần kết hợp khi thay đổi kích thước

Trong phần trước, bạn đã thấy một số thay đổi về cấu hình mà ứng dụng của bạn có thể mong đợi trong một môi trường cửa sổ thay đổi kích thước tuỳ ý. Trong phần này, bạn sẽ duy trì tính liên tục của trạng thái giao diện người dùng của ứng dụng trong suốt thời gian diễn ra những thay đổi này.

Hãy bắt đầu bằng cách mở rộng hàm có khả năng kết hợp NavigationDrawerHeader (có trong ReplyHomeScreen.kt) để hiện địa chỉ email khi được nhấp vào.

@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)
                ),


            )
        }
    }
}

Khi bạn đã thêm tiêu đề có thể mở rộng vào ứng dụng của mình, hãy

  1. chạy ứng dụng trên thiết bị kiểm thử
  2. nhấn vào tiêu đề để mở rộng
  3. thử thay đổi kích thước cửa sổ

Bạn sẽ thấy tiêu đề không còn giữ được trạng thái khi bị đổi kích thước đáng kể.

Tiêu đề trên ngăn điều hướng của ứng dụng được nhấn vào và mở rộng, nhưng lại thu gọn sau khi ứng dụng đổi kích thước

Trạng thái giao diện người dùng bị mất do remember chỉ giúp bạn giữ lại trạng thái trong quá trình tái kết hợp chứ không phải trên hoạt động hoặc trong quá trình tạo lại. Thường thì chúng ta sẽ dùng phương pháp chuyển trạng thái lên trên (state hoisting), chuyển trạng thái sang phương thức gọi của thành phần kết hợp để khiến các thành phần kết hợp trở thành không có trạng thái, nhờ đó tránh được hoàn toàn vấn đề này. Theo đó, bạn có thể sử dụng remember nếu muốn giữ trạng thái của phần tử trên giao diện người dùng bên trong các hàm có khả năng kết hợp.

Để giải quyết những vấn đề này, hãy thay remember bằng rememberSaveable. Cách này có hiệu quả, bởi vì rememberSaveable lưu và khôi phục giá trị đã ghi nhớ cho savedInstanceState. Thay đổi remember thành rememberSaveable, chạy ứng dụng của bạn trên thiết bị kiểm thử và thử đổi kích thước ứng dụng một lần nữa. Bạn sẽ nhận thấy rằng trạng thái của tiêu đề có thể mở rộng sẽ được giữ nguyên trong suốt quá trình thay đổi kích thước đúng như mong muốn.

6. Tránh trùng lặp công việc ở chế độ nền một cách không cần thiết

Bạn đã tìm hiểu cách sử dụng rememberSaveable để duy trì trạng thái giao diện người dùng nội bộ của thành phần kết hợp thông qua các thay đổi về cấu hình có thể xảy ra thường xuyên do việc thay đổi kích thước cửa sổ tuỳ ý. Tuy nhiên, ứng dụng thường phải chuyển trạng thái giao diện người dùng và logic ra khỏi các thành phần kết hợp. Di chuyển quyền sở hữu trạng thái sang ViewModel là một trong những cách hay nhất để duy trì trạng thái trong quá trình đổi kích thước. Khi chuyển trạng thái vào một ViewModel, có thể bạn sẽ gặp phải sự cố với những công việc ở chế độ nền trong thời gian dài, chẳng hạn như khi truy cập các tệp nặng trong hệ thống hoặc khi đưa ra các lệnh gọi mạng cần thiết để khởi tạo màn hình của bạn.

Để xem ví dụ về những loại vấn đề mà bạn có thể gặp phải, hãy thêm một câu lệnh nhật ký vào phương thức initializeUIState trong 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
        )
}

Giờ thì chạy ứng dụng trên thiết bị kiểm thử rồi thử đổi kích thước cửa sổ ứng dụng của bạn nhiều lần.

Khi xem Logcat, bạn sẽ nhận thấy rằng ứng dụng của mình đã chạy phương thức khởi tạo nhiều lần. Đây có thể là một vấn đề đối với công việc mà bạn chỉ muốn chạy một lần để khởi tạo giao diện người dùng. Các lệnh gọi mạng bổ sung, phương thức I/O tệp hoặc công việc khác có thể cản trở hiệu suất của thiết bị và gây ra những sự cố khác ngoài ý muốn.

Để tránh trùng lặp công việc ở chế độ nền một cách không cần thiết, hãy xoá phương thức gọi đến initializeUIState() qua phương thức onCreate() của hoạt động. Thay vào đó, hãy khởi tạo dữ liệu trong phương thức init của ViewModel. Điều này giúp đảm bảo rằng phương thức khởi tạo chỉ chạy một lần, khi ReplyViewModel được khởi tạo lần đầu tiên:

init {
    initializeUIState()
}

Hãy thử chạy lại ứng dụng và bạn có thể thấy tác vụ khởi tạo không cần thiết theo mô phỏng chỉ chạy một lần, bất kể bạn thay đổi kích thước cửa sổ ứng dụng bao nhiêu lần. Lý do là các ViewModel tồn tại vượt quá vòng đời của Activity. Bằng cách chỉ chạy đoạn mã khởi tạo một lần khi tạo ViewModel, chúng ta tách đoạn mã đó khỏi mọi hoạt động tạo lại Activity và ngăn chặn những công việc không cần thiết. Nếu đây thực sự là một phương thức gọi máy chủ tốn kém tài nguyên hoặc một hoạt động I/O tệp nặng để khởi tạo giao diện người dùng, thì bạn sẽ tiết kiệm được đáng kể tài nguyên và cải thiện được trải nghiệm người dùng.

7. XIN CHÚC MỪNG!

Các bạn đã làm được! Tuyệt vời! Bạn đã triển khai được một số phương pháp hay nhất để cho phép ứng dụng Android thay đổi kích thước hiệu quả trên ChromeOS cũng như các môi trường nhiều cửa sổ và nhiều màn hình khác.

Mã nguồn mẫu

Sao chép kho lưu trữ trên GitHub

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

...hoặc tải tệp ZIP của kho lưu trữ xuống rồi giải nén tệp đó