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 hoạt động (activity) và mảnh (fragment) cũng như các vấn đề liên quan đến vòng đời trong khi thay đổi cấu hình. Để lưu dữ liệu ứng dụng, bạn có thể lưu trạng thái của thực thể nhưng phương thức này đi kèm với một số giới hạn riêng. Trong lớp học lập trình này, bạn sẽ tìm hiểu về một cách hiệu quả để thiết kế ứng dụng và lưu giữ dữ liệu ứng dụng trong quá trình thay đổi cấu hình, bằng cách tận dụng thư viện Android Jetpack.
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 tác vụ phức tạp để bạn có thể tập trung vào mã mình quan tâm, chẳng hạn như logic ứng dụng.
Bộ thành phần cấu trúc Android nằm trong thư viện Android Jetpack, giúp bạn thiết kế những ứng dụng có cấu trúc phù hợp. Thành phần cấu trúc (architecture) cung cấp hướng dẫn về cấu trúc ứng dụng. Đây cũng là phương pháp hay nhất được đề xuất.
Cấu trúc ứng dụng là một bộ quy tắc thiết kế. Cũng giống như bản thiết kế của một ngôi nhà, cấu trúc tạo nên ứng dụng. Một cấu trúc ứng dụng phù hợp có thể giúp mã trở nên hiệu quả, linh hoạt, dễ mở rộng và duy trì lâu dài về sau.
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 để lưu trữ dữ liệu ứng dụng. 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 (activity) và mảnh (fragment) trong quá trình thay đổi cấu hình hoặc các sự kiện khác.
Điều kiện tiên quyết
- Cách tải mã nguồn xuống qua GitHub rồi mở mã đó trong Android Studio.
- Cách sử dụng hoạt động và mảnh để tạo cũng như chạy một ứng dụng Android cơ bản trong Kotlin.
- Kiến thức về trường văn bản Material và các tiện ích phổ biến trên giao diện người dùng như
TextView
vàButton
. - Cách sử dụng liên kết thành phần hiển thị (view binding) trong ứng dụng.
- Thông tin cơ bản về vòng đời của hoạt động và mảnh.
- Cách thêm thông tin ghi nhật ký vào ứng dụng và đọc nhật ký bằng Logcat trong Android Studio.
Kiến thức bạn sẽ học được
- Giới thiệu các nội dung cơ bản 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ị. - Thuộc tính sao lưu trong Kotlin.
- Cách sử dụng
MaterialAlertDialog
trong thư viện Thành phần Material Design.
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.
Bạn cần có
- Một máy tính đã cài đặt Android Studio.
- Mã khởi động cho ứng dụng Unscramble.
2. Tổng quan về ứng dụng khởi độ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 này đưa ra một từ được xáo trộn tại một thời điểm và người chơi phải đoán từ đó dựa trên tất cả chữ cái của từ được xáo trộn. Nếu xếp từ chính xác, người chơi giành được điểm. Nếu không, người chơi có thể thử nhiều lần. Ứng dụng cũng có tuỳ chọn để bỏ qua từ hiện tại. Ở góc trên cùng bên trái, ứng dụng cho thấy số từ đã chơi trong phiên trò chơi hiện tại. Có 10 từ mỗi phiên trò chơi.
Tải đoạn mã khởi đầu xuống
Lớp học lập trình này cung cấp đoạn mã khởi đầu để bạn có thể mở rộng bằng các tính năng được dạy ở đây. Mã khởi động có thể chứa mã mà bạn đã biết và chưa biết từ các lớp học lập trình trước. Bạn sẽ tìm hiểu thêm về mã mới trong các lớp học lập trình về sau.
Nếu bạn sử dụng đoạn mã khởi đầu lấy trên GitHub, hãy lưu ý tên thư mục là android-basics-kotlin-unscramble-app-starter
. Hãy chọn thư mục này khi bạn mở dự án trong Android Studio.
- Chuyển đến trang kho lưu trữ GitHub được cung cấp cho dự án.
- Xác minh rằng tên nhánh khớp với tên 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.
- Trên trang GitHub cho dự án này, nhấp vào nút Code (Mã). Thao tác này sẽ khiến một cửa sổ bật lên.
- 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 (thường nằm trong thư mục Downloads (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ở thì chuyển sang chọn tuỳ chọn File (Tệp) > Open (Mở) trong 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 (thường 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 Chạy để tạo bản dựng và chạy ứng dụng. Đảm bảo ứng dụng được xây dựng như mong đợi.
Tổng quan về mã khởi động
- Mở dự án bằng đoạn 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.
- Chơi thử vài từ trong trò chơi, nhấn vào nút Submit (Gửi) và Skip (Bỏ qua). Hãy lưu ý rằng thao tác nhấn vào những nút này sẽ chuyển sang từ tiếp theo và tăng số lượng từ.
- Ngoài ra, điểm chỉ tăng khi bạn nhấn vào nút Submit (Gửi).
Vấn đề trong đoạn mã khởi đầu
Khi chơi trò chơi này, bạn có thể đã quan sát thấy các lỗi sau:
- Khi bạn nhấp vào nút Submit (Gửi), ứng dụng không kiểm tra từ của người chơi. Người chơi luôn giành được điểm.
- Không có cách nào để kết thúc trò chơi. Ứng dụng cho phép bạn chơi nhiều hơn 10 từ.
- Màn hình trò chơi cho thấy một từ được xáo trộn, điểm của người chơi và số từ. Thay đổi hướng màn hình bằng cách xoay thiết bị hoặc trình mô phỏng. Nhận thấy rằng từ, điểm và số từ hiện tại bị mất và trò chơi bắt đầu lại từ đầu.
Vấn đề chính trong ứng dụng
Ứng dụng khởi đầu này không lưu và khôi phục trạng thái cũng như dữ liệu của ứng dụng trong quá trình thay đổi cấu hình, chẳng hạn như khi hướng của thiết bị thay đổi.
Bạn có thể giải quyết vấn đề này bằng cách sử dụng lệnh gọi lại onSaveInstanceState()
. Tuy nhiên, để sử dụng phương thức onSaveInstanceState()
, bạn phải viết thêm mã để lưu trạng thái trong gói, đồng thời phải triển khai logic để truy xuất trạng thái đó. Ngoài ra, có thể lượng dữ liệu được lưu trữ là rất nhỏ.
Bạn có thể giải quyết những vấn đề này bằng cách sử dụng bộ thành phần cấu trúc Android mà bạn tìm hiểu được trong lộ trình này.
Tìm hiểu mã khởi động
Đoạn mã khởi đầu mà bạn tải xuống 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ẽ tập trung vào việc triển khai logic trò chơi. Bạn sẽ sử dụng các thành phần cấu trúc để triển khai cấu trúc ứng dụng đề xuất và giải quyết những vấn đề nêu trên. Dưới đây là hướng dẫn ngắn gọn về một số tệp để bạn bắt đầu.
game_fragment.xml
- Mở
res/layout/game_fragment.xml
trong khung hiển thị Design (Thiết kế). - Mã này chứa bố cục của màn hình duy nhất trong ứng dụng là màn hình trò chơi.
- Bố cục này chứa một trường văn bản cho từ của người chơi, cùng với
TextViews
để hiện điểm và số từ. Ngoài ra còn có hướng dẫn và nút (Submit (Gửi) và Skip (Bỏ qua)) để chơi trò chơi.
main_activity.xml
Xác định bố cục hoạt động chính bằng một mảnh trò chơi duy nhất.
thư mục res/values
Bạn đã làm quen với các tệp tài nguyên trong thư mục này.
colors.xml
chứa màu giao diện (theme) dùng trong ứng dụngstrings.xml
chứa tất cả chuỗi mà ứng dụng cần- Thư mục
themes
vàstyles
chứa nội dung tuỳ chỉnh giao diện người dùng cho ứng dụng
MainActivity.kt
Chứa mã được tạo mặc định theo mẫu để đặt khung hiển thị nội dung của hoạt động là main_activity.xml.
ListOfWords.kt
Tệp này chứa danh sách các từ được sử dụng trong trò chơi, cũng như 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.
GameFragment.kt
Đây là mảnh duy nhất trong ứng dụng nơi diễn ra hầu hết các thao tác của trò chơi:
- Các biến được định nghĩa cho từ được xáo trộn hiện tại (
currentScrambledWord
), số từ (currentWordCount
) và điểm (score
). - Xác định được quá trình liên kết thực thể đối tượng với quyền truy cập thành phần hiển thị
game_fragment
tên làbinding
. - Hàm
onCreateView()
tăng cường XML bố cụcgame_fragment
thông qua đối tượng liên kết. - Hàm
onViewCreated()
thiết lập trình nghe lượt nhấp vào nút và cập nhật giao diện người dùng. onSubmitWord()
là trình nghe lượt nhấp cho nút Submit (Gửi), chức năng này hiện từ được xáo trộn tiếp theo, xoá trường văn bản, đồng thời tăng điểm và số từ mà không cần xác thực từ của người chơi.onSkipWord()
là trình nghe lượt nhấp cho nút Skip (Bỏ qua), chức năng này cập nhật giao diện người dùng tương tự nhưonSubmitWord()
(ngoại trừ điểm).getNextScrambledWord()
là chức năng trợ giúp sẽ chọn một từ ngẫu nhiên trong danh sách từ và trộn các chữ cái trong từ đó.- Các chức năng
restartGame()
vàexitGame()
lần lượt được dùng để khởi động lại và kết thúc trò chơi. Bạn sẽ sử dụng các chức năng này sau. setErrorTextField()
xoá nội dung trường văn bản và đặt lại trạng thái lỗi.- Hàm
updateNextWordOnScreen()
hiện từ được xáo trộn mới.
3. Tìm hiểu về cấu trúc ứng dụng
Cấu trúc đưa ra hướng dẫn giúp bạn phân bổ trách nhiệm trong ứ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 tỷ lệ ứng dụng và mở rộng ứng dụng qua các tính năng bổ sung sau này. Điều này cũng giúp hoạt động cộng tác trong nhóm trở nên dễ dàng hơn.
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ế phân tách các vấn đề nêu rõ rằng bạn phải chia ứng dụng thành các lớp, 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
Một nguyên tắc quan trọng khác là 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ô 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 Views
và các thành phần ứng dụng trong ứng dụng, nên không bị ảnh hưởng bởi vòng đời của ứng dụng và các vấn đề liên quan.
Các lớp hoặc thành phần chính trong cấu trúc Android là Đơn vị điều khiển giao diện người dùng (hoạt động/mảnh), ViewModel
, LiveData
và Room
. Các thành phần này xử lý một số hoạt động phức tạp của vòng đời và giúp bạn tránh các vấn đề liên quan đến vòng đời. Bạn sẽ tìm hiểu về LiveData
và Room
trong các lớp học lập trình sau.
Sơ đồ này thể hiện một phần cơ bản của cấu trúc:
Bộ điều khiển giao diện người dùng (Hoạt động/Mảnh)
Hoạt động và mảnh là các đơn vị điều khiển giao diện người dùng. Đơn vị điều khiển giao diện người dùng kiểm soát giao diện người dùng bằng cách vẽ các thành phần hiển thị trên màn hình, ghi lại các sự kiện người dùng và mọi hoạt động khác liên quan đến giao diện người dùng mà người dùng tương tác. Dữ liệu trong ứng dụng hoặc logic quyết định bất kỳ về dữ liệu đó không được nằm trong các lớp đơn vị điều khiển giao diện người dùng.
Hệ thống Android có thể huỷ bỏ đơn vị điều khiển giao diện người dùng bất cứ lúc nào dựa trên một số hoạt động tương tác cụ thể của người dùng hoặc do các điều kiện liên quan đến hệ thống như bộ nhớ thấp. Vì các sự kiện này không thuộc quyền kiểm soát của bạn nên bạn không nên lưu trữ dữ liệu hoặc trạng thái ứng dụng trong đơn vị điều khiển giao diện người dùng. Thay vào đó, bạn nên thêm logic quyết định về dữ liệu vào ViewModel
.
Ví dụ: trong ứng dụng Unscramble, từ, điểm và số từ được xáo trộn sẽ xuất hiện trong một mảnh (đơn vị điều khiển giao diện người dùng). Mã quyết định (chẳng hạn như xác định từ được xáo trộn tiếp theo, các phép tính điểm và số từ) phải có trong ViewModel
.
ViewModel
ViewModel
là mô hình dữ liệu ứng dụng xuất hiện trong thành phần hiển thị. Mô hình là thành phần chịu trách nhiệm xử lý dữ liệu cho ứng dụng. Chúng cho phép ứng dụng 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ỷ khi hoạt động hoặc mảnh bị khung Android huỷ và tạo lại. Các đối tượng ViewModel
tự động được giữ lại (chúng không bị huỷ như thực thể hoạt động hoặc mảnh) trong quá trình thay đổi cấu hình. Nhờ vậy, dữ liệu mà chúng giữ lại có sẵn ngay cho thực thể hoạt động hoặc mảnh tiếp theo.
Để 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 đó.
Tóm tắt:
Trách nhiệm của mảnh/hoạt động (đơn vị điều khiển giao diện người dùng) | Trách nhiệm |
Hoạt động và mảnh chịu trách nhiệm vẽ thành phần hiển thị và dữ liệu ra màn hình và phản hồi sự kiện người dùng. |
|
4. Thêm ViewModel
Trong nhiệm vụ này, bạn thêm ViewModel
vào ứng dụng để lưu trữ dữ liệu ứng dụng (từ được xáo trộn, số từ và điểm).
Ứng dụng sẽ được cấu trúc theo cách sau. MainActivity
chứa GameFragment
và GameFragment
sẽ truy cập thông tin về trò chơi qua GameViewModel
.
- Trong cửa sổ Android của Android Studio thuộc thư mục Gradle Scripts (Tập lệnh Gradle), hãy mở tệp
build.gradle(Module:Unscramble.app)
. - Để sử dụng
ViewModel
trong ứng dụng, hãy xác minh rằng bạn có phần phụ thuộc thư viện ViewModel bên trong khốidependencies
. Bước này đã được thực hiện cho bạn. Tuỳ thuộc vào phiên bản thư viện mới nhất, số phiên bản thư viện trong mã đã tạo có thể khác nhau.
// ViewModel
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1'
Bạn nên luôn dùng phiên bản mới nhất của thư viện thay cho phiên bản được đề cập trong lớp học lập trình.
- Tạo tệp lớp Kotlin mới tên là
GameViewModel
. Trong cửa sổ Android, hãy nhấp chuột phải vào thư mục ui.game. Chọn New > Kotlin File/Class (Mới > Tệp/Lớp Kotlin).
- Đặt tên cho
GameViewModel
rồi chọn Class (Lớp) trong danh sách. - Thay đổi
GameViewModel
thành lớp con trênViewModel
.ViewModel
là lớp trừu tượng, do đó bạn cần mở rộng lớp này để sử dụng trong ứng dụng. Hãy xem định nghĩa về lớpGameViewModel
ở phần bên dưới.
class GameViewModel : ViewModel() {
}
Đính kèm ViewModel vào mảnh
Để liên kết ViewModel
với đơn vị điều khiển giao diện người dùng (hoạt động/mảnh), hãy tạo thuộc tính tham chiếu (đối tượng) cho ViewModel
bên trong đơn vị điều khiển giao diện người dùng.
Trong bước này, bạn tạo một thực thể đối tượng của GameViewModel
bên trong đơn vị điều khiển giao diện người dùng tương ứng, tức là GameFragment
.
- Ở đầu lớp
GameFragment
, hãy thêm một thuộc tính thuộc loạiGameViewModel
. - Khởi tạo
GameViewModel
bằng cách sử dụng tính năng uỷ quyền thuộc tínhby viewModels()
của Kotlin. Bạn sẽ tìm hiểu thêm về tính năng này trong phần tiếp theo.
private val viewModel: GameViewModel by viewModels()
- Nếu Android Studio nhắc, hãy nhập
androidx.fragment.app.viewModels
.
Tính năng uỷ quyền thuộc tính của Kotlin
Trong Kotlin, mỗi thuộc tính có thể biến đổi (var
) đều có các hàm getter và setter mặc định được tạo tự động cho mỗi thuộc tính. Hàm setter và getter được gọi khi bạn gán giá trị hoặc đọc giá trị của thuộc tính.
Đối với thuộc tính chỉ đọc (val
), các hàm này hơi khác với thuộc tính có thể biến đổi. Chỉ có hàm getter được tạo theo mặc định. Hàm getter này được gọi khi bạn đọc giá trị của thuộc tính chỉ đọc.
Tính năng uỷ quyền thuộc tính trong Kotlin giúp bạn chuyển giao trách nhiệm của hàm getter-setter cho một lớp khác.
Lớp này (được gọi là lớp uỷ quyền (delegate class)) cung cấp các hàm getter và setter của thuộc tính đó và xử lý các thay đổi trên thuộc tính đó.
Từng thuộc tính uỷ quyền được định nghĩa bằng mệnh đề by
và một thực thể của lớp uỷ quyền:
// Syntax for property delegation
var <property-name> : <property-type> by <delegate-class>()
Trong ứng dụng, nếu bạn khởi tạo mô hình hiển thị bằng hàm khởi tạo GameViewModel
mặc định, như minh hoạ dưới đây:
private val viewModel = GameViewModel()
Sau đó, ứng dụng sẽ xoá trạng thái của thuộc tính tham chiếu viewModel
khi thiết bị trải qua quá trình thay đổi cấu hình. Ví dụ: nếu bạn xoay thiết bị thì hoạt động sẽ bị huỷ và được tạo lại, nên bạn sẽ có thực thể mô hình hiển thị mới nhưng vẫn ở trạng thái ban đầu.
Thay vào đó, hãy sử dụng tính năng uỷ quyền thuộc tính và uỷ quyền trách nhiệm của đối tượng viewModel
cho một lớp riêng tên là viewModels
. Tức là khi bạn truy cập vào đối tượng viewModel
, đối tượng này sẽ được lớp uỷ quyền viewModels
xử lý trong nội bộ. Lớp uỷ quyền tạo đối tượng viewModel
cho bạn trong lần truy cập đầu tiên, đồng thời giữ lại giá trị của đối tượng này trong quá trình thay đổi cấu hình và trả về giá trị khi có yêu cầu.
5. Di chuyển dữ liệu sang ViewModel
Hoạt động tách dữ liệu giao diện người dùng của ứng dụng khỏi đơn vị điều khiển giao diện người dùng (các lớp Activity
/Fragment
) cho phép bạn tuân thủ tốt hơn nguyên tắc trách nhiệm duy nhất mà chúng ta đã thảo luận ở trên. Hoạt động và mảnh chịu trách nhiệm vẽ thành phần hiển thị và dữ liệu ra màn hình, trong khi ViewModel
chịu trách nhiệm giữ và xử lý tất cả dữ liệu cần thiết cho giao diện người dùng.
Trong nhiệm vụ này, bạn di chuyển các biến dữ liệu từ lớp GameFragment
sang lớp GameViewModel
.
- Di chuyển các biến dữ liệu
score
,currentWordCount
,currentScrambledWord
sang lớpGameViewModel
.
class GameViewModel : ViewModel() {
private var score = 0
private var currentWordCount = 0
private var currentScrambledWord = "test"
...
- Lưu ý lỗi về các thuộc tính tham chiếu chưa được giải quyết. Điều này là do các thuộc tính ở chế độ riêng tư đối với
ViewModel
và đơn vị điều khiển giao diện người dùng không thể truy cập được. Bạn sẽ khắc phục những lỗi này ở phần tiếp theo.
Để giải quyết vấn đề này, bạn không thể tạo các phương thức chỉ định truy cập cho thuộc tính public
– không nền dùng các lớp khác chỉnh sửa dữ liệu. Điều này rất rủi ro vì lớp bên ngoài có thể thay đổi dữ liệu theo những cách ngoài ý muốn, không tuân theo quy tắc trò chơi được chỉ định trong mô hình hiển thị. Ví dụ: lớp bên ngoài có thể thay đổi score
thành giá trị âm.
Bên trong ViewModel
, dữ liệu phải có thể chỉnh sửa được, vậy nên dữ liệu phải là private
và var
. Bên ngoài ViewModel
, dữ liệu phải có thể đọc được nhưng không thể chỉnh sửa được, vậy nên dữ liệu phải xuất hiện dưới dạng public
và val
. Để đạt được hành vi này, Kotlin có một tính năng gọi là thuộc tính sao lưu.
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.
Bạn đã biết rằng đối với mọi thuộc tính, 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 đè 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 bạn. Để 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ụ minh hoạ thuộc tính sao lưu:
// 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
Hãy xem xét một ví dụ, trong ứng dụng của bạn, 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
. Theo quy ước, cần sử dụng dấu gạch dưới cho tiền tố của thuộc tínhprivate
.
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ư đơn vị điều khiển giao diện người dùng. Vì chỉ có phương thứcget()
bị ghi đè nên thuộc tính này không thể biến đổi và chỉ có thể đọc. Khi một lớp bên ngoài truy cập thuộc tính này, thuộc tính này trả về giá trị_count
và bạn không thể sửa đổi được giá trị đó. Điề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 giá trị đó một cách an toàn.
Thêm thuộc tính sao lưu vào currentScrambledWord
- Trong
GameViewModel
, hãy thay đổi phần khai báocurrentScrambledWord
để thêm thuộc tính sao lưu. Hiện tại, bạn chỉ có thể truy cập và chỉnh sửa_currentScrambledWord
trongGameViewModel
. Đơn vị điều khiển giao diện người dùngGameFragment
có thể đọc giá trị đó bằng cách sử dụng thuộc tính chỉ có thể đọc làcurrentScrambledWord
.
private var _currentScrambledWord = "test"
val currentScrambledWord: String
get() = _currentScrambledWord
- Trong
GameFragment
, hãy cập nhật phương thứcupdateNextWordOnScreen()
để sử dụng thuộc tínhviewModel
chỉ có thể đọc làcurrentScrambledWord
.
private fun updateNextWordOnScreen() {
binding.textViewUnscrambledWord.text = viewModel.currentScrambledWord
}
- Trong
GameFragment
, hãy xoá mã này trong các phương thứconSubmitWord()
vàonSkipWord()
. Bạn sẽ triển khai các phương thức này sau. Giờ đây, bạn đã có thể biên dịch mã này mà không gặp lỗi nào.
6. Vòng đời của ViewModel
Khung này giúp ViewModel
vẫn hoạt động, miễn là phạm vi của hoạt động hoặc mảnh còn hoạt động. ViewModel
không bị huỷ nếu mô hình sở hữu thuộc tính này bị huỷ trong quá trình thay đổi cấu hình, chẳng hạn như tính năng xoay màn hình. Thực thể mới của mô hình sở hữu kết nối lại với thực thể ViewModel
hiện có, như minh hoạ trong sơ đồ sau:
Tìm hiểu vòng đời của ViewModel
Hãy thêm thông tin đăng nhập trong GameViewModel
và GameFragment
để giúp bạn hiểu rõ hơn về vòng đời của ViewModel
.
- Trong
GameViewModel.kt
, hãy thêm khốiinit
bằng câu lệnh nhật ký.
class GameViewModel : ViewModel() {
init {
Log.d("GameFragment", "GameViewModel created!")
}
...
}
Kotlin cung cấp một khối khởi tạo (còn gọi là khối init
) làm nơi chứa mã thiết lập ban đầu cần thiết trong quá trình khởi tạo thực thể đối tượng. Các khối khởi tạo có tiền tố là từ khoá init
, theo sau là dấu ngoặc nhọn {}
. Khối mã này được chạy khi thực thể đối tượng được tạo và khởi chạy lần đầu tiên.
- Trong lớp
GameViewModel
, hãy ghi đè phương thứconCleared()
.ViewModel
bị huỷ khi mảnh được liên kết bị tách rời hoặc khi hoạt động kết thúc. Ngay trước khiViewModel
bị huỷ, hệ thống sẽ thực hiện lệnh gọi lạionCleared()
. - Thêm câu lệnh nhật ký vào trong
onCleared()
để theo dõi vòng đời củaGameViewModel
.
override fun onCleared() {
super.onCleared()
Log.d("GameFragment", "GameViewModel destroyed!")
}
- Trong
GameFragment
bên trongonCreateView()
, sau khi bạn tham chiếu đến đối tượng liên kết, hãy thêm câu lệnh nhật ký để ghi lại quá trình tạo mảnh. Lệnh gọi lạionCreateView()
sẽ được kích hoạt khi mảnh được tạo lần đầu tiên và cũng được kích hoạt mỗi khi được tạo lại cho mọi sự kiện (chẳng hạn như thay đổi cấu hình).
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = GameFragmentBinding.inflate(inflater, container, false)
Log.d("GameFragment", "GameFragment created/re-created!")
return binding.root
}
- Trong
GameFragment
, hãy ghi đè phương thức gọi lạionDetach()
. Phương thức này sẽ được gọi khi hoạt động và mảnh tương ứng bị huỷ.
override fun onDetach() {
super.onDetach()
Log.d("GameFragment", "GameFragment destroyed!")
}
- Chạy ứng dụng này trong Android Studio, mở cửa sổ Logcat và lọc dữ liệu trên
GameFragment
. Hãy lưu ý rằngGameFragment
vàGameViewModel
đã được tạo.
com.example.android.unscramble D/GameFragment: GameFragment created/re-created! com.example.android.unscramble D/GameFragment: GameViewModel created!
- Bật chế độ tự động xoay trên thiết bị hoặc trình mô phỏng rồi thay đổi hướng màn hình một vài lần.
GameFragment
bị huỷ và tạo lại mỗi lần nhưngGameViewModel
chỉ được tạo một lần và sẽ không được tạo lại hoặc bị huỷ cho mỗi lần gọi.
com.example.android.unscramble D/GameFragment: GameFragment created/re-created! com.example.android.unscramble D/GameFragment: GameViewModel created! com.example.android.unscramble D/GameFragment: GameFragment destroyed! com.example.android.unscramble D/GameFragment: GameFragment created/re-created! com.example.android.unscramble D/GameFragment: GameFragment destroyed! com.example.android.unscramble D/GameFragment: GameFragment created/re-created! com.example.android.unscramble D/GameFragment: GameFragment destroyed! com.example.android.unscramble D/GameFragment: GameFragment created/re-created! com.example.android.unscramble D/GameFragment: GameFragment destroyed! com.example.android.unscramble D/GameFragment: GameFragment created/re-created!
- Thoát khỏi trò chơi hoặc thoát khỏi ứng dụng bằng cách sử dụng mũi tên quay lại.
GameViewModel
bị huỷ và hệ thống thực hiện lệnh gọi lạionCleared()
.GameFragment
bị huỷ.
com.example.android.unscramble D/GameFragment: GameViewModel destroyed! com.example.android.unscramble D/GameFragment: GameFragment destroyed!
7. Điền ViewModel
Trong nhiệm vụ này, bạn tiếp tục điền GameViewModel
bằng phương thức trợ giúp để nhận từ tiếp theo, xác thực từ của người chơi để tăng điểm và kiểm tra số từ để kết thúc trò chơi.
Khởi tạo trễ
Thông thường khi khai báo một biến, bạn sẽ cung cấp giá trị ban đầu cho biến đó. Tuy nhiên, nếu chưa sẵn sàng gán giá trị thì bạn có thể khởi tạo giá trị đó sau. Để khởi tạo trễ một thuộc tính trong Kotlin, bạn sử dụng từ khoá lateinit
, tức là khởi tạo trễ. Nếu đảm bảo việc khởi tạo thuộc tính trước khi sử dụng thì bạn có thể khai báo thuộc tính này bằng lateinit
. Bộ nhớ không được cấp cho biến cho đến khi biến được khởi tạo. Nếu bạn cố gắng truy cập vào biến trước khi khởi tạo biến đó, ứng dụng sẽ gặp sự cố.
Chuyển sang từ tiếp theo
Hãy tạo phương thức getNextWord()
trong lớp GameViewModel
bằng các chức năng sau:
- Lấy một từ ngẫu nhiên trên
allWordsList
và chỉ định từ đó chocurrentWord.
- Tạo một từ được xáo trộn bằng cách xáo trộn các chữ cái trong
currentWord
và gán từ đó chocurrentScrambledWord
- Xử lý trường hợp có từ được xáo trộn giống với từ không được xáo trộn.
- Đảm bảo bạn không đưa ra cùng một từ hai lần trong suốt trò chơi.
Thực hiện các bước sau trong lớp GameViewModel
:
- Trong
GameViewModel,
, hãy thêm một biến mới của lớp thuộc loạiMutableList<String>
tên làwordsList
để giữ danh sách các từ mà bạn sử dụng trong trò chơi nhằm tránh việc lặp lại. - Thêm một biến lớp khác tên là
currentWord
để chứa từ mà người chơi đang cố gắng xếp. Hãy sử dụng từ khoálateinit
vì bạn sẽ khởi tạo thuộc tính này sau.
private var wordsList: MutableList<String> = mutableListOf()
private lateinit var currentWord: String
- Thêm phương thức
private
mới tên làgetNextWord()
, ở trên khốiinit
, trong đó không có tham số nào trả về giá trị rỗng. - Lấy một từ ngẫu nhiên trên
allWordsList
và chỉ định từ đó chocurrentWord
private fun getNextWord() {
currentWord = allWordsList.random()
}
- Trong
getNextWord()
, hãy chuyển đổi chuỗicurrentWord
thành mảng ký tự và gán mảng ký tự đó choval
mới có tên làtempWord
. Để xáo trộn từ, hãy đảo các ký tự trong mảng này bằng phương thứcshuffle()
trong Kotlin.
val tempWord = currentWord.toCharArray()
tempWord.shuffle()
Array
tương tự như MutableList
nhưng có kích thước cố định khi khởi tạo. Array
không thể mở rộng hoặc thu nhỏ kích thước (bạn cần sao chép một mảng để đổi kích thước thuộc tính này), khác với MutableList
có hàm add()
và remove()
nên có thể tăng và giảm kích thước.
- Đôi khi, thứ tự ngẫu nhiên của các ký tự sẽ giống với từ gốc. Thêm vòng lặp
while
dưới đây xung quanh lệnh gọi để xáo trộn nhằm tiếp tục vòng lặp cho đến khi từ được xáo trộn khác với từ ban đầu.
while (String(tempWord).equals(currentWord, false)) {
tempWord.shuffle()
}
- Thêm khối
if-else
để kiểm tra xem một từ đã được dùng hay chưa. NếuwordsList
chứacurrentWord
, hãy gọigetNextWord()
. Nếu không, hãy cập nhật giá trị của_currentScrambledWord
bằng từ mới được xáo trộn, tăng số từ và thêm từ mới vàowordsList
.
if (wordsList.contains(currentWord)) {
getNextWord()
} else {
_currentScrambledWord = String(tempWord)
++currentWordCount
wordsList.add(currentWord)
}
- Sau đây là phương thức
getNextWord()
đã hoàn thành để bạn tham khảo.
/*
* Updates currentWord and currentScrambledWord with the next word.
*/
private fun getNextWord() {
currentWord = allWordsList.random()
val tempWord = currentWord.toCharArray()
tempWord.shuffle()
while (String(tempWord).equals(currentWord, false)) {
tempWord.shuffle()
}
if (wordsList.contains(currentWord)) {
getNextWord()
} else {
_currentScrambledWord = String(tempWord)
++currentWordCount
wordsList.add(currentWord)
}
}
Khởi tạo trễ currentScrambledWord
Bây giờ, bạn đã tạo phương thức getNextWord()
để lấy từ được xáo trộn tiếp theo. Bạn sẽ thực hiện lệnh gọi tới từ đó khi GameViewModel
được khởi tạo lần đầu tiên. Sử dụng khối init
để khởi tạo các thuộc tính lateinit
trong lớp, chẳng hạn như từ hiện tại. Kết quả là từ đầu tiên xuất hiện trên màn hình sẽ là một từ được xáo trộn thay vì test (kiểm thử).
- Chạy ứng dụng. Hãy lưu ý rằng từ đầu tiên luôn là "test" (kiểm thử).
- Để một từ được xáo trộn xuất hiện ở phần bắt đầu ứng dụng, bạn cần gọi phương thức
getNextWord()
, sau đó phương thức này sẽ cập nhậtcurrentScrambledWord
. Hãy gọi đến phương thứcgetNextWord()
bên trong khốiinit
củaGameViewModel
.
init {
Log.d("GameFragment", "GameViewModel created!")
getNextWord()
}
- Thêm đối tượng sửa đổi
lateinit
vào thuộc tính_currentScrambledWord
. Thêm thông tin đề cập rõ ràng về loại dữ liệuString
vì không có giá trị khởi tạo nào được cung cấp.
private lateinit var _currentScrambledWord: String
- Chạy ứng dụng. Hãy chú ý một từ được xáo trộn mới sẽ xuất hiện khi mở ứng dụng. Tuyệt vời!
Thêm phương thức trợ giúp
Tiếp theo, hãy thêm phương thức trợ giúp để xử lý và sửa đổi dữ liệu bên trong ViewModel
. Bạn sẽ sử dụng phương pháp này trong các nhiệm vụ sau.
- Trong lớp
GameViewModel
, hãy thêm một phương thức khác tên lànextWord().
Lấy từ tiếp theo trong danh sách rồi trả vềtrue
nếu số từ nhỏ hơnMAX_NO_OF_WORDS
.
/*
* Returns true if the current word count is less than MAX_NO_OF_WORDS.
* Updates the next word.
*/
fun nextWord(): Boolean {
return if (currentWordCount < MAX_NO_OF_WORDS) {
getNextWord()
true
} else false
}
8. Hộp thoại
Trong đoạn mã khởi đầu, trò chơi không bao giờ kết thúc, ngay cả sau khi người dùng đã giải xong 10 từ. Hãy sửa đổi ứng dụng để sau khi người dùng chơi xong 10 từ, trò chơi sẽ kết thúc và bạn sẽ cho thấy một hộp thoại có điểm số cuối cùng. Bạn cũng sẽ cung cấp cho người dùng tuỳ chọn chơi lại hoặc thoát khỏi trò chơi.
Đây là lần đầu tiên bạn thêm hộp thoại vào ứng dụng. Hộp thoại là cửa sổ nhỏ (màn hình) nhắc người dùng đưa ra quyết định hoặc nhập thông tin bổ sung. Thường thì 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
- Hộp thoại cảnh báo
- Tiêu đề (không bắt buộc)
- Thông báo
- Các nút văn bản
Triển khai hộp thoại điểm số cuối cùng
Sử dụng MaterialAlertDialog
trong thư viện Thành phần Material Design để thêm hộp thoại vào ứng dụng rồi tuân theo hướng dẫn Material. Vì hộp thoại có liên quan đến giao diện người dùng nên GameFragment
sẽ chịu trách nhiệm tạo và hiện hộp thoại điểm cuối cùng.
- Trước tiên, hãy thêm thuộc tính sao lưu vào biến
score
. TrongGameViewModel
, hãy thay đổi nội dung khai báo biếnscore
thành nội dung sau đây.
private var _score = 0
val score: Int
get() = _score
- Trong
GameFragment
, hãy thêm hàm riêng tư tên làshowFinalScoreDialog()
. Để tạoMaterialAlertDialog
, hãy sử dụng lớpMaterialAlertDialogBuilder
để thiết lập từng phần của hộp thoại theo từng bước. Gọi hàm khởi tạoMaterialAlertDialogBuilder
truyền vào nội dung bằng phương thứcrequireContext()
của mảnh. Phương thứcrequireContext()
trả về mộtContext
khác rỗng.
/*
* Creates and shows an AlertDialog with the final score.
*/
private fun showFinalScoreDialog() {
MaterialAlertDialogBuilder(requireContext())
}
Như tên gọi, Context
đề cập đến ngữ cảnh hoặc trạng thái hiện tại của một ứng dụng, hoạt động hoặc mảnh. Thuộc tính này chứa thông tin liên quan đến hoạt động, mảnh hoặc ứng dụng. Thường thì thuộc tính này dùng để truy cập các tài nguyên, cơ sở dữ liệu và các dịch vụ hệ thống khác. Trong bước này, bạn truyền ngữ cảnh vào mảnh để tạo hộp thoại thông báo.
Nếu Android Studio nhắc, import
com.google.android.material.dialog.MaterialAlertDialogBuilder
.
- Thêm mã để đặt tiêu đề trên hộp thoại thông báo, sử dụng tài nguyên chuỗi qua
strings.xml
.
MaterialAlertDialogBuilder(requireContext())
.setTitle(getString(R.string.congratulations))
- Thiết lập thông báo để hiện điểm cuối cùng, sử dụng phiên bản chỉ có thể đọc của biến điểm (
viewModel.score
) bạn đã thêm trước đó.
.setMessage(getString(R.string.you_scored, viewModel.score))
- Đảm bảo không huỷ được hộp thoại thông báo khi người dùng nhấn phím quay lại, bằng cách sử dụng phương thức
setCancelable()
và truyềnfalse
.
.setCancelable(false)
- Thêm hai nút văn bản EXIT (THOÁT) và PLAY AGAIN (CHƠI LẠI) bằng cách sử dụng các phương thức
setNegativeButton()
vàsetPositiveButton()
. GọiexitGame()
vàrestartGame()
lần lượt qua hàm lambda.
.setNegativeButton(getString(R.string.exit)) { _, _ ->
exitGame()
}
.setPositiveButton(getString(R.string.play_again)) { _, _ ->
restartGame()
}
Có thể bạn mới sử dụng cú pháp này, nhưng đây là cách viết tắt của setNegativeButton(getString(R.string.exit), { _, _ -> exitGame()})
, trong đó phương thức setNegativeButton()
nhận 2 tham số: String
và hàm DialogInterface.OnClickListener()
có thể được biểu thị dưới dạng lambda. Khi đối số cuối cùng được truyền vào là một hàm, bạn có thể đặt biểu thức lambda bên ngoài dấu ngoặc đơn. Đây gọi là cú pháp trailing lambda. Cả hai cách viết mã (với lambda bên trong hoặc bên ngoài dấu ngoặc đơn) đều được chấp nhận. Điều tương tự cũng áp dụng cho hàm setPositiveButton
.
- Ở cuối màn hình, hãy thêm
show()
để tạo rồi hiện hộp thoại thông báo.
.show()
- Sau đây là phương thức
showFinalScoreDialog()
hoàn chỉnh để bạn tham khảo.
/*
* Creates and shows an AlertDialog with the final score.
*/
private fun showFinalScoreDialog() {
MaterialAlertDialogBuilder(requireContext())
.setTitle(getString(R.string.congratulations))
.setMessage(getString(R.string.you_scored, viewModel.score))
.setCancelable(false)
.setNegativeButton(getString(R.string.exit)) { _, _ ->
exitGame()
}
.setPositiveButton(getString(R.string.play_again)) { _, _ ->
restartGame()
}
.show()
}
9. Triển khai OnClickListener cho nút Submit (Gửi)
Trong nhiệm vụ này, bạn sử dụng ViewModel
và hộp thoại cảnh báo bạn đã thêm để triển khai logic trò chơi cho trình xử lý lượt nhấp vào nút Submit (Gửi).
Hiện từ được xáo trộn
- Nếu bạn chưa thực hiện thì trong
GameFragment
, hãy xoá mã bên trongonSubmitWord()
được gọi khi người dùng nhấn vào nút Submit (Gửi). - Thêm quy trình kiểm tra giá trị trả về của phương thức
viewModel.nextWord()
. Nếutrue
thì tức là đang có một từ khác, hãy cập nhật từ được xáo trộn trên màn hình bằngupdateNextWordOnScreen()
. Ngược lại, trò chơi sẽ kết thúc, vậy nên hãy hiện hộp thoại thông báo có điểm số cuối cùng.
private fun onSubmitWord() {
if (viewModel.nextWord()) {
updateNextWordOnScreen()
} else {
showFinalScoreDialog()
}
}
- Chạy ứng dụng! Chơi thử một số từ. Hãy nhớ rằng bạn chưa triển khai nút Skip (Bỏ qua), vậy nên bạn không thể bỏ qua từ.
- Xin lưu ý rằng trường văn bản không được cập nhật, nên người chơi phải tự xoá từ trước đó. Điểm cuối cùng trong hộp thoại thông báo luôn bằng 0. Bạn sẽ sửa các lỗi này trong những bước tiếp theo.
Thêm phương thức trợ giúp để xác thực từ của người chơi
- Trong
GameViewModel
, hãy thêm một phương thức riêng tư mới tên làincreaseScore()
, trong đó không có tham số và không có giá trị trả về. Tăng biếnscore
bằngSCORE_INCREASE
.
private fun increaseScore() {
_score += SCORE_INCREASE
}
- Trong
GameViewModel
, hãy thêm một phương thức trợ giúp tên làisUserWordCorrect()
để trả vềBoolean
và lấyString
(từ của người chơi) làm tham số. - Trong
isUserWordCorrect()
, hãy xác thực từ của người chơi và cộng điểm nếu đoán đúng. Thao tác này sẽ cập nhật điểm cuối cùng trong hộp thoại thông báo.
fun isUserWordCorrect(playerWord: String): Boolean {
if (playerWord.equals(currentWord, true)) {
increaseScore()
return true
}
return false
}
Cập nhật trường văn bản
Hiện lỗi trong trường văn bản
Đối với trường văn bản Material, TextInputLayout
đi kèm với chức năng tích hợp để hiện thông báo lỗi. Ví dụ: trong trường văn bản sau, nhãn sẽ thay đổi màu, biểu tượng lỗi xuất hiện, thông báo lỗi xuất hiện, v.v.
Để hiện lỗi trong trường văn bản, bạn có thể thiết lập thông báo lỗi theo cách động trong mã hoặc theo cách tĩnh trong tệp bố cục. Sau đây là ví dụ về cách thiết lập và thiết lập lại lỗi:
// Set error text
passwordLayout.error = getString(R.string.error)
// Clear error text
passwordLayout.error = null
Trong đoạn mã khởi đầu, bạn sẽ thấy phương thức trợ giúp setErrorTextField(error: Boolean)
đã được xác định để giúp bạn thiết lập và thiết lập lại lỗi trong trường văn bản. Hãy gọi phương thức này bằng true
hoặc false
dưới dạng tham số đầu vào, tuỳ theo bạn có muốn hiện lỗi trong trường văn bản hay không.
Đoạn mã trong mã khởi đầu
private fun setErrorTextField(error: Boolean) {
if (error) {
binding.textField.isErrorEnabled = true
binding.textField.error = getString(R.string.try_again)
} else {
binding.textField.isErrorEnabled = false
binding.textInputEditText.text = null
}
}
Trong nhiệm vụ này, bạn triển khai phương thức onSubmitWord()
. Khi một từ được gửi, hãy xác thực lượt đoán của người dùng bằng cách kiểm tra từ gốc. Nếu từ đó chính xác, hãy chuyển sang từ tiếp theo (hoặc hiện hộp thoại nếu trò chơi đã kết thúc). Nếu từ đó không chính xác, hãy hiện lỗi trên trường văn bản và giữ nguyên từ hiện tại.
- Trong
GameFragment,
ở phần bắt đầu củaonSubmitWord()
, hãy tạoval
tên làplayerWord
. Lưu trữ từ của người chơi trong đó, bằng cách trích xuất từ qua trường văn bản trong biếnbinding
.
private fun onSubmitWord() {
val playerWord = binding.textInputEditText.text.toString()
...
}
- Trong
onSubmitWord()
, bên dưới phần khai báoplayerWord
, hãy xác thực từ của người chơi. Thêm câu lệnhif
để kiểm tra từ của người chơi bằng phương thứcisUserWordCorrect()
, truyền vàoplayerWord
. - Bên trong khối
if
, hãy thiết lập lại trường văn bản, gọisetErrorTextField
trongfalse
. - Di chuyển mã hiện có bên trong khối
if
.
private fun onSubmitWord() {
val playerWord = binding.textInputEditText.text.toString()
if (viewModel.isUserWordCorrect(playerWord)) {
setErrorTextField(false)
if (viewModel.nextWord()) {
updateNextWordOnScreen()
} else {
showFinalScoreDialog()
}
}
}
- Nếu từ của người dùng không chính xác, hãy hiện thông báo lỗi trong trường văn bản. Thêm một khối
else
vào khốiif
ở trên và gọisetErrorTextField()
truyền vàotrue
. Phương thứconSubmitWord()
hoàn tất sẽ có dạng như sau:
private fun onSubmitWord() {
val playerWord = binding.textInputEditText.text.toString()
if (viewModel.isUserWordCorrect(playerWord)) {
setErrorTextField(false)
if (viewModel.nextWord()) {
updateNextWordOnScreen()
} else {
showFinalScoreDialog()
}
} else {
setErrorTextField(true)
}
}
- Chạy ứng dụng. Chơi thử một số từ. Nếu từ của người chơi là chính xác, từ đó sẽ được xoá khi nhấp vào nút Submit (Gửi). Nếu không, thông báo với nội dung "Try again!" (Hãy thử lại!) sẽ xuất hiện. Hãy lu ý rằng nút Skip (Bỏ qua) vẫn chưa hoạt động. Bạn sẽ thêm cách triển khai này trong nhiệm vụ tiếp theo.
10. Triển khai nút Skip (Bỏ qua)
Trong nhiệm vụ này, bạn thêm cách triển khai cho onSkipWord()
để xử lý khi người dùng nhấp vào nút Skip (Bỏ qua).
- Tương tự như
onSubmitWord()
, hãy thêm một điều kiện vào phương thứconSkipWord()
. Nếutrue
, hãy hiện từ đó trên màn hình và thiết lập lại trường văn bản. Nếufalse
và không còn từ nào ở vòng này, hãy hiện hộp thoại thông báo có điểm cuối cùng.
/*
* Skips the current word without changing the score.
*/
private fun onSkipWord() {
if (viewModel.nextWord()) {
setErrorTextField(false)
updateNextWordOnScreen()
} else {
showFinalScoreDialog()
}
}
- Chạy ứng dụng. Chơi trò chơi. Lưu ý rằng nút Skip (Bỏ qua) và Submit (Gửi) đang hoạt động như dự kiến. Tuyệt vời!
11. Xác minh ViewModel lưu giữ dữ liệu
Đối với nhiệm vụ này, hãy thêm tính năng ghi nhật ký vào GameFragment
để ghi nhận rằng dữ liệu ứng dụng được lưu giữ trong ViewModel
khi thay đổi cấu hình. Để truy cập currentWordCount
trong GameFragment
, bạn cần hiện phiên bản chỉ có thể đọc bằng cách sử dụng thuộc tính sao lưu.
- Trong
GameViewModel
, hãy nhấp chuột phải vào biếncurrentWordCount
, chọn Refactor (Tái cấu trúc) > Rename… (Đổi tên…) . Đặt tiền tố cho tên mới bằng dấu gạch dưới,_currentWordCount
. - Thêm trường sao lưu.
private var _currentWordCount = 0
val currentWordCount: Int
get() = _currentWordCount
- Trong
GameFragment
bên trongonCreateView()
, phía trên câu lệnh trả về, hãy thêm một nhật ký khác để in dữ liệu ứng dụng, từ, điểm và số từ.
Log.d("GameFragment", "Word: ${viewModel.currentScrambledWord} " +
"Score: ${viewModel.score} WordCount: ${viewModel.currentWordCount}")
- Trong Android Studio, hãy mở Logcat, lọc theo
GameFragment
. Chạy ứng dụng và chơi thử một số từ. Thay đổi hướng của thiết bị. Mảnh (đơn vị điều khiển giao diện người dùng) đã bị huỷ rồi tạo lại. Quan sát nhật ký. Giờ đây, bạn có thể thấy điểm và số từ tăng lên!
com.example.android.unscramble D/GameFragment: GameFragment created/re-created! com.example.android.unscramble D/GameFragment: GameViewModel created! com.example.android.unscramble D/GameFragment: Word: oimfnru Score: 0 WordCount: 1 com.example.android.unscramble D/GameFragment: GameFragment destroyed! com.example.android.unscramble D/GameFragment: GameFragment created/re-created! com.example.android.unscramble D/GameFragment: Word: ofx Score: 80 WordCount: 5 com.example.android.unscramble D/GameFragment: GameFragment destroyed! com.example.android.unscramble D/GameFragment: GameFragment created/re-created! com.example.android.unscramble D/GameFragment: Word: ofx Score: 80 WordCount: 5 com.example.android.unscramble D/GameFragment: GameFragment destroyed! com.example.android.unscramble D/GameFragment: GameFragment created/re-created! com.example.android.unscramble D/GameFragment: Word: nvoiil Score: 160 WordCount: 9 com.example.android.unscramble D/GameFragment: GameFragment destroyed! com.example.android.unscramble D/GameFragment: GameFragment created/re-created! com.example.android.unscramble D/GameFragment: Word: nvoiil Score: 160 WordCount: 9
Hãy lưu ý rằng dữ liệu ứng dụng được lưu giữ trong ViewModel
trong khi thay đổi hướng. Bạn sẽ cập nhật giá trị điểm và số từ trong giao diện người dùng bằng cách sử dụng LiveData
và liên kết dữ liệu trong các lớp học lập trình sau này.
12. Cập nhật logic khởi động lại trò chơi
- Chạy lại ứng dụng, chơi hết tất cả các từ. Trong hộp thoại thông báo Congratulations! (Xin chúc mừng!), hãy nhấp vào PLAY AGAIN (CHƠI LẠI). Ứng dụng sẽ không cho phép bạn chơi lại vì số từ hiện đã đạt đến giá trị
MAX_NO_OF_WORDS
. Bạn cần thiết lập lại số từ thành 0 để chơi lại trò chơi từ đầu. - Để thiết lập lại dữ liệu ứng dụng, trong
GameViewModel
, hãy thêm một phương thức tên làreinitializeData()
. Thiết lập điểm và số từ thành0
. Xoá danh sách từ và gọi phương thứcgetNextWord()
.
/*
* Re-initializes the game data to restart the game.
*/
fun reinitializeData() {
_score = 0
_currentWordCount = 0
wordsList.clear()
getNextWord()
}
- Trong
GameFragment
, ở đầu phương thứcrestartGame()
, hãy gọi đến phương thức mới tạoreinitializeData()
.
private fun restartGame() {
viewModel.reinitializeData()
setErrorTextField(false)
updateNextWordOnScreen()
}
- Chạy lại ứng dụng. Chơi trò chơi. Khi bạn đến hộp thoại chúc mừng, hãy nhấp vào Play Again (Chơi lại). Giờ đây, bạn đã có thể chơi lại trò chơi thành công!
Cuối cùng, ứng dụng sẽ có dạng như dưới đây. Trò chơi hiện 10 được xáo trộn ngẫu nhiên để người chơi xếp lại theo thứ tự đúng. Bạn có thể Skip (Bỏ qua) từ đó hoặc đoán từ rồi nhấn vào Submit (Gửi). Nếu bạn đoán chính xác thì điểm sẽ tăng lên. Nếu bạn đoán không chính xác thì trạng thái lỗi trong trường văn bản sẽ xuất hiện. Với mỗi từ mới, số từ cũng sẽ tăng lên.
Xin lưu ý rằng điểm và số từ xuất hiện trên màn hình vẫn chưa được cập nhật. Tuy nhiên, thông tin vẫn được lưu trữ trong mô hình hiển thị và được lưu giữ trong khi thay đổi cấu hình (chẳng hạn như xoay thiết bị). Bạn sẽ cập nhật điểm và số từ trên màn hình trong các lớp học lập trình sau này.
Khi kết thúc 10 từ, trò chơi sẽ kết thúc và hộp thoại thông báo sẽ bật lên, trong đó có điểm cuối cùng cũng như tuỳ chọn thoát khỏi trò chơi hoặc chơi lại.
Xin chúc mừng! Bạn đã tạo ViewModel
đầu tiên và đã lưu dữ liệu!
13. Mã giải pháp
GameFragment.kt
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import com.example.android.unscramble.R
import com.example.android.unscramble.databinding.GameFragmentBinding
import com.google.android.material.dialog.MaterialAlertDialogBuilder
/**
* Fragment where the game is played, contains the game logic.
*/
class GameFragment : Fragment() {
private val viewModel: GameViewModel by viewModels()
// Binding object instance with access to the views in the game_fragment.xml layout
private lateinit var binding: GameFragmentBinding
// Create a ViewModel the first time the fragment is created.
// If the fragment is re-created, it receives the same GameViewModel instance created by the
// first fragment
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
// Inflate the layout XML file and return a binding object instance
binding = GameFragmentBinding.inflate(inflater, container, false)
Log.d("GameFragment", "GameFragment created/re-created!")
Log.d("GameFragment", "Word: ${viewModel.currentScrambledWord} " +
"Score: ${viewModel.score} WordCount: ${viewModel.currentWordCount}")
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// Setup a click listener for the Submit and Skip buttons.
binding.submit.setOnClickListener { onSubmitWord() }
binding.skip.setOnClickListener { onSkipWord() }
// Update the UI
updateNextWordOnScreen()
binding.score.text = getString(R.string.score, 0)
binding.wordCount.text = getString(
R.string.word_count, 0, MAX_NO_OF_WORDS)
}
/*
* Checks the user's word, and updates the score accordingly.
* Displays the next scrambled word.
* After the last word, the user is shown a Dialog with the final score.
*/
private fun onSubmitWord() {
val playerWord = binding.textInputEditText.text.toString()
if (viewModel.isUserWordCorrect(playerWord)) {
setErrorTextField(false)
if (viewModel.nextWord()) {
updateNextWordOnScreen()
} else {
showFinalScoreDialog()
}
} else {
setErrorTextField(true)
}
}
/*
* Skips the current word without changing the score.
*/
private fun onSkipWord() {
if (viewModel.nextWord()) {
setErrorTextField(false)
updateNextWordOnScreen()
} else {
showFinalScoreDialog()
}
}
/*
* Gets a random word for the list of words and shuffles the letters in it.
*/
private fun getNextScrambledWord(): String {
val tempWord = allWordsList.random().toCharArray()
tempWord.shuffle()
return String(tempWord)
}
/*
* Creates and shows an AlertDialog with the final score.
*/
private fun showFinalScoreDialog() {
MaterialAlertDialogBuilder(requireContext())
.setTitle(getString(R.string.congratulations))
.setMessage(getString(R.string.you_scored, viewModel.score))
.setCancelable(false)
.setNegativeButton(getString(R.string.exit)) { _, _ ->
exitGame()
}
.setPositiveButton(getString(R.string.play_again)) { _, _ ->
restartGame()
}
.show()
}
/*
* Re-initializes the data in the ViewModel and updates the views with the new data, to
* restart the game.
*/
private fun restartGame() {
viewModel.reinitializeData()
setErrorTextField(false)
updateNextWordOnScreen()
}
/*
* Exits the game.
*/
private fun exitGame() {
activity?.finish()
}
override fun onDetach() {
super.onDetach()
Log.d("GameFragment", "GameFragment destroyed!")
}
/*
* Sets and resets the text field error status.
*/
private fun setErrorTextField(error: Boolean) {
if (error) {
binding.textField.isErrorEnabled = true
binding.textField.error = getString(R.string.try_again)
} else {
binding.textField.isErrorEnabled = false
binding.textInputEditText.text = null
}
}
/*
* Displays the next scrambled word on screen.
*/
private fun updateNextWordOnScreen() {
binding.textViewUnscrambledWord.text = viewModel.currentScrambledWord
}
}
GameViewModel.kt
import android.util.Log
import androidx.lifecycle.ViewModel
/**
* ViewModel containing the app data and methods to process the data
*/
class GameViewModel : ViewModel(){
private var _score = 0
val score: Int
get() = _score
private var _currentWordCount = 0
val currentWordCount: Int
get() = _currentWordCount
private lateinit var _currentScrambledWord: String
val currentScrambledWord: String
get() = _currentScrambledWord
// List of words used in the game
private var wordsList: MutableList<String> = mutableListOf()
private lateinit var currentWord: String
init {
Log.d("GameFragment", "GameViewModel created!")
getNextWord()
}
override fun onCleared() {
super.onCleared()
Log.d("GameFragment", "GameViewModel destroyed!")
}
/*
* Updates currentWord and currentScrambledWord with the next word.
*/
private fun getNextWord() {
currentWord = allWordsList.random()
val tempWord = currentWord.toCharArray()
tempWord.shuffle()
while (String(tempWord).equals(currentWord, false)) {
tempWord.shuffle()
}
if (wordsList.contains(currentWord)) {
getNextWord()
} else {
_currentScrambledWord = String(tempWord)
++_currentWordCount
wordsList.add(currentWord)
}
}
/*
* Re-initializes the game data to restart the game.
*/
fun reinitializeData() {
_score = 0
_currentWordCount = 0
wordsList.clear()
getNextWord()
}
/*
* Increases the game score if the player's word is correct.
*/
private fun increaseScore() {
_score += SCORE_INCREASE
}
/*
* Returns true if the player word is correct.
* Increases the score accordingly.
*/
fun isUserWordCorrect(playerWord: String): Boolean {
if (playerWord.equals(currentWord, true)) {
increaseScore()
return true
}
return false
}
/*
* Returns true if the current word count is less than MAX_NO_OF_WORDS
*/
fun nextWord(): Boolean {
return if (_currentWordCount < MAX_NO_OF_WORDS) {
getNextWord()
true
} else false
}
}
14. Tóm tắt
- Theo hướng dẫn về cấu trúc ứng dụng Android, bạn nên 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.
- Đơn vị điều khiển giao diện người dùng là lớp dựa trên giao diện người dùng (UI) như
Activity
hoặcFragment
. Đơn vị điều khiển giao diện người dùng chỉ được chứa logic xử lý các thao tác trên giao diện người dùng và hệ điều hành; chúng không được là nguồn dữ liệu xuất hiện trên giao diện người dùng. Hãy đặt dữ liệu đó và mọi logic có liên quan vàoViewModel
. - Lớp
ViewModel
lưu trữ và quản lý dữ liệu liên quan đến giao diện người dùng. LớpViewModel
duy trì dữ liệu sau các thay đổi về cấu hình, chẳng hạn như xoay màn hình. ViewModel
là một trong những bộ thành phần cấu trúc Android đề xuất.
15. Tìm hiểu thêm
- Tổng quan về ViewModel
- Hướng dẫn về cấu trúc ứng dụng
- Trải nghiệm thực tế về thành phần Material cho Android: Hộp thoại
- Cấu trúc hộp thoại cảnh báo
- MaterialAlertDialogBuilder
- Thuộc tính sao lưu
- Bộ thành phần cấu trúc Android
- Hộp thoại Material trên Android
- Thuộc tính và trường: Phương thức getter, setter, const, lateinit