1. Giới thiệu
Một trong những lợi thế tuyệt vời của việc phát triển ứng dụng trên nền tảng Android là cơ hội lớn để tiếp cận người dùng dưới nhiều hình thức, chẳng hạn như thiết bị đeo, thiết bị có thể gập lại, máy tính bảng, máy tính và thậm chí cả TV. Khi sử dụng một ứng dụng, người dùng của bạn có thể muốn dùng cùng một ứng dụng đó trên các thiết bị có màn hình lớn để tận dụng không gian tăng thêm. Người dùng Android ngày càng sử dụng ứng dụng trên nhiều thiết bị có kích thước màn hình khác nhau và mong đợi trải nghiệm người dùng chất lượng cao trên tất cả các thiết bị.
Tới nay, bạn đã học được cách tạo ứng dụng chủ yếu cho thiết bị di động. Trong lớp học lập trình này, bạn sẽ tìm hiểu cách chuyển đổi ứng dụng để thích ứng với các kích thước màn hình khác. Bạn sẽ sử dụng các mẫu bố cục điều hướng thích ứng đẹp mắt và dùng được cho cả thiết bị di động lẫn thiết bị có màn hình lớn, chẳng hạn như thiết bị có thể gập lại, máy tính bảng và máy tính.
Điều kiện tiên quyết
- Làm quen với lập trình Kotlin, bao gồm các lớp, hàm và điều kiện
- Làm quen với cách sử dụng lớp
ViewModel
- Làm quen với cách tạo hàm
Composables
- Trải nghiệm bố cục xây dựng bằng Jetpack Compose
- Trải nghiệm chạy các ứng dụng trên một thiết bị hoặc trình mô phỏng
Kiến thức bạn sẽ học được
- Cách tạo điều hướng giữa các màn hình mà không cần Biểu đồ điều hướng cho các ứng dụng đơn giản
- Cách dùng Jetpack Compose để tạo bố cục điều hướng thích ứng
- Cách tạo trình xử lý quay lại tuỳ chỉnh
Sản phẩm bạn sẽ tạo ra
- Bạn sẽ triển khai tính năng điều hướng động trong ứng dụng Reply (Trả lời) hiện có để làm cho bố cục của ứng dụng thích ứng với mọi kích thước màn hình
Sản phẩm hoàn thiện sẽ giống như hình dưới đây:
Những gì bạn cần
- Một máy tính có quyền truy cập Internet, trình duyệt web và Android Studio
- Quyền truy cập vào GitHub
2. Tổng quan về ứng dụng
Giới thiệu về ứng dụng Reply (Trả lời)
Reply là một ứng dụng nhiều màn hình giống với một chương trình email khách (ứng dụng khách dùng để gởi/quản lý email).
Ứng dụng chứa 4 danh mục. Những danh mục này sẽ hiển thị theo các thẻ khác nhau, cụ thể là: hộp thư đến, thư đã gửi, thư nháp và thư rác.
Tải mã khởi động xuống
Trong Android Studio, hãy mở thư mục basic-android-kotlin-compose-training-reply-app
.
- Chuyển đến trang kho lưu trữ GitHub được cung cấp cho dự án.
- Xác minh rằng tên chi nhánh khớp với tên chi nhánh được chỉ định trong lớp học lập trình. Ví dụ: trong ảnh chụp màn hình sau đây, tên nhánh là main (chính).
- Trên trang GitHub cho dự án này, hãy nhấp vào nút Code (Mã nguồn), một cửa sổ bật lên sẽ hiện ra.
- Trong cửa sổ bật lên, nhấp vào nút Download ZIP (Tải tệp ZIP xuống) để lưu dự án vào máy tính. Chờ quá trình tải xuống hoàn tất.
- Xác định vị trí của tệp trên máy tính (có thể trong thư mục Tải xuống (Tệp đã tải xuống)).
- Nhấp đúp vào tệp ZIP để giải nén. Thao tác này sẽ tạo một thư mục mới chứa các tệp dự án.
Mở dự án trong Android Studio
- Khởi động Android Studio.
- Trong cửa sổ Welcome to Android Studio (Chào mừng bạn đến với Android Studio), hãy nhấp vào Open (Mở).
Lưu ý: Nếu Android Studio đã mở sẵn thì hãy chuyển sang chọn tuỳ chọn File (Tệp) > Open (Mở) trên trình đơn.
- Trong trình duyệt tệp, hãy chuyển đến vị trí của thư mục dự án chưa giải nén (có thể nằm trong thư mục Downloads (Tệp đã tải xuống)).
- Nhấp đúp vào thư mục dự án đó.
- Chờ Android Studio mở dự án.
- Nhấp vào nút Run (Chạy) để tạo và chạy ứng dụng. Đảm bảo rằng ứng dụng được xây dựng như dự kiến.
3. Hướng dẫn từng bước về mã khởi động
Các thư mục quan trọng trong ứng dụng Reply (Trả lời)
Lớp dữ liệu và giao diện người dùng của dự án ứng dụng Reply (Trả lời) được phân tách thành các thư mục khác nhau. ReplyViewModel
, ReplyUiState
và các thành phần kết hợp khác nằm trong thư mục ui
. Các lớp data
và enum
xác định lớp dữ liệu và lớp trình cung cấp dữ liệu nằm trong thư mục data
.
Khởi tạo dữ liệu trong ứng dụng Reply (Trả lời)
Ứng dụng Reply (Trả lời) được khởi tạo bằng dữ liệu thông qua phương thức initilizeUIState()
trong ReplyViewModel
, phương thức này được thực thi trong hàm init
.
ReplyViewModel.kt
...
init {
initializeUIState()
}
private fun initializeUIState() {
var mailboxes: Map<MailboxType, List<Email>> =
LocalEmailsDataProvider.allEmails.groupBy { it.mailbox }
_uiState.value =
ReplyUiState(
mailboxes = mailboxes,
currentSelectedEmail = mailboxes[MailboxType.Inbox]?.get(0)
?: LocalEmailsDataProvider.defaultEmail
)
}
...
Thành phần kết hợp cấp màn hình
Cũng giống như các ứng dụng khác, ứng dụng Reply (Trả lời) sử dụng thành phần kết hợp ReplyApp
làm thành phần kết hợp chính nơi bạn khai báo viewModel
và uiState
. Nhiều hàm viewModel
cũng được truyền dưới dạng đối số lambda cho thành phần kết hợp (composable) ReplyHomeScreen
.
ReplyApp.kt
...
@Composable
fun ReplyApp(modifier: Modifier = Modifier) {
val viewModel: ReplyViewModel = viewModel()
val replyUiState = viewModel.uiState.collectAsState().value
ReplyHomeScreen(
replyUiState = replyUiState,
onTabPressed = { mailboxType: MailboxType ->
viewModel.updateCurrentMailbox(mailboxType = mailboxType)
viewModel.resetHomeScreenStates()
},
onEmailCardPressed = { email: Email ->
viewModel.updateDetailsScreenStates(
email = email
)
},
onDetailScreenBackPressed = {
viewModel.resetHomeScreenStates()
},
modifier = modifier
)
}
Các thành phần kết hợp khác
ReplyHomeScreen.kt
: chứa các thành phần kết hợp màn hình dành cho màn hình chính, bao gồm cả phần tử điều hướng.ReplyHomeContent.kt
: chứa các thành phần kết hợp xác định chi tiết hơn thành phần kết hợp trên màn hình chính.ReplyDetailsScreen.kt
: chứa các thành phần kết hợp màn hình và thành phần kết hợp nhỏ hơn cho màn hình chi tiết.
Vui lòng xem kỹ từng tệp để hiểu rõ hơn về các thành phần kết hợp trước khi chuyển sang phần tiếp theo của lớp học lập trình.
4. Thay đổi màn hình mà không cần biểu đồ điều hướng
Trong lộ trình trước đó, bạn đã học cách sử dụng lớp NavHostController
để điều hướng từ màn hình này sang màn hình khác. Với Compose, bạn cũng có thể thay đổi màn hình bằng những câu lệnh có điều kiện đơn giản nhờ sử dụng trạng thái có thể thay đổi trong thời gian chạy. Điều này đặc biệt hữu ích trong các ứng dụng nhỏ như Reply (Trả lời), nơi bạn chỉ muốn chuyển đổi giữa 2 màn hình.
Thay đổi màn hình bằng cách thay đổi trạng thái
Trong Compose, màn hình được kết hợp lại khi có thay đổi về trạng thái. Bạn có thể thay đổi màn hình bằng cách sử dụng các điều kiện đơn giản để phản hồi các thay đổi về trạng thái.
Bạn sẽ sử dụng các điều kiện để hiển thị nội dung trên màn hình chính khi người dùng đang ở màn hình chính và hiển thị nội dung trên màn hình chi tiết khi người dùng không ở màn hình chính.
Sửa đổi ứng dụng Reply (Trả lời) để cho phép màn hình thay đổi khi trạng thái thay đổi bằng cách hoàn thành các bước sau:
- Mở mã khởi động trong Android Studio.
- Ở thành phần kết hợp
ReplyHomeScreen
trongReplyHomeScreen.kt
, hãy gói thành phần kết hợpReplyAppContent
bằng câu lệnhif
khi thuộc tínhisShowingHomepage
của đối tượngreplyUiState
làtrue
.
ReplyHomeScreen.kt
@Composable
fun ReplyHomeScreen(
replyUiState: ReplyUiState,
onTabPressed: (MailboxType) -> Unit = {},
onEmailCardPressed: (Int) -> Unit = {},
onDetailScreenBackPressed: () -> Unit = {},
modifier: Modifier = Modifier
) {
...
if (replyUiState.isShowingHomepage) {
ReplyAppContent(
replyUiState = replyUiState,
onTabPressed = onTabPressed,
onEmailCardPressed = onEmailCardPressed,
navigationItemContentList = navigationItemContentList,
modifier = modifier
)
}
}
Bây giờ, bạn phải tính đến trường hợp người dùng không ở màn hình chính thông qua việc hiển thị màn hình chi tiết.
- Thêm một nhánh
else
có thành phần kết hợpReplyDetailsScreen
trong phần nội dung của nhánh đó. ThêmreplyUIState
,onDetailScreenBackPressed
vàmodifier
làm đối số cho thành phần kết hợpReplyDetailsScreen
.
ReplyHomeScreen.kt
@Composable
fun ReplyHomeScreen(
replyUiState: ReplyUiState,
onTabPressed: (MailboxType) -> Unit = {},
onEmailCardPressed: (Int) -> Unit = {},
onDetailScreenBackPressed: () -> Unit = {},
modifier: Modifier = Modifier
) {
...
if (replyUiState.isShowingHomepage) {
ReplyAppContent(
replyUiState = replyUiState,
onTabPressed = onTabPressed,
onEmailCardPressed = onEmailCardPressed,
navigationItemContentList = navigationItemContentList,
modifier = modifier
)
} else {
ReplyDetailsScreen(
replyUiState = replyUiState,
onBackPressed = onDetailScreenBackPressed,
modifier = modifier
)
}
}
replyUiState
là một đối tượng trạng thái. Như vậy, khi có thay đổi về thuộc tính isShowingHomepage
của đối tượng replyUiState
, thành phần kết hợp ReplyHomeScreen
sẽ được kết hợp lại và câu lệnh if/else
sẽ được đánh giá lại trong thời gian chạy. Phương pháp này hỗ trợ điều hướng giữa các màn hình mà không cần sử dụng lớp NavHostController
.
Tạo trình xử lý quay lại tuỳ chỉnh
Một lợi thế của việc sử dụng thành phần kết hợp NavHost
để chuyển đổi giữa các màn hình là hướng của màn hình trước được lưu trong ngăn xếp lui. Những màn hình đã lưu này cho phép nút quay lại của hệ thống dễ dàng chuyển về màn hình trước đó khi được gọi. Vì ứng dụng Reply (Trả lời) không sử dụng NavHost
nên bạn phải thêm mã này để xử lý nút quay lại theo cách thủ công. Bạn sẽ thực hiện việc này ở bước tiếp theo.
Hoàn thành các bước sau để tạo một trình xử lý quay lại tuỳ chỉnh trong ứng dụng Reply (Trả lời):
- Trên dòng đầu tiên của thành phần kết hợp
ReplyDetailsScreen
, hãy thêm một thành phần kết hợpBackHandler
. - Gọi hàm
onBackPressed()
trong phần nội dung của thành phần kết hợpBackHandler
.
ReplyDetailsScreen.kt
...
import androidx.activity.compose.BackHandler
...
@Composable
fun ReplyDetailsScreen(
replyUiState: ReplyUiState,
modifier: Modifier = Modifier,
onBackPressed: () -> Unit = {},
) {
BackHandler {
onBackPressed()
}
...
5. Chạy ứng dụng trên các thiết bị có màn hình lớn
Kiểm tra ứng dụng bằng trình mô phỏng có thể thay đổi kích thước
Để tạo ra các ứng dụng hữu ích, nhà phát triển cần hiểu được trải nghiệm của người dùng dưới nhiều dạng thức. Do đó, bạn phải kiểm thử ứng dụng trên nhiều hệ số hình dạng ngay từ đầu quá trình phát triển.
Bạn có thể sử dụng nhiều trình mô phỏng ở nhiều kích thước màn hình để đạt được mục tiêu này. Tuy nhiên, việc này có thể rườm rà, nhất là khi bạn đang xây dựng cho nhiều kích thước màn hình cùng lúc. Bạn có thể cũng cần kiểm thử cách một ứng dụng đang phản hồi các thay đổi về kích thước màn hình, chẳng hạn như các thay đổi về hướng, về kích thước cửa sổ trên máy tính và về trạng thái gập trên thiết bị có thể gập lại.
Android Studio sẽ giúp bạn kiểm thử các trường hợp này bằng việc giới thiệu về trình mô phỏng có thể thay đổi kích thước.
Hoàn thành các bước sau để thiết lập trình mô phỏng có thể thay đổi kích thước:
- Đảm bảo là bạn đang chạy Android Studio Chipmunk | 2021.2.1 trở lên.
- Trong Android Studio, hãy chọn Tools (Công cụ) > Device Manager (Trình quản lý thiết bị).
- Trong Trình quản lý thiết bị, hãy nhấp vào mục Tạo thiết bị.
- Chọn danh mục Điện thoại và thiết bị Có thể thay đổi kích thước (Thử nghiệm).
- Nhấp vào Next (Tiếp theo).
- Chọn API cấp 33.
- Nhấp vào Next (Tiếp theo).
- Đặt tên cho Thiết bị Android ảo mới.
- Nhấp vào Finish (Hoàn tất).
Chạy ứng dụng trên trình mô phỏng màn hình lớn
Bạn hiện đã thiết lập trình mô phỏng có thể đổi kích thước, hãy xem ứng dụng trông như thế nào trên màn hình lớn.
- Chạy ứng dụng trên trình mô phỏng có thể đổi kích thước.
- Chọn Máy tính bảng cho chế độ hiển thị.
- Kiểm tra ứng dụng ở chế độ Máy tính bảng khi nằm ngang.
Lưu ý rằng màn hình máy tính bảng được kéo dài theo chiều ngang. Mặc dù hướng này hoạt động đúng chức năng, nhưng nó có thể không phải là hướng sử dụng tốt nhất trong không gian màn hình lớn. Chúng ta sẽ giải quyết vấn đề đó ở bước tiếp theo.
Thiết kế cho màn hình lớn
Cảm nhận đầu tiên của bạn khi xem ứng dụng này trên máy tính bảng là ứng dụng được thiết kế không bắt mắt và kém hấp dẫn. Bạn đã đúng: bố cục này không được thiết kế để sử dụng cho màn hình lớn.
Khi thiết kế bố cục cho màn hình lớn (chẳng hạn như máy tính bảng và thiết bị có thể gập lại), bạn phải cân nhắc đến sự thoải mái và hiệu quả cho người dùng cũng như khoảng cách giữa các ngón tay của người dùng với màn hình. Với thiết bị di động, ngón tay của người dùng có thể dễ dàng di chuyển trên phần lớn màn hình; vị trí của các phần tử tương tác (chẳng hạn như nút và phần tử điều hướng) đều không quan trọng bằng. Tuy nhiên, đối với màn hình lớn, việc thành phần tương tác quan trọng nằm ở giữa màn hình có thể khiến người dùng khó tiếp cận với các thành phần này.
Như bạn thấy trong ứng dụng Reply (Trả lời), việc thiết kế màn hình lớn không chỉ đơn giản là kéo giãn hoặc phóng to các thành phần trên giao diện người dùng để vừa với màn hình. Đây là cơ hội để bạn sử dụng không gian tăng thêm nhằm tạo ra trải nghiệm khác biệt cho người dùng. Ví dụ: bạn có thể thêm một bố cục khác trên cùng một màn hình để tránh việc phải chuyển đến màn hình khác hoặc làm được nhiều việc cùng lúc.
Thiết kế này có thể làm tăng hiệu suất của người dùng và thúc đẩy họ tương tác nhiều hơn. Nhưng trước khi triển khai thiết kế này, trước tiên, bạn phải tìm hiểu cách tạo các bố cục tuỳ theo kích thước màn hình.
6. Tạo bố cục thích ứng với các kích thước màn hình khác nhau
Điểm ngắt là gì?
Bạn có thể thắc mắc về cách hiển thị nhiều bố cục cho cùng một ứng dụng. Câu trả lời ngắn gọn là sử dụng nhiều điều kiện ở các trạng thái khác nhau, giống như cách bạn đã làm ở đầu lớp học lập trình này.
Để tạo một ứng dụng thích ứng, bạn cần có bố cục thay đổi dựa trên kích thước màn hình. Điểm đo lường mà bố cục thay đổi được gọi là điểm ngắt. Material Design đã tạo một phạm vi điểm ngắt mở rộng trên hầu hết các màn hình Android.
Ví dụ: bảng phạm vi điểm ngắt này cho biết rằng nếu ứng dụng của bạn đang chạy trên một thiết bị có kích thước màn hình nhỏ hơn 600 dp, thì bạn nên hiển thị bố cục dành cho thiết bị di động.
Sử dụng các lớp kích thước cửa sổ
API WindowSizeClass
được giới thiệu cho Compose giúp việc triển khai các điểm ngắt của Material Design trở nên đơn giản hơn.
Các lớp (class) kích thước cửa sổ giới thiệu 3 danh mục kích thước: Nhỏ gọn, Trung bình và Mở rộng, cho cả chiều rộng và chiều cao.
Hoàn thành các bước sau để triển khai API WindowSizeClass
trong ứng dụng Reply (Trả lời):
- Thêm phần phụ thuộc
material3-window-size-class
vào tệpbuild.gradle
của mô-đun.
build.gradle
...
dependencies {
...
"androidx.compose.material3:material3-window-size-class:$material3_version"
...
- Nhấp vào Đồng bộ hoá ngay để đồng bộ hoá gradle sau khi thêm phần phụ thuộc.
Với tệp build.grade
đã cập nhật, bạn hiện có thể tạo một biến để lưu trữ kích thước của cửa sổ ứng dụng tại một thời điểm bất kỳ.
- Ở hàm
onCreate()
trong tệpMainActivity.kt
, hãy chỉ định phương thứccalculateWindowSizeClass
có ngữ cảnhthis
được truyền trong tham số cho một biến có tênwindowSize
. - Nhập gói
calculateWindowSizeClass
phù hợp.
MainActivity.kt
...
import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass
...
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
ReplyTheme {
val windowSize = calculateWindowSizeClass(this)
ReplyApp()
...
- Hãy chú ý phần gạch chân màu đỏ dưới cú pháp
calculateWindowSizeClass
cho thấy biểu tượng bóng đèn màu đỏ. Nhấp vào bóng đèn màu đỏ ở bên trái của biếnwindowSize
rồi chọn Opt in for 'ExperimentalMaterial3WindowSizeClassApi' on 'onCreate' (Chọn sử dụng cho "ExperimentalMaterial3WindowSizeClassApi" trên "onCreate") để tạo chú thích phía trên phương thứconCreate()
.
Bạn có thể sử dụng biến WindowWidthSizeClass
trong MainActivity.kt
để xác định bố cục sẽ hiển thị trong các thành phần kết hợp khác nhau. Hãy chuẩn bị thành phần kết hợp ReplyApp
để nhận giá trị này.
- Trong tệp
ReplyApp.kt
, hãy chỉnh sửa thành phần kết hợpReplyApp
để chấp nhậnWindowWidthSizeClass
dưới dạng tham số và nhập gói phù hợp.
ReplyApp.kt
...
import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
...
@Composable
fun ReplyApp(
windowSize: WindowWidthSizeClass,
modifier: Modifier = Modifier
) {
...
- Chuyển biến
windowSize
vào thành phầnReplyApp
trong phương thứconCreate()
của tệpMainActivity.kt
.
MainActivity.kt
...
setContent {
ReplyTheme {
val windowSize = calculateWindowSizeClass(this)
ReplyApp(
windowSize = windowSize.widthSizeClass
)
...
Bạn cũng cần cập nhật bản xem trước của ứng dụng cho tham số windowSize
.
- Truyền
WindowWidthSizeClass.Compact
dưới dạng tham sốwindowSize
tới thành phần kết hợpReplyApp
để xem trước thành phần và nhập gói thích hợp.
MainActivity.kt
...
import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
...
@Preview(showBackground = true)
@Composable
fun ReplyAppPreview() {
ReplyTheme {
ReplyApp(
windowSize = WindowWidthSizeClass.Compact,
)
}
}
- Để thay đổi bố cục ứng dụng dựa trên kích thước màn hình, hãy thêm câu lệnh
when
vào thành phần kết hợpReplyApp
dựa trên giá trịWindowWidthSizeClass
.
ReplyApp.kt
...
@Composable
fun ReplyApp(
windowSize: WindowWidthSizeClass,
modifier: Modifier = Modifier
) {
val viewModel: ReplyViewModel = viewModel()
val replyUiState = viewModel.uiState.collectAsState().value
when (windowSize) {
WindowWidthSizeClass.Compact -> {
}
WindowWidthSizeClass.Medium -> {
}
WindowWidthSizeClass.Expanded -> {
}
else -> {
}
}
...
Đến đây, bạn đã thiết lập một nền tảng để sử dụng các giá trị WindowSizeClass
nhằm thay đổi bố cục trong ứng dụng. Bước tiếp theo là xác định cách bạn muốn ứng dụng hiển thị trên nhiều kích thước màn hình.
7. Triển khai bố cục điều hướng thích ứng
Triển khai điều hướng giao diện người dùng thích ứng
Thanh điều hướng dưới cùng hiện đang được sử dụng cho tất cả các kích thước màn hình.
Như đã thảo luận trước đó, phần tử điều hướng này không lý tưởng mấy vì người dùng có thể khó tiếp cận các phần tử điều hướng thiết yếu này trên màn hình lớn hơn. May mắn là đã có các mẫu đề xuất liên quan đến nhiều phần tử điều hướng cho các lớp kích thước cửa sổ khác nhau trong phần điều hướng cho giao diện người dùng thích ứng. Đối với ứng dụng Reply (Trả lời), bạn có thể triển khai các phần tử sau:
Dải điều hướng là một thành phần điều hướng khác theo thiết kế Material Design, cho phép các tuỳ chọn điều hướng nhỏ gọn dành cho các đích đến chính tiếp cận được từ cạnh ứng dụng.
Tương tự như vậy, một ngăn điều hướng cố định/vĩnh viễn được tạo bằng thiết kế Material Design là một tuỳ chọn khác để cung cấp khả năng truy cập mang tính nhỏ gọn (ergonomic) cho màn hình lớn hơn.
Triển khai ngăn điều hướng
Để tạo ngăn điều hướng cho các màn hình mở rộng, bạn có thể sử dụng tham số navigationType
. Hãy làm theo các bước sau đây:
- Để thể hiện các loại phần tử điều hướng khác nhau, hãy tạo một tệp
WindowStateUtils.kt
mới trong một góiutils
mới thuộc thư mụcui
. - Thêm một lớp
Enum
để đại diện cho các loại thành phần điều hướng khác nhau.
WindowStateUtils.kt
package com.example.reply.ui.utils
enum class ReplyNavigationType {
BOTTOM_NAVIGATION, NAVIGATION_RAIL, PERMANENT_NAVIGATION_DRAWER
}
Để triển khai thành công ngăn điều hướng, bạn cần xác định loại điều hướng dựa trên kích thước cửa sổ của ứng dụng.
- Trong thành phần kết hợp
ReplyApp
, hãy tạo một biếnnavigationType
và gán giá trịReplyNavigationType
thích hợp cho biến đó theo kích thước màn hình trong câu lệnhwhen
.
ReplyApp.kt
...
import com.example.reply.ui.utils.ReplyNavigationType
...
when (windowSize) {
WindowWidthSizeClass.Compact -> {
navigationType = ReplyNavigationType.BOTTOM_NAVIGATION
}
WindowWidthSizeClass.Medium -> {
navigationType = ReplyNavigationType.NAVIGATION_RAIL
}
WindowWidthSizeClass.Expanded -> {
navigationType = ReplyNavigationType.PERMANENT_NAVIGATION_DRAWER
}
else -> {
navigationType = ReplyNavigationType.BOTTOM_NAVIGATION
}
}
...
Bạn có thể dùng giá trị navigationType
trong thành phần kết hợp ReplyHomeScreen
. Bạn có thể chuẩn bị cho việc này bằng cách đặt giá trị này làm tham số cho thành phần kết hợp.
- Trong thành phần kết hợp
ReplyHomeScreen
, hãy thêmnavigationType
làm tham số.
ReplyHomeScreen.kt
...
@Composable
fun ReplyHomeScreen(
navigationType: ReplyNavigationType,
replyUiState: ReplyUiState,
onTabPressed: (MailboxType) -> Unit = {},
onEmailCardPressed: (Email) -> Unit = {},
onDetailScreenBackPressed: () -> Unit = {},
modifier: Modifier = Modifier
)
...
- Truyền
navigationType
vào thành phần kết hợpReplyHomeScreen
.
ReplyApp.kt
...
ReplyHomeScreen(
navigationType = navigationType,
replyUiState = replyUiState,
onTabPressed = { mailboxType: MailboxType ->
viewModel.updateCurrentMailbox(mailboxType = mailboxType)
viewModel.resetHomeScreenStates()
},
onEmailCardPressed = { email: Email ->
viewModel.updateDetailsScreenStates(
email = email
)
},
onDetailScreenBackPressed = {
viewModel.resetHomeScreenStates()
},
modifier = modifier
)
...
Tiếp theo, bạn có thể tạo một nhánh để hiển thị nội dung ứng dụng bằng ngăn điều hướng khi người dùng mở ứng dụng trên màn hình mở rộng và hiển thị màn hình chính.
- Trong phần thân (nội dung) của thành phần kết hợp
ReplyHomeScreen
, hãy thêm câu lệnhif
cho điều kiệnnavigationType == ReplyNavigationType.PERMANENT_NAVIGATION_DRAWER && replyUiState.isShowingHomepage
.
ReplyHomeScreen.kt
import androidx.compose.material3.PermanentNavigationDrawer
...
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ReplyHomeScreen(
navigationType: ReplyNavigationType,
replyUiState: ReplyUiState,
onTabPressed: (MailboxType) -> Unit = {},
onEmailCardPressed: (Email) -> Unit = {},
onDetailScreenBackPressed: () -> Unit = {},
modifier: Modifier = Modifier
) {
...
if (navigationType == ReplyNavigationType.PERMANENT_NAVIGATION_DRAWER
&& replyUiState.isShowingHomepage
) {
}
if (replyUiState.isShowingHomepage) {
ReplyAppContent(
replyUiState = replyUiState,
...
- Để tạo ngăn vĩnh viễn, hãy tạo thành phần kết hợp
PermanentNavigationDrawer
trong nội dung của câu lệnh if và thêm thành phần kết hợpNavigationDrawerContent
làm dữ liệu đầu vào cho tham sốdrawerContent
. - Thêm thành phần kết hợp
ReplyAppContent
làm đối số lambda cuối cùng củaPermanentNavigationDrawer
.
ReplyHomeScreen.kt
...
if (navigationType == ReplyNavigationType.PERMANENT_NAVIGATION_DRAWER
&& replyUiState.isShowingHomepage
) {
PermanentNavigationDrawer(
drawerContent = {
NavigationDrawerContent(
selectedDestination = replyUiState.currentMailbox,
onTabPressed = onTabPressed,
navigationItemContentList = navigationItemContentList
)
}
) {
ReplyAppContent(
replyUiState = replyUiState,
onTabPressed = onTabPressed,
onEmailCardPressed = onEmailCardPressed,
navigationItemContentList = navigationItemContentList,
modifier = modifier
)
}
}
...
- Thêm một nhánh
else
sử dụng phần thân (nội dung) của thành phần kết hợp trước để duy trì việc phân nhánh trước đó cho các màn hình không được mở rộng.
ReplyHomeScreen.kt
...
if (navigationType == ReplyNavigationType.PERMANENT_NAVIGATION_DRAWER
&& replyUiState.isShowingHomepage
) {
PermanentNavigationDrawer(
drawerContent = {
NavigationDrawerContent(
selectedDestination = replyUiState.currentMailbox,
onTabPressed = onTabPressed,
navigationItemContentList = navigationItemContentList
)
}
) {
ReplyAppContent(
replyUiState = replyUiState,
onTabPressed = onTabPressed,
onEmailCardPressed = onEmailCardPressed,
navigationItemContentList = navigationItemContentList,
modifier = modifier
)
}
} else {
if (replyUiState.isShowingHomepage) {
ReplyAppContent(
replyUiState = replyUiState,
onTabPressed = onTabPressed,
onEmailCardPressed = onEmailCardPressed,
navigationItemContentList = navigationItemContentList,
modifier = modifier
)
} else {
ReplyDetailsScreen(
replyUiState = replyUiState,
onBackPressed = onDetailScreenBackPressed,
modifier = modifier
)
}
}
}
...
- Thêm chú thích thử nghiệm vào thành phần kết hợp
ReplyHomeScreen
. Bạn cần làm điều này vì APIPermanentNavigationDrawer
vẫn đang trong quá trình thử nghiệm.
ReplyHomeScreen.kt
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ReplyHomeScreen(
navigationType: ReplyNavigationType,
replyUiState: ReplyUiState,
onTabPressed: (MailboxType) -> Unit = {},
onEmailCardPressed: (Email) -> Unit = {},
onDetailScreenBackPressed: () -> Unit = {},
modifier: Modifier = Modifier
) {
...
- Chạy ứng dụng ở chế độ Máy tính bảng. Bạn sẽ thấy màn hình sau:
Triển khai dải điều hướng
Tương tự như cách triển khai ngăn điều hướng, bạn cần dùng tham số navigationType
để chuyển đổi giữa các thành phần điều hướng.
Trước tiên, hãy thêm dải điều hướng cho các màn hình với kích cỡ trung bình.
- Bắt đầu với việc chuẩn bị cho thành phần kết hợp
ReplyAppContent
bằng cách thêmnavigationType
làm tham số.
ReplyHomeScreen.kt
...
@Composable
private fun ReplyAppContent(
navigationType: ReplyNavigationType,
replyUiState: ReplyUiState,
onTabPressed: ((MailboxType) -> Unit) = {},
onEmailCardPressed: (Email) -> Unit = {},
navigationItemContentList: List<NavigationItemContent>,
modifier: Modifier = Modifier
) {
...
- Chuyển giá trị
navigationType
vào cả hai thành phần kết hợpReplyAppContent
.
ReplyHomeScreen.kt
...
ReplyAppContent(
navigationType = navigationType,
replyUiState = replyUiState,
onTabPressed = onTabPressed,
onEmailCardPressed = onEmailCardPressed,
navigationItemContentList = navigationItemContentList,
modifier = modifier
)
}
} else {
if (replyUiState.isShowingHomepage) {
ReplyAppContent(
navigationType = navigationType,
replyUiState = replyUiState,
onTabPressed = onTabPressed,
onEmailCardPressed = onEmailCardPressed,
navigationItemContentList = navigationItemContentList,
modifier = modifier
)
...
Tiếp theo, hãy thêm tính năng phân nhánh để cho phép ứng dụng hiển thị dải điều hướng trong một số trường hợp.
- Trong dòng đầu tiên của nội dung thành phần kết hợp
ReplyAppContent
, hãy gói thành phần kết hợpReplyNavigationRail
xung quanh thành phần kết hợpAnimatedVisibility
và đặt tham sốvisibility
thànhtrue
nếu giá trịReplyNavigationType
làNavigationRail
.
ReplyHomeScreen.kt
...
@Composable
private fun ReplyAppContent(
navigationType: ReplyNavigationType,
replyUiState: ReplyUiState,
onTabPressed: ((MailboxType) -> Unit) = {},
onEmailCardPressed: (Email) -> Unit = {},
navigationItemContentList: List<NavigationItemContent>,
modifier: Modifier = Modifier
) {
AnimatedVisibility(visible = navigationType == ReplyNavigationType.NAVIGATION_RAIL) {
ReplyNavigationRail(
currentTab = replyUiState.currentMailbox,
onTabPressed = onTabPressed,
navigationItemContentList = navigationItemContentList
)
}
Column(
modifier = Modifier
.fillMaxSize() .background(MaterialTheme.colorScheme.inverseOnSurface)
) {
ReplyListOnlyContent(
replyUiState = replyUiState,
onEmailCardPressed = onEmailCardPressed,
modifier = Modifier.weight(1f)
)
ReplyBottomNavigationBar(
currentTab = replyUiState.currentMailbox,
onTabPressed = onTabPressed,
navigationItemContentList = navigationItemContentList
)
}
}
...
- Để căn chỉnh chính xác các thành phần kết hợp, hãy gói cả thành phần kết hợp
AnimatedVisibility
vàColumn
có ở nội dungReplyAppContent
trong thành phần kết hợpRow
.
ReplyHomeScreen.kt
...
@Composable
private fun ReplyAppContent(
navigationType: ReplyNavigationType,
replyUiState: ReplyUiState,
onTabPressed: ((MailboxType) -> Unit) = {},
onEmailCardPressed: (Email) -> Unit = {},
navigationItemContentList: List<NavigationItemContent>,
modifier: Modifier = Modifier
) {
Row(modifier = modifier.fillMaxSize()) {
AnimatedVisibility(visible = navigationType == ReplyNavigationType.NAVIGATION_RAIL) {
ReplyNavigationRail(
currentTab = replyUiState.currentMailbox,
onTabPressed = onTabPressed,
navigationItemContentList = navigationItemContentList
)
}
Column(
modifier = Modifier
.fillMaxSize() .background(MaterialTheme.colorScheme.inverseOnSurface)
) {
ReplyListOnlyContent(
replyUiState = replyUiState,
onEmailCardPressed = onEmailCardPressed,
modifier = Modifier.weight(1f)
)
ReplyBottomNavigationBar(
currentTab = replyUiState.currentMailbox,
onTabPressed = onTabPressed,
navigationItemContentList = navigationItemContentList
)
}
}
}
...
Cuối cùng, hãy đảm bảo thanh điều hướng ở dưới cùng sẽ hiển thị trong một số trường hợp.
- Sau thành phần kết hợp
ReplyListOnlyContent
, hãy gói thành phần kết hợpReplyBottomNavigationBar
bằng một thành phần kết hợpAnimatedVisibility
. - Đặt tham số
visible
khi giá trịReplyNavigationType
làBOTTOM_NAVIGATION
.
ReplyHomeScreen.kt
...
ReplyListOnlyContent(
replyUiState = replyUiState,
onEmailCardPressed = onEmailCardPressed,
modifier = Modifier.weight(1f)
)
AnimatedVisibility(visible = navigationType == ReplyNavigationType.BOTTOM_NAVIGATION) {
ReplyBottomNavigationBar(
currentTab = replyUiState.currentMailbox,
onTabPressed = onTabPressed,
navigationItemContentList = navigationItemContentList
)
}
...
- Chạy ứng dụng ở chế độ Mở ra và gập lại. Bạn sẽ thấy màn hình sau:
8. Lấy mã giải pháp
Để tải xuống mã cho lớp học lập trình đã kết thúc, bạn có thể sử dụng lệnh git này:
$ git clone https://github.com/google-developer-training/basic-android-kotlin-compose-training-reply-app.git cd basic-android-kotlin-compose-training-reply-app git checkout nav-update
Ngoài ra, bạn có thể tải kho lưu trữ xuống dưới dạng tệp zip, giải nén và mở trong Android Studio.
Nếu bạn muốn xem mã giải pháp, hãy xem mã đó trên GitHub.
9. Kết luận
Xin chúc mừng! Bạn đã tiến gần hơn một bước để tạo ứng dụng Reply (Trả lời) thích ứng cho mọi kích thước màn hình bằng cách triển khai bố cục điều hướng thích ứng. Bạn đã nâng cao trải nghiệm người dùng bằng cách sử dụng nhiều hệ số hình dạng trong Android. Trong lớp học lập trình tiếp theo, bạn sẽ cải thiện hơn nữa kỹ năng làm việc với các ứng dụng thích ứng bằng cách triển khai bố cục, thử nghiệm và xem trước nội dung thích ứng.
Đừng quên chia sẻ công việc của bạn trên mạng xã hội với #AndroidBasics!