1. Trước khi bắt đầu
Trong các lớp học lập trình trước, bạn đã tìm hiểu về vòng đời của các hoạt động và vấn đề liên quan đến vòng đời với các thay đổi về cấu hình. Khi cấu hình thay đổi, bạn có thể lưu dữ liệu của ứng dụng bằng nhiều cách, chẳng hạn như sử dụng rememberSaveable
hoặc lưu trạng thái của thực thể. Tuy nhiên, những tuỳ chọn này có thể gây ra sự cố. Trong hầu hết các trường hợp, bạn có thể sử dụng rememberSaveable
, nhưng điều đó có nghĩa là giữ logic trong hoặc gần các thành phần kết hợp. Khi các ứng dụng phát triển, bạn nên di chuyển dữ liệu và logic khỏi các thành phần kết hợp. Trong lớp học lập trình này, bạn sẽ tìm hiểu về cách hiệu quả để thiết kế ứng dụng và lưu trữ dữ liệu ứng dụng trong quá trình thay đổi cấu hình bằng việc tận dụng thư viện Android Jetpack, ViewModel
và các nguyên tắc về cấu trúc ứng dụng Android.
Thư viện Android Jetpack là tập hợp các thư viện giúp bạn phát triển những ứng dụng Android tuyệt vời một cách dễ dàng hơn. Các thư viện này giúp bạn thực hiện theo những phương pháp hay nhất, tránh phải viết mã nguyên mẫu và đơn giản hoá các nhiệm vụ phức tạp để bạn có thể tập trung vào mã mà bạn quan tâm, chẳng hạn như logic ứng dụng.
Cấu trúc ứng dụng là một bộ quy tắc thiết kế dành cho ứng dụng. Giống như bản thiết kế của một ngôi nhà, cấu trúc sẽ tạo nên kết cấu cho ứng dụng. Một cấu trúc ứng dụng tốt có thể giúp mã hoạt động hiệu quả, linh hoạt, có thể mở rộng, có thể kiểm thử và duy trì trong nhiều năm tới. Hướng dẫn về cấu trúc ứng dụng đưa ra nội dung đề xuất đối với cấu trúc ứng dụng và các phương pháp hay nhất nên áp dụng.
Trong lớp học lập trình này, bạn sẽ tìm hiểu cách sử dụng ViewModel
. Đây là một trong những thành phần cấu trúc của thư viện Android Jetpack có thể lưu trữ dữ liệu ứng dụng của bạn. Dữ liệu đã lưu trữ sẽ không bị mất nếu khung này huỷ rồi tạo lại hoạt động trong quá trình thay đổi cấu hình hoặc các sự kiện khác. Tuy nhiên, dữ liệu sẽ bị mất nếu hoạt động bị huỷ bỏ trong tình huống bị buộc tắt. ViewModel
chỉ lưu dữ liệu vào bộ nhớ đệm thông qua các hoạt động tái tạo nhanh.
Điều kiện tiên quyết
- Kiến thức về Kotlin, bao gồm các hàm, hàm lambda và các thành phần kết hợp không có trạng thái
- Kiến thức cơ bản về cách xây dựng bố cục trong Jetpack Compose
- Kiến thức cơ bản về Material Design.
Kiến thức bạn sẽ học được
- Giới thiệu về cấu trúc ứng dụng Android
- Cách sử dụng lớp
ViewModel
trong ứng dụng. - Cách sử dụng
ViewModel
để lưu giữ dữ liệu trên giao diện người dùng trong quá trình thay đổi cấu hình thiết bị.
Sản phẩm bạn sẽ tạo ra
- Ứng dụng trò chơi Unscramble (Xếp từ) mà người dùng có thể đoán các từ được xáo trộn.
Những gì bạn cần
- Phiên bản mới nhất của Android Studio
- Kết nối Internet để tải mã khởi đầu xuống.
2. Tổng quan về ứng dụng
Tổng quan về trò chơi
Ứng dụng Unscramble là trò chơi xáo trộn từ một người chơi. Ứng dụng sẽ hiển thị một từ được xáo trộn và người chơi phải đoán từ đó bằng tất cả các chữ cái hiện lên. Người chơi giành được điểm nếu từ chính xác. Nếu không, người chơi có thể thử nhiều lần. Ứng dụng cũng cho phép bạn chọn bỏ qua từ hiện tại. Ở góc trên cùng bên phải, ứng dụng cho thấy số từ bị xáo trộn đã chơi trong ván hiện tại. Có 10 từ bị xáo trộn trong mỗi ván.
Tải mã nguồn ban đầu
Để bắt đầu, hãy tải mã khởi đầu xuống:
Ngoài ra, bạn có thể sao chép kho lưu trữ GitHub cho mã:
$ git clone https://github.com/google-developer-training/basic-android-kotlin-compose-training-unscramble.git $ cd basic-android-kotlin-compose-training-unscramble $ git checkout starter
Bạn có thể duyệt tìm mã khởi đầu trong kho lưu trữ GitHub Unscramble
.
3. Tổng quan về ứng dụng khởi đầu
Để làm quen với mã khởi đầu, bạn hãy hoàn thành các bước sau:
- Mở dự án bằng mã khởi đầu trong Android Studio.
- Chạy ứng dụng trên thiết bị Android hoặc trên trình mô phỏng.
- Nhấn vào nút Submit (Gửi) và Skip (Bỏ qua) để kiểm thử ứng dụng.
Bạn sẽ nhận thấy có lỗi trong ứng dụng. Từ bị xáo trộn không hiển thị nhưng được cố định giá trị trong mã thành "scrambleun" và không có gì xảy ra khi bạn nhấn vào các nút.
Trong lớp học lập trình này, bạn sẽ triển khai chức năng của trò chơi bằng cách dùng cấu trúc ứng dụng Android.
Tìm hiểu mã khởi đầu
Mã khởi đầu có bố cục màn hình trò chơi được thiết kế sẵn cho bạn. Trong lộ trình này, bạn sẽ triển khai logic trò chơi. Bạn sẽ dùng các thành phần cấu trúc để triển khai cấu trúc ứng dụng được đề xuất và giải quyết những vấn đề nêu trên. Dưới đây là hướng dẫn từng bước ngắn gọn về một số tệp để bạn bắt đầu.
WordsData.kt
Tệp này chứa danh sách các từ được dùng trong trò chơi, hằng số cho số từ tối đa trong mỗi trò chơi và số điểm mà người chơi giành được cho mỗi từ chính xác.
package com.example.android.unscramble.data
const val MAX_NO_OF_WORDS = 10
const val SCORE_INCREASE = 20
// Set with all the words for the Game
val allWords: Set<String> =
setOf(
"animal",
"auto",
"anecdote",
"alphabet",
"all",
"awesome",
"arise",
"balloon",
"basket",
"bench",
// ...
"zoology",
"zone",
"zeal"
)
MainActivity.kt
Tệp này chủ yếu chứa mã tạo mẫu. Bạn sẽ hiển thị thành phần kết hợp GameScreen
trong khối setContent{}
.
GameScreen.kt
Mọi thành phần kết hợp giao diện người dùng đều được xác định trong tệp GameScreen.kt
. Các phần sau đây cung cấp hướng dẫn từng bước về một số hàm có khả năng kết hợp.
GameStatus
GameStatus
là một hàm có khả năng kết hợp cho thấy điểm số trò chơi ở cuối màn hình. Hàm có khả năng kết hợp chứa một thành phần kết hợp văn bản trong Card
. Hiện tại, điểm số được mã hoá cứng thành 0
.
// No need to copy, this is included in the starter code.
@Composable
fun GameStatus(score: Int, modifier: Modifier = Modifier) {
Card(
modifier = modifier
) {
Text(
text = stringResource(R.string.score, score),
style = typography.headlineMedium,
modifier = Modifier.padding(8.dp)
)
}
}
GameLayout
GameLayout
là một hàm có khả năng kết hợp, hiển thị chức năng chính của trò chơi, bao gồm từ bị xáo trộn, hướng dẫn chơi và một trường văn bản nhận các lượt đoán của người dùng.
Lưu ý rằng đoạn mã GameLayout
bên dưới chứa một cột bên trong một Card
có 3 phần tử con gồm văn bản từ bị xáo trộn, văn bản hướng dẫn và trường văn bản OutlinedTextField
cho người dùng nhập từ. Hiện tại, từ bị xáo trộn được mã hoá cứng thành scrambleun
. Trong phần sau của lớp học lập trình này, bạn sẽ triển khai chức năng để hiển thị một từ trong tệp WordsData.kt
.
// No need to copy, this is included in the starter code.
@Composable
fun GameLayout(modifier: Modifier = Modifier) {
val mediumPadding = dimensionResource(R.dimen.padding_medium)
Card(
modifier = modifier,
elevation = CardDefaults.cardElevation(defaultElevation = 5.dp)
) {
Column(
verticalArrangement = Arrangement.spacedBy(mediumPadding),
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.padding(mediumPadding)
) {
Text(
modifier = Modifier
.clip(shapes.medium)
.background(colorScheme.surfaceTint)
.padding(horizontal = 10.dp, vertical = 4.dp)
.align(alignment = Alignment.End),
text = stringResource(R.string.word_count, 0),
style = typography.titleMedium,
color = colorScheme.onPrimary
)
Text(
text = "scrambleun",
style = typography.displayMedium
)
Text(
text = stringResource(R.string.instructions),
textAlign = TextAlign.Center,
style = typography.titleMedium
)
OutlinedTextField(
value = "",
singleLine = true,
shape = shapes.large,
modifier = Modifier.fillMaxWidth(),
colors = TextFieldDefaults.textFieldColors(containerColor = colorScheme.surface),
onValueChange = { },
label = { Text(stringResource(R.string.enter_your_word)) },
isError = false,
keyboardOptions = KeyboardOptions.Default.copy(
imeAction = ImeAction.Done
),
keyboardActions = KeyboardActions(
onDone = { }
)
)
}
}
}
Thành phần kết hợp OutlinedTextField
tương tự như thành phần kết hợp TextField
từ các ứng dụng trong các lớp học lập trình trước.
Có hai loại trường văn bản:
- Các trường văn bản được tô màu nền
- Trường văn bản có đường viền
Các trường văn bản có đường viền ít nhấn mạnh vào hình ảnh hơn là các trường văn bản được tô màu nền. Khi xuất hiện ở những vị trí như biểu mẫu, nơi nhiều trường văn bản được đặt cùng nhau, việc giảm mức độ nhấn mạnh của các trường này sẽ giúp đơn giản hoá bố cục.
Trong mã khởi đầu, OutlinedTextField
không cập nhật khi người dùng nhập vào một dự đoán. Bạn sẽ cập nhật tính năng này trong lớp học lập trình.
GameScreen
Thành phần kết hợp GameScreen
chứa các hàm có khả năng kết hợp GameStatus
và GameLayout
, tiêu đề trò chơi, số từ và các thành phần kết hợp dành cho nút Gửi và Bỏ qua.
@Composable
fun GameScreen() {
val mediumPadding = dimensionResource(R.dimen.padding_medium)
Column(
modifier = Modifier
.verticalScroll(rememberScrollState())
.padding(mediumPadding),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = stringResource(R.string.app_name),
style = typography.titleLarge,
)
GameLayout(
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight()
.padding(mediumPadding)
)
Column(
modifier = Modifier
.fillMaxWidth()
.padding(mediumPadding),
verticalArrangement = Arrangement.spacedBy(mediumPadding),
horizontalAlignment = Alignment.CenterHorizontally
) {
Button(
modifier = Modifier.fillMaxWidth(),
onClick = { }
) {
Text(
text = stringResource(R.string.submit),
fontSize = 16.sp
)
}
OutlinedButton(
onClick = { },
modifier = Modifier.fillMaxWidth()
) {
Text(
text = stringResource(R.string.skip),
fontSize = 16.sp
)
}
}
GameStatus(score = 0, modifier = Modifier.padding(20.dp))
}
}
Sự kiện nhấp chuột vào nút sẽ không được triển khai trong mã khởi động. Bạn sẽ triển khai các sự kiện này trong lớp học lập trình.
FinalScoreDialog
Thành phần kết hợp FinalScoreDialog
sẽ hiển thị một hộp thoại (là một cửa sổ nhỏ để nhắc người dùng) với các lựa chọn Play Again (Chơi lại) hoặc Exit (Thoát) trò chơi. Trong phần sau của lớp học lập trình này, bạn sẽ triển khai logic để hiển thị hộp thoại này ở cuối trò chơi.
// No need to copy, this is included in the starter code.
@Composable
private fun FinalScoreDialog(
score: Int,
onPlayAgain: () -> Unit,
modifier: Modifier = Modifier
) {
val activity = (LocalContext.current as Activity)
AlertDialog(
onDismissRequest = {
// Dismiss the dialog when the user clicks outside the dialog or on the back
// button. If you want to disable that functionality, simply use an empty
// onDismissRequest.
},
title = { Text(text = stringResource(R.string.congratulations)) },
text = { Text(text = stringResource(R.string.you_scored, score)) },
modifier = modifier,
dismissButton = {
TextButton(
onClick = {
activity.finish()
}
) {
Text(text = stringResource(R.string.exit))
}
},
confirmButton = {
TextButton(onClick = onPlayAgain) {
Text(text = stringResource(R.string.play_again))
}
}
)
}
4. Tìm hiểu về cấu trúc ứng dụng
Cấu trúc ứng dụng cung cấp các nguyên tắc giúp bạn phân bổ trách nhiệm của ứng dụng giữa các lớp. Một cấu trúc ứng dụng được thiết kế hợp lý sẽ giúp bạn điều chỉnh quy mô ứng dụng và mở rộng ứng dụng với các tính năng bổ sung. Cấu trúc này cũng có thể đơn giản hoá việc cộng tác trong nhóm.
Nguyên tắc cấu trúc phổ biến nhất là tách biệt vấn đề và điều khiển giao diện người dùng bằng mô hình.
Tách biệt vấn đề
Nguyên tắc thiết kế tách biệt vấn đề nêu rõ rằng bạn phải chia ứng dụng thành các lớp hàm, mỗi lớp có những trách nhiệm riêng.
Điều khiển giao diện người dùng bằng mô hình
Giao diện người dùng điều khiển trong nguyên tắc mô hình cho biết rằng bạn nên điều khiển giao diện người dùng bằng mô hình, tốt hơn hết là một mô hình liên tục. Mô hình là thành phần chịu trách nhiệm xử lý dữ liệu cho ứng dụng. Mô hình độc lập với các phần tử trên giao diện người dùng và các thành phần ứng dụng trong ứng dụng của bạn, vậy nên vòng đời của ứng dụng và những vấn đề liên quan sẽ không ảnh hưởng tới mô hình.
Cấu trúc ứng dụng được đề xuất
Theo các nguyên tắc cấu trúc phổ biến đã đề cập trong phần trước, mỗi ứng dụng phải có ít nhất 2 lớp:
- Lớp giao diện người dùng: là một lớp hiển thị dữ liệu ứng dụng trên màn hình nhưng độc lập với dữ liệu.
- Lớp dữ liệu: là một lớp (layer) lưu trữ, truy xuất và hiển thị dữ liệu ứng dụng.
Bạn có thể thêm một lớp khác có tên là lớp miền để đơn giản hoá cũng như sử dụng lại các lượt tương tác giữa giao diện người dùng và các lớp dữ liệu. Lớp này là lớp không bắt buộc và nằm ngoài phạm vi của khoá học này.
Lớp giao diện người dùng
Vai trò của lớp giao diện người dùng hoặc tầng trình diễn là hiển thị dữ liệu ứng dụng trên màn hình. Bất cứ khi nào dữ liệu thay đổi do một hoạt động tương tác của người dùng, chẳng hạn như nhấn vào một nút, giao diện người dùng sẽ cập nhật để phản ánh các thay đổi đó.
Lớp giao diện người dùng gồm các thành phần sau:
- Các thành phần trên giao diện người dùng: kết xuất (hiển thị) dữ liệu trên màn hình. Bạn tạo các phần tử này bằng cách sử dụng Jetpack Compose.
- Chủ thể trạng thái: thành phần chứa dữ liệu, hiển thị thông tin dữ liệu này tới giao diện người dùng và xử lý logic của ứng dụng. Một ví dụ về chủ thể trạng thái có thể kể đến là ViewModel.
ViewModel
Thành phần ViewModel
giữ và hiển thị trạng thái mà giao diện người dùng sử dụng. Trạng thái giao diện người dùng là dữ liệu ứng dụng do ViewModel
chuyển đổi. ViewModel
cho phép ứng dụng của bạn tuân theo nguyên tắc cấu trúc điều khiển giao diện người dùng bằng mô hình.
ViewModel
lưu trữ dữ liệu liên quan đến ứng dụng không bị huỷ bỏ khi khung Android huỷ và tạo lại hoạt động. Không giống như thực thể hoạt động, các đối tượng ViewModel
sẽ không bị huỷ. Ứng dụng sẽ tự động giữ lại các đối tượng ViewModel
trong quá trình thay đổi cấu hình. Nhờ vậy, dữ liệu mà các đối tượng này lưu giữ sẽ có sẵn ngay sau khi quá trình kết hợp lại diễn ra.
Để triển khai ViewModel
trong ứng dụng, hãy mở rộng lớp ViewModel
(lấy từ thư viện thành phần cấu trúc) rồi lưu trữ dữ liệu ứng dụng trong lớp đó.
Trạng thái giao diện người dùng
Giao diện người dùng là nội dung mà người dùng nhìn thấy, còn trạng thái giao diện người dùng là nội dung mà ứng dụng chỉ định để người dùng nhìn thấy. Giao diện người dùng cho biết trạng thái giao diện người dùng một cách trực quan. Mọi thay đổi đối với trạng thái giao diện người dùng đều được phản ánh ngay lập tức trong giao diện người dùng.
Giao diện người dùng là kết quả của việc liên kết các thành phần thuộc giao diện người dùng trên màn hình với trạng thái giao diện người dùng.
// Example of UI state definition, do not copy over
data class NewsItemUiState(
val title: String,
val body: String,
val bookmarked: Boolean = false,
...
)
Bất biến
Định nghĩa về trạng thái giao diện người dùng trong ví dụ ở trên là bất biến. Các đối tượng bất biến đảm bảo việc nhiều nguồn không làm thay đổi trạng thái ứng dụng ngay lập tức. Biện pháp bảo vệ này giải phóng giao diện người dùng để tập trung vào một vai trò duy nhất: đọc trạng thái và cập nhật các phần tử trên giao diện người dùng cho phù hợp. Do đó, bạn hoàn toàn không nên trực tiếp sửa đổi trạng thái giao diện người dùng trong giao diện người dùng, trừ phi giao diện người dùng đó là nguồn duy nhất của dữ liệu. Việc vi phạm nguyên tắc này sẽ dẫn đến nhiều nguồn thông tin chính xác cho cùng một phần thông tin, dẫn đến những điểm thiếu đồng nhất của dữ liệu và các lỗi nhỏ.
5. Thêm ViewModel
Trong nhiệm vụ này, bạn thêm ViewModel
vào ứng dụng để lưu trữ trạng thái giao diện người dùng của trò chơi (từ được xáo trộn, số từ và điểm). Để giải quyết vấn đề trong mã khởi đầu mà bạn nhận thấy ở phần trước, bạn cần lưu dữ liệu trò chơi trong ViewModel
.
- Mở
build.gradle.kts (Module :app)
, di chuyển đến khốidependencies
rồi thêm phần phụ thuộc sau choViewModel
. Phần phụ thuộc này dùng để thêm ViewModel nhận biết vòng đời vào ứng dụng Compose.
dependencies {
// other dependencies
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.6.1")
//...
}
- Trong gói
ui
, hãy tạo một lớp/tệp Kotlin có tên làGameViewModel
. Mở rộng lớp này từ lớpViewModel
.
import androidx.lifecycle.ViewModel
class GameViewModel : ViewModel() {
}
- Trong gói
ui
, thêm một lớp mô hình cho giao diện người dùng trạng thái có tên làGameUiState
. Biến lớp này thành lớp dữ liệu rồi thêm biến cho từ bị xáo trộn hiện tại.
data class GameUiState(
val currentScrambledWord: String = ""
)
StateFlow
StateFlow
là một luồng có thể quan sát của lớp chứa dữ liệu, phát ra các thông tin cập nhật hiện tại và trạng thái mới. Thuộc tính value
của giá trị đó phản ánh giá trị trạng thái hiện tại. Để cập nhật trạng thái và gửi trạng thái này đến luồng, hãy gán một giá trị mới cho thuộc tính giá trị của lớp MutableStateFlow
.
Trong Android, StateFlow
hoạt động hiệu quả đối với các lớp phải duy trì trạng thái bất biến có thể ghi nhận được.
StateFlow
có thể được cung cấp từ GameUiState
để các thành phần kết hợp có thể theo dõi thông tin cập nhật trạng thái giao diện người dùng, đồng thời giúp duy trì trạng thái màn hình sau khi cấu hình thay đổi.
Trong lớp GameViewModel
, hãy thêm thuộc tính _uiState
sau.
import kotlinx.coroutines.flow.MutableStateFlow
// Game UI state
private val _uiState = MutableStateFlow(GameUiState())
Thuộc tính sao lưu
Thuộc tính sao lưu cho phép bạn trả về nội dung qua một phương thức getter khác với đối tượng chính xác.
Đối với thuộc tính var
, khung Kotlin sẽ tạo phương thức getter và setter.
Đối với phương thức getter và setter, bạn có thể ghi đè lên một hoặc cả hai phương pháp này rồi cung cấp hành vi tuỳ chỉnh của riêng mình. Để triển khai thuộc tính sao lưu, bạn sẽ ghi đè phương thức getter để trả về phiên bản dữ liệu chỉ có thể đọc. Ví dụ sau đây cho thấy một thuộc tính sao lưu:
//Example code, no need to copy over
// Declare private mutable variable that can only be modified
// within the class it is declared.
private var _count = 0
// Declare another public immutable field and override its getter method.
// Return the private property's value in the getter method.
// When count is accessed, the get() function is called and
// the value of _count is returned.
val count: Int
get() = _count
Một ví dụ khác là giả sử bạn muốn dữ liệu ứng dụng ở chế độ riêng tư trong ViewModel
:
Bên trong lớp ViewModel
:
- Thuộc tính
_count
làprivate
và có thể biến đổi. Do đó, bạn chỉ có thể truy cập và chỉnh sửa tệp này trong lớpViewModel
.
Bên ngoài lớp ViewModel
:
- Đối tượng sửa đổi chế độ hiển thị mặc định trong Kotlin là
public
, vậy nêncount
là công khai và có thể truy cập được qua các lớp khác, chẳng hạn như bộ điều khiển giao diện người dùng. Kiểuval
không thể có phương thức setter. Kiểu này không thể thay đổi và chỉ có thể đọc, vì vậy, bạn chỉ có thể ghi đè phương thứcget()
. Khi một lớp bên ngoài truy cập thuộc tính này, thuộc tính sẽ trả về giá trị của_count
và giá trị đó không thể sửa đổi. Thuộc tính sao lưu này giúp bảo vệ dữ liệu ứng dụng trongViewModel
khỏi những thay đổi ngoài ý muốn và không an toàn do các lớp bên ngoài thực hiện, nhưng cho phép phương thức gọi bên ngoài truy cập an toàn vào giá trị của nó.
- Trong tệp
GameViewModel.kt
, hãy thêm thuộc tính sao lưu vàouiState
có tên là_uiState
. Đặt tên thuộc tính làuiState
, thuộc tính này thuộc kiểuStateFlow<GameUiState>
.
Hiện tại, bạn chỉ có thể truy cập và chỉnh sửa _uiState
trong GameViewModel
. Giao diện người dùng có thể đọc giá trị đó bằng cách sử dụng thuộc tính chỉ có thể đọc là uiState
. Bạn có thể khắc phục lỗi khởi động trong bước tiếp theo.
import kotlinx.coroutines.flow.StateFlow
// Game UI state
// Backing property to avoid state updates from other classes
private val _uiState = MutableStateFlow(GameUiState())
val uiState: StateFlow<GameUiState>
- Đặt
uiState
thành_uiState.asStateFlow()
.
asStateFlow()
làm cho luồng trạng thái có thể thay đổi này trở thành luồng trạng thái chỉ đọc.
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
// Game UI state
private val _uiState = MutableStateFlow(GameUiState())
val uiState: StateFlow<GameUiState> = _uiState.asStateFlow()
Hiện từ được xáo trộn một cách ngẫu nhiên
Trong nhiệm vụ này, bạn sẽ thêm các phương thức trợ giúp để chọn một từ ngẫu nhiên trong WordsData.kt
và xáo trộn từ đó.
- Trong
GameViewModel
, hãy thêm thuộc tính có tên làcurrentWord
thuộc loạiString
để lưu từ bị xáo trộn hiện tại.
private lateinit var currentWord: String
- Thêm phương thức trợ giúp để chọn một từ ngẫu nhiên trong danh sách và xáo trộn từ đó. Đặt tên cho phương thức đó là
pickRandomWordAndShuffle()
mà không có tham số đầu vào và trả về một giá trịString
.
import com.example.unscramble.data.allWords
private fun pickRandomWordAndShuffle(): String {
// Continue picking up a new random word until you get one that hasn't been used before
currentWord = allWords.random()
if (usedWords.contains(currentWord)) {
return pickRandomWordAndShuffle()
} else {
usedWords.add(currentWord)
return shuffleCurrentWord(currentWord)
}
}
Android Studio sẽ gắn cờ thông báo lỗi hàm và biến không xác định.
- Trong
GameViewModel
, hãy thêm thuộc tính dưới đây vào sau thuộc tínhcurrentWord
để đóng vai trò như một tập hợp có thể thay đổi để lưu trữ các từ đã dùng trong trò chơi.
// Set of words used in the game
private var usedWords: MutableSet<String> = mutableSetOf()
- Thêm một phương thức trợ giúp khác có tên là
shuffleCurrentWord()
để xáo trộn từ hiện tại. Phương thức này sẽ nhậnString
và trả vềString
đã xáo trộn.
private fun shuffleCurrentWord(word: String): String {
val tempWord = word.toCharArray()
// Scramble the word
tempWord.shuffle()
while (String(tempWord).equals(word)) {
tempWord.shuffle()
}
return String(tempWord)
}
- Thêm một hàm trợ giúp có tên là
resetGame()
để khởi động trò chơi. Bạn sẽ sử dụng hàm này sau để bắt đầu và khởi động lại trò chơi. Trong hàm này, xoá tất cả các từ trong tập hợpusedWords
và bắt đầu_uiState
. Chọn một từ mới chocurrentScrambledWord
bằngpickRandomWordAndShuffle()
.
fun resetGame() {
usedWords.clear()
_uiState.value = GameUiState(currentScrambledWord = pickRandomWordAndShuffle())
}
- Thêm một khối
init
vàoGameViewModel
và gọiresetGame()
qua khối đó.
init {
resetGame()
}
Khi tạo ứng dụng ngay, bạn vẫn không thấy sự thay đổi nào trong giao diện người dùng. Bạn không truyền dữ liệu từ ViewModel
sang các thành phần kết hợp trong GameScreen
.
6. Thiết kế giao diện người dùng trong Compose
Trong Compose, cách duy nhất để cập nhật giao diện người dùng là thay đổi trạng thái ứng dụng. Bạn chỉ có thể kiểm soát trạng thái giao diện người dùng. Mỗi khi trạng thái giao diện người dùng thay đổi, Compose sẽ tạo lại các phần đã thay đổi của cây giao diện người dùng. Các thành phần kết hợp có thể chấp nhận trạng thái và hiển thị sự kiện. Ví dụ: TextField
/OutlinedTextField
chấp nhận một giá trị và hiển thị một lệnh gọi lại onValueChange
yêu cầu trình xử lý gọi lại thay đổi giá trị đó.
//Example code no need to copy over
var name by remember { mutableStateOf("") }
OutlinedTextField(
value = name,
onValueChange = { name = it },
label = { Text("Name") }
)
Vì các thành phần kết hợp chấp nhận trạng thái và hiển thị sự kiện nên mẫu luồng dữ liệu một chiều rất phù hợp với Jetpack Compose. Phần này tập trung vào cách triển khai mẫu luồng dữ liệu một chiều trong Compose, cách triển khai các sự kiện và phần tử giữ trạng thái, cũng như cách làm việc với ViewModel
trong Compose.
Luồng dữ liệu một chiều
Luồng dữ liệu một chiều (UDF) là một mẫu thiết kế trong đó trạng thái chạy xuống và các sự kiện chạy lên. Bằng cách làm theo luồng dữ liệu một chiều, bạn có thể phân tách các thành phần có thể kết hợp hiển thị trạng thái trong giao diện người dùng khỏi các phần của ứng dụng lưu trữ rồi thay đổi trạng thái.
Vòng lặp cập nhật giao diện người dùng cho một ứng dụng sử dụng luồng dữ liệu một chiều như sau:
- Sự kiện: Một phần của giao diện người dùng tạo sự kiện và truyền sự kiện lên trên (chẳng hạn như lượt nhấp vào nút được chuyển đến ViewModel để xử lý) hoặc một sự kiện được truyền từ các lớp khác trong ứng dụng, chẳng hạn như cho biết phiên người dùng đã hết hạn.
- Trạng thái cập nhật: Trình xử lý sự kiện có thể thay đổi trạng thái.
- Trạng thái hiển thị: Phần tử giữ trạng thái chuyển xuống trạng thái và giao diện người dùng sẽ hiển thị trạng thái này.
Việc sử dụng mẫu UDF cho cấu trúc ứng dụng có các tác động sau:
ViewModel
giữ và hiển thị trạng thái mà giao diện người dùng sử dụng.- Trạng thái giao diện người dùng là dữ liệu ứng dụng do
ViewModel
chuyển đổi. - Giao diện người dùng thông báo cho
ViewModel
về các sự kiện của người dùng. ViewModel
xử lý các thao tác của người dùng và cập nhật trạng thái.- Trạng thái đã cập nhật được đưa trở lại giao diện người dùng để hiển thị.
- Quá trình này lặp lại cho bất kỳ sự kiện nào gây ra hiện tượng đột biến trạng thái.
Truyền dữ liệu
Truyền thực thể của ViewModel cho giao diện người dùng – tức là từ GameViewModel
đến GameScreen()
trong tệp GameScreen.kt
. Trong GameScreen()
, hãy sử dụng thực thể ViewModel để truy cập vào uiState
bằng collectAsState()
.
Hàm collectAsState()
thu thập các giá trị từ StateFlow
này và biểu thị giá trị mới nhất của hàm này qua State
. StateFlow.value
được dùng làm giá trị ban đầu. Mỗi khi có một giá trị mới được đăng vào StateFlow
, State
được trả về sẽ cập nhật, dẫn đến việc kết hợp lại mọi hoạt động sử dụng State.value
.
- Trong hàm
GameScreen
, hãy truyền một đối số thứ hai thuộc loạiGameViewModel
với giá trị mặc định củaviewModel()
.
import androidx.lifecycle.viewmodel.compose.viewModel
@Composable
fun GameScreen(
gameViewModel: GameViewModel = viewModel()
) {
// ...
}
- Trong hàm
GameScreen()
, thêm một biến mới có tên làgameUiState
. Sử dụng đối tượng uỷ quyềnby
và gọicollectAsState()
trênuiState
.
Cách tiếp cận này đảm bảo rằng mỗi khi có sự thay đổi trong giá trị uiState
, quá trình kết hợp lại sẽ diễn ra đối với các thành phần kết hợp sử dụng giá trị gameUiState
.
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
@Composable
fun GameScreen(
// ...
) {
val gameUiState by gameViewModel.uiState.collectAsState()
// ...
}
- Truyền
gameUiState.currentScrambledWord
vào thành phần kết hợpGameLayout()
. Bạn sẽ thêm đối số ở bước sau, vậy nên giờ hãy bỏ qua lỗi này.
GameLayout(
currentScrambledWord = gameUiState.currentScrambledWord,
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight()
.padding(mediumPadding)
)
- Thêm
currentScrambledWord
làm một tham số khác vào hàm có khả năng kết hợpGameLayout()
.
@Composable
fun GameLayout(
currentScrambledWord: String,
modifier: Modifier = Modifier
) {
}
- Cập nhật hàm có khả năng kết hợp
GameLayout()
để hiển thịcurrentScrambledWord
. Đặt tham sốtext
của trường văn bản đầu tiên trong cột thànhcurrentScrambledWord
.
@Composable
fun GameLayout(
// ...
) {
Column(
verticalArrangement = Arrangement.spacedBy(24.dp)
) {
Text(
text = currentScrambledWord,
fontSize = 45.sp,
modifier = modifier.align(Alignment.CenterHorizontally)
)
//...
}
}
- Chạy và tạo bản dựng ứng dụng. Bạn sẽ thấy từ được xáo trộn.
Hiện từ đoán
Trong thành phần kết hợp GameLayout()
, việc cập nhật từ đoán của người dùng là một trong các lệnh gọi lại sự kiện chạy từ GameScreen
đến ViewModel
. Dữ liệu gameViewModel.userGuess
sẽ lưu chuyển từ ViewModel
xuống GameScreen
.
- Trong tệp
GameScreen.kt
, trong thành phần kết hợpGameLayout()
, hãy đặtonValueChange
thànhonUserGuessChanged
vàonKeyboardDone()
thành thao tác bàn phímonDone
. Bạn sẽ sửa lỗi trong bước tiếp theo.
OutlinedTextField(
value = "",
singleLine = true,
modifier = Modifier.fillMaxWidth(),
onValueChange = onUserGuessChanged,
label = { Text(stringResource(R.string.enter_your_word)) },
isError = false,
keyboardOptions = KeyboardOptions.Default.copy(
imeAction = ImeAction.Done
),
keyboardActions = KeyboardActions(
onDone = { onKeyboardDone() }
),
- Trong hàm có khả năng kết hợp
GameLayout()
, thêm 2 đối số khác: hàm lambdaonUserGuessChanged
nhận đối sốString
và không trả về giá trị nào, cònonKeyboardDone
không nhận và cũng không trả về giá trị nào.
@Composable
fun GameLayout(
onUserGuessChanged: (String) -> Unit,
onKeyboardDone: () -> Unit,
currentScrambledWord: String,
modifier: Modifier = Modifier,
) {
}
- Trong lệnh gọi hàm
GameLayout()
, hãy thêm các đối số lambda choonUserGuessChanged
vàonKeyboardDone
.
GameLayout(
onUserGuessChanged = { gameViewModel.updateUserGuess(it) },
onKeyboardDone = { },
currentScrambledWord = gameUiState.currentScrambledWord,
)
Bạn sẽ sớm xác định được phương thức updateUserGuess
trong GameViewModel
.
- Trong tệp
GameViewModel.kt
, hãy thêm một phương thức có tên làupdateUserGuess()
. Phương thức này sẽ nhận đối sốString
là từ đoán của người dùng. Bên trong hàm này, hãy cập nhậtuserGuess
với giá trị được truyền vào làguessedWord
.
fun updateUserGuess(guessedWord: String){
userGuess = guessedWord
}
Tiếp theo, bạn sẽ thêm userGuess
vào ViewModel.
- Trong tệp
GameViewModel.kt
, hãy thêm thuộc tính biến có tên làuserGuess
. Hãy sử dụngmutableStateOf()
để Compose có thể quan sát giá trị này và đặt giá trị ban đầu thành""
.
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
var userGuess by mutableStateOf("")
private set
- Trong tệp
GameScreen.kt
bên trongGameLayout()
, hãy thêm một tham sốString
khác chouserGuess
. Đặt tham sốvalue
củaOutlinedTextField
thànhuserGuess
.
fun GameLayout(
currentScrambledWord: String,
userGuess: String,
onUserGuessChanged: (String) -> Unit,
onKeyboardDone: () -> Unit,
modifier: Modifier = Modifier
) {
Column(
verticalArrangement = Arrangement.spacedBy(24.dp)
) {
//...
OutlinedTextField(
value = userGuess,
//..
)
}
}
- Trong hàm
GameScreen
, hãy cập nhật lệnh gọi hàmGameLayout()
để thêm tham sốuserGuess
.
GameLayout(
currentScrambledWord = gameUiState.currentScrambledWord,
userGuess = gameViewModel.userGuess,
onUserGuessChanged = { gameViewModel.updateUserGuess(it) },
onKeyboardDone = { },
//...
)
- Tạo và chạy ứng dụng của bạn.
- Hãy thử đoán và nhập một từ. Trường văn bản có thể hiển thị từ mà người dùng đoán.
7. Xác minh từ dự đoán và cập nhật điểm
Trong nhiệm vụ này, bạn sẽ triển khai một phương thức để xác minh từ mà người dùng đoán và sau đó cập nhật điểm số trò chơi hoặc hiển thị lỗi. Bạn sẽ cập nhật điểm số mới và từ mới sau trên giao diện người dùng trạng thái trò chơi.
- Trong
GameViewModel
, thêm một phương thức khác có tên làcheckUserGuess()
. - Trong hàm
checkUserGuess()
, hãy thêm một khốiif else
để xác minh xem suy đoán của người dùng có giống vớicurrentWord
hay không. Đặt lạiuserGuess
thành chuỗi trống.
fun checkUserGuess() {
if (userGuess.equals(currentWord, ignoreCase = true)) {
} else {
}
// Reset user guess
updateUserGuess("")
}
- Nếu người dùng đoán đúng, hãy đặt
isGuessedWordWrong
thànhtrue
.MutableStateFlow<T>.
update()
cập nhậtMutableStateFlow.value
bằng cách sử dụng giá trị được chỉ định.
import kotlinx.coroutines.flow.update
if (userGuess.equals(currentWord, ignoreCase = true)) {
} else {
// User's guess is wrong, show an error
_uiState.update { currentState ->
currentState.copy(isGuessedWordWrong = true)
}
}
- Trong lớp
GameUiState
, hãy thêmBoolean
có tên làisGuessedWordWrong
rồi khởi động thànhfalse
.
data class GameUiState(
val currentScrambledWord: String = "",
val isGuessedWordWrong: Boolean = false,
)
Tiếp theo, bạn truyền lệnh gọi lại sự kiện checkUserGuess()
từ GameScreen
lên ViewModel
khi người dùng nhấp vào nút Gửi hoặc phím Xong (Done) trên bàn phím. Truyền dữ liệu gameUiState.isGuessedWordWrong
xuống từ ViewModel
sang GameScreen
để thiết lập lỗi trong trường văn bản.
- Trong tệp
GameScreen.kt
, ở cuối hàm có khả năng kết hợpGameScreen()
, hãy gọigameViewModel.checkUserGuess()
bên trong biểu thức lambdaonClick
của nút Submit (Gửi).
Button(
modifier = modifier
.fillMaxWidth()
.weight(1f)
.padding(start = 8.dp),
onClick = { gameViewModel.checkUserGuess() }
) {
Text(stringResource(R.string.submit))
}
- Trong hàm có khả năng kết hợp
GameScreen()
, hãy cập nhật lệnh gọi hàmGameLayout()
để truyềngameViewModel.checkUserGuess()
trong biểu thức lambdaonKeyboardDone
.
GameLayout(
currentScrambledWord = gameUiState.currentScrambledWord,
userGuess = gameViewModel.userGuess,
onUserGuessChanged = { gameViewModel.updateUserGuess(it) },
onKeyboardDone = { gameViewModel.checkUserGuess() }
)
- Trong hàm có khả năng kết hợp
GameLayout()
, thêm tham số hàm choBoolean
,isGuessWrong
. Đặt tham sốisError
củaOutlinedTextField
thànhisGuessWrong
để hiển thị lỗi trong trường văn bản nếu người dùng đoán sai.
fun GameLayout(
currentScrambledWord: String,
isGuessWrong: Boolean,
userGuess: String,
onUserGuessChanged: (String) -> Unit,
onKeyboardDone: () -> Unit,
modifier: Modifier = Modifier
) {
Column(
// ,...
OutlinedTextField(
// ...
isError = isGuessWrong,
keyboardOptions = KeyboardOptions.Default.copy(
imeAction = ImeAction.Done
),
keyboardActions = KeyboardActions(
onDone = { onKeyboardDone() }
),
)
}
}
- Trong hàm có khả năng kết hợp
GameScreen()
, hãy cập nhật lệnh gọi hàmGameLayout()
để truyềnisGuessWrong
.
GameLayout(
currentScrambledWord = gameUiState.currentScrambledWord,
userGuess = gameViewModel.userGuess,
onUserGuessChanged = { gameViewModel.updateUserGuess(it) },
onKeyboardDone = { gameViewModel.checkUserGuess() },
isGuessWrong = gameUiState.isGuessedWordWrong,
// ...
)
- Tạo và chạy ứng dụng của bạn.
- Nhập vào một phỏng đoán sai rồi nhấp vào Submit (Gửi). Quan sát trường văn bản chuyển sang màu đỏ biểu thị lỗi.
Lưu ý rằng nhãn trường văn bản vẫn hiện dòng chữ "Enter your word" (Nhập từ của bạn). Để thân thiện với người dùng, bạn cần thêm một số văn bản lỗi để cho biết từ đó không chính xác.
- Ở tệp
GameScreen.kt
, trong thành phần kết hợpGameLayout()
, hãy cập nhật tham số nhãn của trường văn bản tuỳ thuộc vàoisGuessWrong
như sau:
OutlinedTextField(
// ...
label = {
if (isGuessWrong) {
Text(stringResource(R.string.wrong_guess))
} else {
Text(stringResource(R.string.enter_your_word))
}
},
// ...
)
- Trong tệp
strings.xml
, hãy thêm một chuỗi vào nhãn lỗi.
<string name="wrong_guess">Wrong Guess!</string>
- Xây dựng rồi chạy lại ứng dụng của bạn.
- Nhập vào một phỏng đoán sai rồi nhấp vào Submit (Gửi). Hãy lưu ý nhãn lỗi.
8. Cập nhật điểm và số từ
Trong nhiệm vụ này, bạn sẽ cập nhật điểm số và số từ khi người dùng chơi trò chơi. Điểm số phải là một phần của _ uiState
.
- Trong
GameUiState
, hãy thêm một biếnscore
và khởi tạo biến đó bằng 0.
data class GameUiState(
val currentScrambledWord: String = "",
val isGuessedWordWrong: Boolean = false,
val score: Int = 0
)
- Để cập nhật giá trị điểm, trong
GameViewModel
, trong hàmcheckUserGuess()
, bên trong điều kiệnif
, khi người dùng đoán đúng, hãy tăng giá trịscore
.
import com.example.unscramble.data.SCORE_INCREASE
fun checkUserGuess() {
if (userGuess.equals(currentWord, ignoreCase = true)) {
// User's guess is correct, increase the score
val updatedScore = _uiState.value.score.plus(SCORE_INCREASE)
} else {
//...
}
}
- Trong
GameViewModel
, hãy thêm một phương thức khác tên làupdateGameState
để cập nhật điểm, tăng số đếm từ hiện tại và chọn một từ mới trong tệpWordsData.kt
. ThêmInt
có tên làupdatedScore
dưới dạng tham số. Cập nhật các biến giao diện người dùng của trạng thái trò chơi như sau:
private fun updateGameState(updatedScore: Int) {
_uiState.update { currentState ->
currentState.copy(
isGuessedWordWrong = false,
currentScrambledWord = pickRandomWordAndShuffle(),
score = updatedScore
)
}
}
- Trong hàm
checkUserGuess()
, nếu người dùng đoán đúng, hãy gọiupdateGameState
có điểm số đã cập nhật để chuẩn bị trò chơi cho vòng tiếp theo.
fun checkUserGuess() {
if (userGuess.equals(currentWord, ignoreCase = true)) {
// User's guess is correct, increase the score
// and call updateGameState() to prepare the game for next round
val updatedScore = _uiState.value.score.plus(SCORE_INCREASE)
updateGameState(updatedScore)
} else {
//...
}
}
Hàm checkUserGuess()
hoàn chỉnh sẽ có dạng như sau:
fun checkUserGuess() {
if (userGuess.equals(currentWord, ignoreCase = true)) {
// User's guess is correct, increase the score
// and call updateGameState() to prepare the game for next round
val updatedScore = _uiState.value.score.plus(SCORE_INCREASE)
updateGameState(updatedScore)
} else {
// User's guess is wrong, show an error
_uiState.update { currentState ->
currentState.copy(isGuessedWordWrong = true)
}
}
// Reset user guess
updateUserGuess("")
}
Tiếp theo, tương tự như thông tin cập nhật về điểm số, bạn cũng cần cập nhật số từ.
- Thêm một biến khác cho số lượng trong
GameUiState
. Gọi nó làcurrentWordCount
và khởi chạy nó bằng1
.
data class GameUiState(
val currentScrambledWord: String = "",
val currentWordCount: Int = 1,
val score: Int = 0,
val isGuessedWordWrong: Boolean = false,
)
- Ở tệp
GameViewModel.kt
, trong hàmupdateGameState()
, hãy tăng số từ như bên dưới. HàmupdateGameState()
được gọi để chuẩn bị trò chơi cho vòng tiếp theo.
private fun updateGameState(updatedScore: Int) {
_uiState.update { currentState ->
currentState.copy(
//...
currentWordCount = currentState.currentWordCount.inc(),
)
}
}
Chuyển điểm và số từ
Hoàn tất các bước sau để chuyển dữ liệu về số từ và điểm số từ ViewModel
xuống GameScreen
.
- Trong tệp
GameScreen.kt
, trong hàm có khả năng kết hợpGameLayout()
, hãy thêm số từ làm đối số và chuyển các đối số định dạngwordCount
vào phần tử văn bản.
fun GameLayout(
onUserGuessChanged: (String) -> Unit,
onKeyboardDone: () -> Unit,
wordCount: Int,
//...
) {
//...
Card(
//...
) {
Column(
// ...
) {
Text(
//..
text = stringResource(R.string.word_count, wordCount),
style = typography.titleMedium,
color = colorScheme.onPrimary
)
// ...
}
- Cập nhật lệnh gọi hàm
GameLayout()
để đưa số từ vào.
GameLayout(
userGuess = gameViewModel.userGuess,
wordCount = gameUiState.currentWordCount,
//...
)
- Trong hàm có khả năng kết hợp
GameScreen()
, hãy cập nhật lệnh gọi hàmGameStatus()
để đưa vào các tham sốscore
. Chuyển điểm số từgameUiState
.
GameStatus(score = gameUiState.score, modifier = Modifier.padding(20.dp))
- Tạo bản dựng và chạy ứng dụng
- Nhập từ suy đoán rồi nhấp vào Submit (Gửi). Hãy lưu ý thông tin cập nhật về điểm số và số từ.
- Nhấp vào Skip (Bỏ qua) và lưu ý là không có gì xảy ra.
Để triển khai chức năng bỏ qua, bạn cần truyền lệnh gọi lại sự kiện bỏ qua đến GameViewModel
.
- Ở tệp
GameScreen.kt
, trong hàm có khả năng kết hợpGameScreen()
, thực hiện lệnh gọi đếngameViewModel.skipWord()
trong biểu thức lambdaonClick
.
Android Studio hiển thị lỗi vì bạn chưa triển khai hàm này. Bạn sẽ khắc phục lỗi này trong bước tiếp theo bằng cách thêm phương thức skipWord()
. Khi người dùng bỏ qua một từ, bạn cần phải cập nhật biến của trò chơi và chuẩn bị trò chơi cho vòng tiếp theo.
OutlinedButton(
onClick = { gameViewModel.skipWord() },
modifier = Modifier.fillMaxWidth()
) {
//...
}
- Trong
GameViewModel
, thêm phương thứcskipWord()
. - Bên trong hàm
skipWord()
, hãy gọi đếnupdateGameState()
, truyền điểm và đặt lại thông tin dự đoán của người dùng.
fun skipWord() {
updateGameState(_uiState.value.score)
// Reset user guess
updateUserGuess("")
}
- Chạy ứng dụng và chơi trò chơi. Giờ đây bạn đã có thể bỏ qua các từ.
Bạn vẫn có thể chơi trò chơi sau hơn 10 từ. Trong nhiệm vụ tiếp theo, bạn sẽ xử lý vòng cuối của trò chơi.
9. Xử lý vòng cuối cùng của trò chơi
Trong cách triển khai hiện tại, người dùng có thể bỏ qua hoặc dùng hơn 10 từ. Trong nhiệm vụ này, bạn sẽ thêm logic để kết thúc trò chơi.
Để triển khai logic kết thúc trò chơi, trước tiên, bạn cần kiểm tra xem người dùng có đạt đến số từ tối đa không.
- Trong
GameViewModel
, hãy thêm một khốiif-else
và di chuyển phần thân (nội dung) hàm hiện có bên trong khốielse
. - Thêm một điều kiện
if
để kiểm tra kích thướcusedWords
có bằng vớiMAX_NO_OF_WORDS
không.
import com.example.android.unscramble.data.MAX_NO_OF_WORDS
private fun updateGameState(updatedScore: Int) {
if (usedWords.size == MAX_NO_OF_WORDS){
//Last round in the game
} else{
// Normal round in the game
_uiState.update { currentState ->
currentState.copy(
isGuessedWordWrong = false,
currentScrambledWord = pickRandomWordAndShuffle(),
currentWordCount = currentState.currentWordCount.inc(),
score = updatedScore
)
}
}
}
- Bên trong khối
if
, hãy thêm cờBoolean
vàoisGameOver
rồi đặt cờ thànhtrue
để cho biết thời điểm kết thúc trò chơi. - Cập nhật
score
và đặt lạiisGuessedWordWrong
bên trong khốiif
. Đoạn mã sau sẽ cho thấy hàm của bạn trông như thế nào:
private fun updateGameState(updatedScore: Int) {
if (usedWords.size == MAX_NO_OF_WORDS){
//Last round in the game, update isGameOver to true, don't pick a new word
_uiState.update { currentState ->
currentState.copy(
isGuessedWordWrong = false,
score = updatedScore,
isGameOver = true
)
}
} else{
// Normal round in the game
_uiState.update { currentState ->
currentState.copy(
isGuessedWordWrong = false,
currentScrambledWord = pickRandomWordAndShuffle(),
currentWordCount = currentState.currentWordCount.inc(),
score = updatedScore
)
}
}
}
- Trong
GameUiState
, hãy thêm biếnBoolean
isGameOver
rồi đặt biến đó thànhfalse
.
data class GameUiState(
val currentScrambledWord: String = "",
val currentWordCount: Int = 1,
val score: Int = 0,
val isGuessedWordWrong: Boolean = false,
val isGameOver: Boolean = false
)
- Chạy ứng dụng và chơi trò chơi. Bạn không thể chơi quá 10 từ.
Khi trò chơi kết thúc, bạn nên thông báo cho người dùng và hỏi xem họ có muốn chơi lại không. Bạn sẽ triển khai tính năng này trong nhiệm vụ tiếp theo.
Hiện hộp thoại kết thúc trò chơi
Trong nhiệm vụ này, bạn sẽ truyền dữ liệu isGameOver
xuống GameScreen
từ ViewModel và sử dụng dữ liệu này để hiển thị hộp thoại cảnh báo có các tuỳ chọn kết thúc hoặc khởi động lại trò chơi.
Hộp thoại là một cửa sổ nhỏ nhắc người dùng đưa ra quyết định hoặc nhập thông tin bổ sung. Thông thường, hộp thoại không lấp đầy toàn bộ màn hình và sẽ yêu cầu người dùng thực hiện hành động trước khi có thể tiếp tục. Android cung cấp nhiều loại hộp thoại. Trong lớp học lập trình này, bạn sẽ tìm hiểu về Hộp thoại thông báo.
Thông tin chi tiết về hộp thoại thông báo
- Vùng chứa (container)
- Biểu tượng (không bắt buộc)
- Dòng tiêu đề (không bắt buộc)
- Văn bản hỗ trợ
- Dải phân cách (không bắt buộc)
- Thao tác
Tệp GameScreen.kt
trong mã khởi động đã cung cấp một hàm hiển thị hộp thoại cảnh báo với các tuỳ chọn thoát hoặc khởi động lại trò chơi.
@Composable
private fun FinalScoreDialog(
onPlayAgain: () -> Unit,
modifier: Modifier = Modifier
) {
val activity = (LocalContext.current as Activity)
AlertDialog(
onDismissRequest = {
// Dismiss the dialog when the user clicks outside the dialog or on the back
// button. If you want to disable that functionality, simply use an empty
// onDismissRequest.
},
title = { Text(stringResource(R.string.congratulations)) },
text = { Text(stringResource(R.string.you_scored, 0)) },
modifier = modifier,
dismissButton = {
TextButton(
onClick = {
activity.finish()
}
) {
Text(text = stringResource(R.string.exit))
}
},
confirmButton = {
TextButton(
onClick = {
onPlayAgain()
}
) {
Text(text = stringResource(R.string.play_again))
}
}
)
}
Trong hàm này, các tham số title
và text
sẽ cho thấy dòng tiêu đề và văn bản hỗ trợ trong hộp thoại cảnh báo. dismissButton
và confirmButton
là các nút văn bản. Trong tham số dismissButton
, bạn sẽ hiển thị văn bản Exit (Thoát) và kết thúc ứng dụng bằng cách hoàn tất hoạt động. Trong tham số confirmButton
, bạn sẽ khởi động lại trò chơi và hiển thị văn bản Play Again (Chơi lại).
- Trong tệp
GameScreen.kt
, trong hàmFinalScoreDialog()
, hãy chú ý đến tham số cho điểm số để hiển thị điểm số trò chơi trong hộp thoại cảnh báo.
@Composable
private fun FinalScoreDialog(
score: Int,
onPlayAgain: () -> Unit,
modifier: Modifier = Modifier
) {
- Trong hàm
FinalScoreDialog()
, hãy lưu ý đến việc sử dụng biểu thức lambda tham sốtext
để sử dụngscore
làm đối số định dạng cho văn bản hộp thoại.
text = { Text(stringResource(R.string.you_scored, score)) }
- Trong tệp
GameScreen.kt
, ở cuối hàm có khả năng kết hợpGameScreen()
, sau khốiColumn
, hãy thêm một điều kiệnif
để kiểm tragameUiState.isGameOver
. - Trong khối
if
, hãy hiện hộp thoại thông báo. Gọi đếnFinalScoreDialog()
rồi truyềnscore
vàgameViewModel.resetGame()
vào lệnh gọi lại sự kiệnonPlayAgain
.
if (gameUiState.isGameOver) {
FinalScoreDialog(
score = gameUiState.score,
onPlayAgain = { gameViewModel.resetGame() }
)
}
resetGame()
là một lệnh gọi lại sự kiện được truyền từ GameScreen
đến ViewModel
.
- Trong tệp
GameViewModel.kt
, thu hồi hàmresetGame()
, khởi động_uiState
rồi chọn một từ mới.
fun resetGame() {
usedWords.clear()
_uiState.value = GameUiState(currentScrambledWord = pickRandomWordAndShuffle())
}
- Tạo và chạy ứng dụng của bạn.
- Chơi cho đến khi kết thúc trò chơi và quan sát hộp thoại cảnh báo với các tuỳ chọn để Exit (Thoát) trò chơi hoặc Play Again (Chơi lại). Hãy thử các lựa chọn xuất hiện trên hộp thoại cảnh báo.
10. Trạng thái trong chế độ xoay thiết bị
Trong các lớp học lập trình trước, bạn đã tìm hiểu những thay đổi về cấu hình trong Android. Khi xảy ra thay đổi về cấu hình, Android sẽ khởi động lại hoạt động từ đầu, chạy tất cả các lệnh gọi lại khởi động vòng đời.
ViewModel
sẽ lưu trữ dữ liệu liên quan đến ứng dụng không bị huỷ bỏ khi khung Android huỷ và tạo lại hoạt động. Các đối tượng ViewModel
tự động được giữ lại và không bị huỷ bỏ như thực thể hoạt động trong quá trình thay đổi cấu hình. Dữ liệu mà các đối tượng này giữ lại sẽ có sẵn ngay sau khi kết hợp lại.
Trong nhiệm vụ này, bạn sẽ kiểm tra xem ứng dụng có giữ lại giao diện người dùng trạng thái trong khi thay đổi cấu hình hay không.
- Chạy ứng dụng và chơi một vài từ. Thay đổi cấu hình của thiết bị từ chế độ dọc sang chế độ ngang hoặc ngược lại.
- Lưu ý rằng dữ liệu được lưu trong giao diện người dùng của trạng thái
ViewModel
được giữ lại trong quá trình thay đổi cấu hình.
11. Lấy mã giải pháp
Để tải mã này xuống khi lớp học lập trình đã kết thúc, bạn có thể sử dụng các lệnh git sau:
$ git clone https://github.com/google-developer-training/basic-android-kotlin-compose-training-unscramble.git $ cd basic-android-kotlin-compose-training-unscramble $ git checkout viewmodel
Ngoài ra, bạn có thể tải kho lưu trữ xuống dưới dạng tệp zip rồi giải nén và mở trong Android Studio.
Nếu bạn muốn tham khảo đoạn mã giải pháp cho lớp học lập trình này, hãy xem trên GitHub.
12. Kết luận
Xin chúc mừng! Bạn đã hoàn tất lớp học lập trình. Giờ đây, bạn đã hiểu cách các hướng dẫn về cấu trúc ứng dụng Android đề xuất việc tách các lớp có những trách nhiệm khác nhau và điều khiển giao diện người dùng bằng mô hình.
Đừng quên chia sẻ công việc của bạn trên mạng xã hội với #AndroidBasics!