Lớp học lập trình này thuộc khoá học Kiến thức cơ bản về Kotlin cho Android. Bạn sẽ nhận được nhiều giá trị nhất qua khoá học này nếu thực hiện các lớp học lập trình theo trình tự. Tất cả lớp học lập trình của khoá học đều được liệt kê trên trang đích của lớp học lập trình Kiến thức cơ bản về cách tạo ứng dụng Android bằng Kotlin.
Màn hình tiêu đề | Màn hình trò chơi | Màn hình điểm số |
Giới thiệu
Trong lớp học lập trình này, bạn sẽ tìm hiểu về một trong các Thành phần cấu trúc Android, ViewModel
:
- Bạn sử dụng lớp
ViewModel
để lưu trữ và quản lý dữ liệu liên quan đến giao diện người dùng theo cách nhận biết vòng đời. LớpViewModel
duy trì dữ liệu sau các thay đổi về cấu hình thiết bị, chẳng hạn như xoay màn hình và thay đổi về khả năng sử dụng bàn phím. - Bạn dùng lớp
ViewModelFactory
để khởi tạo và trả về đối tượngViewModel
vẫn tồn tại sau khi thay đổi cấu hình.
Kiến thức bạn cần có
- Cách tạo các ứng dụng Android cơ bản trong Kotlin.
- Cách sử dụng biểu đồ điều hướng để triển khai tính năng điều hướng trong ứng dụng.
- Cách thêm mã để di chuyển giữa các đích đến của ứng dụng và truyền dữ liệu giữa các đích đến điều hướng.
- Cách vòng đời hoạt động và mảnh hoạt động.
- 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
- Cách sử dụng cấu trúc ứng dụng Android được đề xuất.
- Cách sử dụng các lớp
Lifecycle
,ViewModel
vàViewModelFactory
trong ứng dụng của bạn. - Cách 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ị.
- Mẫu thiết kế phương thức nhà máy là gì và cách sử dụng mẫu này.
- Cách tạo đối tượng
ViewModel
bằng giao diệnViewModelProvider.Factory
.
Bạn sẽ thực hiện
- Thêm
ViewModel
vào ứng dụng để lưu dữ liệu của ứng dụng, nhờ đó dữ liệu vẫn tồn tại sau khi thay đổi cấu hình. - Sử dụng
ViewModelFactory
và mẫu thiết kế phương thức của nhà máy để tạo một đối tượngViewModel
bằng các tham số của hàm khởi tạo.
Trong lớp học lập trình Bài 5, bạn sẽ phát triển ứng dụng GuessTheWord, bắt đầu bằng mã khởi động. GuessTheWord là một trò chơi đố chữ dành cho 2 người chơi, trong đó người chơi hợp tác với nhau để đạt điểm cao nhất có thể.
Người chơi thứ nhất nhìn vào các từ trong ứng dụng và lần lượt diễn tả từng từ, nhớ không cho người chơi thứ hai thấy từ đó. Người chơi thứ hai cố gắng đoán từ.
Để chơi trò chơi, người chơi đầu tiên mở ứng dụng trên thiết bị và thấy một từ, chẳng hạn như "guitar", như trong ảnh chụp màn hình bên dưới.
Người chơi đầu tiên diễn tả từ đó, cẩn thận không nói ra từ đó.
- Khi người chơi thứ hai đoán đúng từ, người chơi thứ nhất sẽ nhấn nút Got It (Đã hiểu). Thao tác này sẽ tăng số lượng từ lên một và cho thấy từ tiếp theo.
- Nếu người chơi thứ hai không đoán được từ, người chơi thứ nhất sẽ nhấn vào nút Bỏ qua. Thao tác này sẽ giảm số lượt đoán đi một và chuyển sang từ tiếp theo.
- Để kết thúc trò chơi, hãy nhấn vào nút Kết thúc trò chơi. (Chức năng này không có trong mã khởi đầu của lớp học lập trình đầu tiên trong loạt lớp học lập trình này.)
Trong nhiệm vụ này, bạn sẽ tải và chạy ứng dụng khởi đầu, đồng thời kiểm tra mã.
Bước 1: Bắt đầu
- Tải mã khởi đầu GuessTheWord xuống rồi mở dự án trong Android Studio.
- Chạy ứng dụng trên thiết bị chạy Android hoặc trên trình mô phỏng.
- Nhấn vào các nút. Lưu ý rằng nút Skip (Bỏ qua) sẽ chuyển sang từ tiếp theo và giảm điểm đi 1, còn nút Got It (Tôi hiểu) sẽ chuyển sang từ tiếp theo và tăng điểm thêm 1. Nút End Game (Kết thúc trò chơi) chưa được triển khai, nên sẽ không có gì xảy ra khi bạn nhấn vào nút này.
Bước 2: Thực hiện quy trình xem xét mã
- Trong Android Studio, hãy khám phá đoạn mã để hiểu cách ứng dụng hoạt động.
- Hãy nhớ xem các tệp được mô tả bên dưới, vì đây là những tệp đặc biệt quan trọng.
MainActivity.kt
Tệp này chỉ chứa mã mặc định được tạo theo mẫu.
res/layout/main_activity.xml
Tệp này chứa bố cục chính của ứng dụng. NavHostFragment
lưu trữ các mảnh khác khi người dùng di chuyển trong ứng dụng.
Mảnh giao diện người dùng
Mã khởi đầu có 3 mảnh trong 3 gói khác nhau trong gói com.example.android.guesstheword.screens
:
title/TitleFragment
cho màn hình tiêu đềgame/GameFragment
cho màn hình trò chơiscore/ScoreFragment
cho màn hình điểm số
screens/title/TitleFragment.kt
Mảnh tiêu đề là màn hình đầu tiên xuất hiện khi ứng dụng khởi chạy. Trình xử lý lượt nhấp được đặt thành nút Play (Phát) để chuyển đến màn hình trò chơi.
screens/game/GameFragment.kt
Đây là mảnh chính, nơi diễn ra hầu hết các thao tác của trò chơi:
- Các biến được xác định cho từ hiện tại và điểm số hiện tại.
wordList
được xác định bên trong phương thứcresetList()
là danh sách mẫu gồm các từ sẽ được dùng trong trò chơi.- Phương thức
onSkip()
là trình xử lý lượt nhấp cho nút Skip (Bỏ qua). Thao tác này sẽ giảm điểm số đi 1, sau đó hiển thị từ tiếp theo bằng phương thứcnextWord()
. - Phương thức
onCorrect()
là trình xử lý lượt nhấp cho nút Đã hiểu. Phương thức này được triển khai tương tự như phương thứconSkip()
. Điểm khác biệt duy nhất là phương thức này sẽ cộng 1 vào điểm số thay vì trừ đi.
screens/score/ScoreFragment.kt
ScoreFragment
là màn hình cuối cùng trong trò chơi và cho biết điểm số cuối cùng của người chơi. Trong lớp học lập trình này, bạn sẽ thêm phương thức triển khai để hiển thị màn hình này và cho biết điểm số cuối cùng.
res/navigation/main_navigation.xml
Biểu đồ điều hướng cho biết cách các mảnh được kết nối thông qua hoạt động điều hướng:
- Từ mảnh tiêu đề, người dùng có thể chuyển đến mảnh trò chơi.
- Từ phân mảnh trò chơi, người dùng có thể chuyển đến phân mảnh điểm số.
- Từ mảnh điểm số, người dùng có thể quay lại mảnh trò chơi.
Trong nhiệm vụ này, bạn sẽ tìm thấy các vấn đề với ứng dụng khởi đầu GuessTheWord.
- Chạy đoạn mã khởi đầu và chơi thử vài từ trong trò chơi, nhấn vào Skip (Bỏ qua) hoặc Got It (Tôi hiểu) sau mỗi từ.
- Màn hình trò chơi hiện cho thấy một từ và điểm số hiện tại. Thay đổi hướng màn hình bằng cách xoay thiết bị hoặc trình mô phỏng. Lưu ý rằng điểm số hiện tại sẽ bị mất.
- Chơi trò chơi với một vài từ nữa. Khi màn hình trò chơi hiển thị một số điểm, hãy đóng rồi mở lại ứng dụng. Lưu ý rằng trò chơi sẽ bắt đầu lại từ đầu vì trạng thái ứng dụng không được lưu.
- Chơi thử vài từ trong trò chơi, sau đó nhấn vào nút End Game (Kết thúc trò chơi). Lưu ý rằng không có điều gì xảy ra.
Vấn đề 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ủ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 hoặc khi ứng dụng tắt rồi khởi động lại.
Bạn có thể giải quyết vấn đề này bằng lệnh gọi lạionSaveInstanceState()
. Tuy nhiên, để sử dụng phương thứconSaveInstanceState()
, bạn phải viết thêm mã để lưu trạng thái trong một 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ỏ. - Màn hình trò chơi không chuyển đến màn hình điểm số khi người dùng nhấn vào nút Kết thúc trò chơi.
Bạn có thể giải quyết những vấn đề này bằng cách sử dụng các thành phần cấu trúc ứng dụng mà bạn tìm hiểu được trong lớp học lập trình này.
Cấu trúc ứng dụng
Cấu trúc ứng dụng là một cách thiết kế các lớp của ứng dụng và mối quan hệ giữa các lớp đó, sao cho mã được sắp xếp, hoạt động hiệu quả trong các trường hợp cụ thể và dễ sử dụng. Trong bộ gồm 4 lớp học lập trình này, những điểm cải tiến mà bạn thực hiện cho ứng dụng Đoán từ tuân theo các nguyên tắc về cấu trúc ứng dụng Android và bạn sử dụng Các thành phần trong cấu trúc Android. Cấu trúc ứng dụng Android tương tự như mẫu cấu trúc MVVM (model-view-viewmodel).
Ứng dụng GuessTheWord tuân theo nguyên tắc thiết kế phân tách các vấn đề và được chia thành các lớp, trong đó mỗi lớp giải quyết một vấn đề riêng biệt. Trong lớp học lập trình đầu tiên của bài học này, các lớp mà bạn làm việc là một bộ điều khiển giao diện người dùng, một ViewModel
và một ViewModelFactory
.
Bộ điều khiển giao diện người dùng
Đơn vị điều khiển giao diện người dùng là một lớp dựa trên giao diện người dùng, chẳng hạn như Activity
hoặc Fragment
. Đơ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 hạn như hiển thị các thành phần hiển thị và ghi lại dữ liệu đầu vào của người dùng. Đừng đưa logic quyết định (chẳng hạn như logic xác định văn bản cần hiển thị) vào đơn vị điều khiển giao diện người dùng.
Trong mã khởi động GuessTheWord, các đơn vị điều khiển giao diện người dùng là 3 mảnh: GameFragment
, ScoreFragment,
và TitleFragment
. Theo nguyên tắc thiết kế "phân tách các mối quan tâm", GameFragment
chỉ chịu trách nhiệm vẽ các phần tử trò chơi lên màn hình và biết thời điểm người dùng nhấn vào các nút, chứ không làm gì khác. Khi người dùng nhấn vào một nút, thông tin này sẽ được truyền đến GameViewModel
.
ViewModel
ViewModel
giữ dữ liệu cần hiển thị trong một mảnh hoặc hoạt động liên kết với ViewModel
. ViewModel
có thể thực hiện các phép tính toán và biến đổi đơn giản đối với dữ liệu để chuẩn bị cho trình kiểm soát giao diện người dùng hiển thị dữ liệu đó. Trong cấu trúc này, ViewModel
thực hiện việc đưa ra quyết định.GameViewModel
lưu giữ dữ liệu như giá trị điểm số, danh sách từ và từ hiện tại, vì đây là dữ liệu sẽ xuất hiện trên màn hình. GameViewModel
cũng chứa logic nghiệp vụ để thực hiện các phép tính đơn giản nhằm xác định trạng thái hiện tại của dữ liệu.
ViewModelFactory
ViewModelFactory
khởi tạo các đối tượng ViewModel
, có hoặc không có tham số hàm khởi tạo.
Trong các lớp học lập trình sau này, bạn sẽ tìm hiểu về các Thành phần kiến trúc Android khác có liên quan đến bộ điều khiển giao diện người dùng và ViewModel
.
Lớp ViewModel
được thiết kế để lưu trữ và quản lý dữ liệu liên quan đến giao diện người dùng. Trong ứng dụng này, mỗi ViewModel
được liên kết với một mảnh.
Trong nhiệm vụ này, bạn sẽ thêm ViewModel
đầu tiên vào ứng dụng, đó là GameViewModel
cho GameFragment
. Bạn cũng sẽ tìm hiểu ý nghĩa của việc ViewModel
nhận biết được vòng đời.
Bước 1: Thêm lớp GameViewModel
- Mở tệp
build.gradle(module:app)
. Bên trong khốidependencies
, hãy thêm phần phụ thuộc Gradle choViewModel
.
Nếu bạn dùng phiên bản mới nhất của thư viện, thì ứng dụng giải pháp sẽ biên dịch như dự kiến. Nếu không, hãy thử giải quyết vấn đề hoặc quay lại phiên bản bên dưới.
//ViewModel
implementation 'androidx.lifecycle:lifecycle-extensions:2.0.0'
- Trong thư mục gói
screens/game/
, hãy tạo một lớp Kotlin mới có tên làGameViewModel
. - Đặt lớp
GameViewModel
mở rộng lớp trừu tượngViewModel
. - Để giúp bạn hiểu rõ hơn về cách
ViewModel
nhận biết vòng đời, hãy thêm một khốiinit
bằng câu lệnhlog
.
class GameViewModel : ViewModel() {
init {
Log.i("GameViewModel", "GameViewModel created!")
}
}
Bước 2: Ghi đè onCleared() và thêm tính năng ghi nhật ký
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 khi ViewModel
bị huỷ, hệ thống sẽ thực hiện lệnh gọi lại onCleared()
để dọn dẹp các tài nguyên.
- Trong lớp
GameViewModel
, hãy ghi đè phương thứconCleared()
. - 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.i("GameViewModel", "GameViewModel destroyed!")
}
Bước 3: Liên kết GameViewModel với mảnh trò chơi
Bạn cần liên kết ViewModel
với một bộ điều khiển giao diện người dùng. Để liên kết hai thành phần này, bạn sẽ tạo một thuộc tính tham chiếu đến 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 thuộc tính tham chiếu 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
.
- Trong lớp
GameFragment
, hãy thêm một trường thuộc loạiGameViewModel
ở cấp cao nhất dưới dạng một biến lớp.
private lateinit var viewModel: GameViewModel
Bước 4: Khởi tạo ViewModel
Trong quá trình thay đổi cấu hình (chẳng hạn như xoay màn hình), các bộ điều khiển giao diện người dùng (chẳng hạn như mảnh) sẽ được tạo lại. Tuy nhiên, các thực thể ViewModel
vẫn tồn tại. Nếu bạn tạo thực thể ViewModel
bằng lớp ViewModel
, thì một đối tượng mới sẽ được tạo mỗi khi mảnh được tạo lại. Thay vào đó, hãy tạo thực thể ViewModel
bằng cách sử dụng ViewModelProvider
.
Cách hoạt động của ViewModelProvider
:
ViewModelProvider
trả về mộtViewModel
hiện có (nếu có) hoặc tạo mộtViewModel
mới (nếu chưa có).ViewModelProvider
tạo một thực thểViewModel
liên kết với phạm vi đã cho (một hoạt động hoặc một mảnh).ViewModel
đã tạo được giữ lại miễn là phạm vi vẫn hoạt động. Ví dụ: nếu phạm vi là một mảnh, thìViewModel
sẽ được giữ lại cho đến khi mảnh đó bị tách ra.
Khởi động ViewModel
bằng phương thức ViewModelProviders.of()
để tạo ViewModelProvider
:
- Trong lớp
GameFragment
, hãy khởi động biếnviewModel
. Đặt mã này vào bên trongonCreateView()
, sau định nghĩa của biến liên kết. Sử dụng phương thứcViewModelProviders.of()
, đồng thời truyền ngữ cảnhGameFragment
và lớpGameViewModel
được liên kết. - Phía trên quá trình khởi động đối tượng
ViewModel
, hãy thêm một câu lệnh nhật ký để ghi lại lệnh gọi phương thứcViewModelProviders.of()
.
Log.i("GameFragment", "Called ViewModelProviders.of")
viewModel = ViewModelProviders.of(this).get(GameViewModel::class.java)
- Chạy ứng dụng. Trong Android Studio, hãy mở ngăn Logcat và lọc dữ liệu trên
Game
. Nhấn vào nút Phát trên thiết bị hoặc trình mô phỏng. Màn hình trò chơi sẽ mở ra.
Như minh hoạ trong Logcat, phương thứconCreateView()
củaGameFragment
gọi phương thứcViewModelProviders.of()
để tạoGameViewModel
. Các câu lệnh ghi nhật ký mà bạn đã thêm vàoGameFragment
vàGameViewModel
sẽ xuất hiện trong Logcat.
- 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, vì vậyViewModelProviders.of()
được gọ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.
I/GameFragment: Called ViewModelProviders.of I/GameViewModel: GameViewModel created! I/GameFragment: Called ViewModelProviders.of I/GameFragment: Called ViewModelProviders.of I/GameFragment: Called ViewModelProviders.of
- Thoát khỏi trò chơi hoặc thoát khỏi mảnh trò chơi.
GameFragment
bị huỷ.GameViewModel
được liên kết cũng bị huỷ và hệ thống thực hiện lệnh gọi lạionCleared()
.
I/GameFragment: Called ViewModelProviders.of I/GameViewModel: GameViewModel created! I/GameFragment: Called ViewModelProviders.of I/GameFragment: Called ViewModelProviders.of I/GameFragment: Called ViewModelProviders.of I/GameViewModel: GameViewModel destroyed!
ViewModel
vẫn tồn tại sau các thay đổi về cấu hình, vì vậy, đây là nơi phù hợp cho dữ liệu cần tồn tại sau các thay đổi về cấu hình:
- Đặt dữ liệu cần hiển thị trên màn hình và mã để xử lý dữ liệu đó trong
ViewModel
. ViewModel
không được chứa nội dung tham chiếu đến các mảnh, hoạt động hoặc khung hiển thị, vì các hoạt động, mảnh và khung hiển thị không thể tiếp tục hoạt động trước các thay đổi về cấu hình.
Để so sánh, sau đây là cách xử lý dữ liệu giao diện người dùng GameFragment
trong ứng dụng khởi động trước khi bạn thêm ViewModel
và sau khi bạn thêm ViewModel
:
- Trước khi bạn thêm
ViewModel
:
Khi ứng dụng trải qua một thay đổi về cấu hình, chẳng hạn như xoay màn hình, thì mảnh trò chơi sẽ bị huỷ và tạo lại. Dữ liệu bị mất. - Sau khi bạn thêm
ViewModel
và di chuyển dữ liệu giao diện người dùng của mảnh trò chơi vàoViewModel
:
Giờ đây, tất cả dữ liệu mà mảnh cần hiển thị đều làViewModel
. Khi ứng dụng trải qua quá trình thay đổi cấu hình,ViewModel
sẽ vẫn tồn tại và dữ liệu được giữ lại.
Trong nhiệm vụ này, bạn sẽ di chuyển dữ liệu giao diện người dùng của ứng dụng vào lớp GameViewModel
, cùng với các phương thức xử lý dữ liệu. Bạn làm việc này để dữ liệu được giữ lại trong quá trình thay đổi cấu hình.
Bước 1: Di chuyển các trường dữ liệu và quy trình xử lý dữ liệu sang ViewModel
Di chuyển các trường dữ liệu và phương thức sau đây từ GameFragment
sang GameViewModel
:
- Di chuyển các trường dữ liệu
word
,score
vàwordList
. Đảm bảo rằngword
vàscore
không phải làprivate
.
Đừng di chuyển biến liên kếtGameFragmentBinding
vì biến này chứa các tham chiếu đến khung hiển thị. Biến này dùng để tăng kích thước bố cục, thiết lập trình nghe lượt nhấp và hiển thị dữ liệu trên màn hình – những việc mà mảnh này đảm nhận. - Di chuyển các phương thức
resetList()
vànextWord()
. Các phương thức này quyết định từ nào sẽ xuất hiện trên màn hình. - Từ bên trong phương thức
onCreateView()
, hãy di chuyển các lệnh gọi phương thức đếnresetList()
vànextWord()
đến khốiinit
củaGameViewModel
.
Các phương thức này phải nằm trong khốiinit
, vì bạn nên đặt lại danh sách từ khiViewModel
được tạo, chứ không phải mỗi khi mảnh được tạo. Bạn có thể xoá câu lệnh nhật ký trong khốiinit
củaGameFragment
.
Trình xử lý lượt nhấp onSkip()
và onCorrect()
trong GameFragment
chứa mã để xử lý dữ liệu và cập nhật giao diện người dùng. Mã cập nhật giao diện người dùng phải nằm trong mảnh, nhưng mã xử lý dữ liệu cần được chuyển sang ViewModel
.
Hiện tại, hãy đặt các phương thức giống hệt nhau ở cả hai nơi:
- Sao chép các phương thức
onSkip()
vàonCorrect()
từGameFragment
sangGameViewModel
. - Trong
GameViewModel
, hãy đảm bảo các phương thứconSkip()
vàonCorrect()
không phải làprivate
, vì bạn sẽ tham chiếu các phương thức này từ mảnh.
Sau đây là mã cho lớp GameViewModel
sau khi tái cấu trúc:
class GameViewModel : ViewModel() {
// The current word
var word = ""
// The current score
var score = 0
// The list of words - the front of the list is the next word to guess
private lateinit var wordList: MutableList<String>
/**
* Resets the list of words and randomizes the order
*/
private fun resetList() {
wordList = mutableListOf(
"queen",
"hospital",
"basketball",
"cat",
"change",
"snail",
"soup",
"calendar",
"sad",
"desk",
"guitar",
"home",
"railway",
"zebra",
"jelly",
"car",
"crow",
"trade",
"bag",
"roll",
"bubble"
)
wordList.shuffle()
}
init {
resetList()
nextWord()
Log.i("GameViewModel", "GameViewModel created!")
}
/**
* Moves to the next word in the list
*/
private fun nextWord() {
if (!wordList.isEmpty()) {
//Select and remove a word from the list
word = wordList.removeAt(0)
}
updateWordText()
updateScoreText()
}
/** Methods for buttons presses **/
fun onSkip() {
if (!wordList.isEmpty()) {
score--
}
nextWord()
}
fun onCorrect() {
if (!wordList.isEmpty()) {
score++
}
nextWord()
}
override fun onCleared() {
super.onCleared()
Log.i("GameViewModel", "GameViewModel destroyed!")
}
}
Sau đây là mã cho lớp GameFragment
sau khi tái cấu trúc:
/**
* Fragment where the game is played
*/
class GameFragment : Fragment() {
private lateinit var binding: GameFragmentBinding
private lateinit var viewModel: GameViewModel
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
// Inflate view and obtain an instance of the binding class
binding = DataBindingUtil.inflate(
inflater,
R.layout.game_fragment,
container,
false
)
Log.i("GameFragment", "Called ViewModelProviders.of")
viewModel = ViewModelProviders.of(this).get(GameViewModel::class.java)
binding.correctButton.setOnClickListener { onCorrect() }
binding.skipButton.setOnClickListener { onSkip() }
updateScoreText()
updateWordText()
return binding.root
}
/** Methods for button click handlers **/
private fun onSkip() {
if (!wordList.isEmpty()) {
score--
}
nextWord()
}
private fun onCorrect() {
if (!wordList.isEmpty()) {
score++
}
nextWord()
}
/** Methods for updating the UI **/
private fun updateWordText() {
binding.wordText.text = word
}
private fun updateScoreText() {
binding.scoreText.text = score.toString()
}
}
Bước 2: Cập nhật các tham chiếu đến trình xử lý lượt nhấp và trường dữ liệu trong GameFragment
- Trong
GameFragment
, hãy cập nhật các phương thứconSkip()
vàonCorrect()
. Xoá mã để cập nhật điểm số và thay vào đó, hãy gọi các phương thứconSkip()
vàonCorrect()
tương ứng trênviewModel
. - Vì bạn đã di chuyển phương thức
nextWord()
sangViewModel
, nên mảnh trò chơi không còn truy cập được vào phương thức này nữa.
TrongGameFragment
, trong phương thứconSkip()
vàonCorrect()
, hãy thay thế lệnh gọi đếnnextWord()
bằngupdateScoreText()
vàupdateWordText()
. Các phương thức này hiển thị dữ liệu trên màn hình.
private fun onSkip() {
viewModel.onSkip()
updateWordText()
updateScoreText()
}
private fun onCorrect() {
viewModel.onCorrect()
updateScoreText()
updateWordText()
}
- Trong
GameFragment
, hãy cập nhật các biếnscore
vàword
để sử dụng các biếnGameViewModel
, vì các biến này hiện nằm trongGameViewModel
.
private fun updateWordText() {
binding.wordText.text = viewModel.word
}
private fun updateScoreText() {
binding.scoreText.text = viewModel.score.toString()
}
- Trong
GameViewModel
, bên trong phương thứcnextWord()
, hãy xoá các lệnh gọi đến phương thứcupdateWordText()
vàupdateScoreText()
. Các phương thức này hiện đang được gọi từGameFragment
. - Tạo bản dựng ứng dụng và đảm bảo không có lỗi. Nếu bạn gặp lỗi, hãy dọn dẹp và tạo lại dự án.
- Chạy ứng dụng và chơi trò chơi với một vài từ. Trong khi ở màn hình trò chơi, hãy xoay thiết bị. Lưu ý rằng điểm số hiện tại và từ hiện tại vẫn được giữ lại sau khi thay đổi hướng.
Tuyệt vời! Giờ đây, tất cả dữ liệu của ứng dụng đều được lưu trữ trong một ViewModel
, vì vậy, dữ liệu sẽ được giữ lại trong quá trình thay đổi cấu hình.
Trong nhiệm vụ này, bạn sẽ triển khai trình nghe lượt nhấp cho nút End Game (Kết thúc trò chơi).
- Trong
GameFragment
, hãy thêm một phương thức có tên làonEndGame()
. Phương thứconEndGame()
sẽ được gọi khi người dùng nhấn vào nút Kết thúc trò chơi.
private fun onEndGame() {
}
- Trong
GameFragment
, bên trong phương thứconCreateView()
, hãy tìm mã thiết lập trình nghe lượt nhấp cho các nút Got It (Tôi hiểu) và Skip (Bỏ qua). Ngay bên dưới 2 dòng này, hãy đặt một trình xử lý lượt nhấp cho nút End Game (Kết thúc trò chơi). Sử dụng biến liên kếtbinding
. Trong trình nghe lượt nhấp, hãy gọi phương thứconEndGame()
.
binding.endGameButton.setOnClickListener { onEndGame() }
- Trong
GameFragment
, hãy thêm một phương thức có tên làgameFinished()
để chuyển ứng dụng đến màn hình điểm số. Truyền điểm số dưới dạng một đối số bằng cách sử dụng Safe Args.
/**
* Called when the game is finished
*/
private fun gameFinished() {
Toast.makeText(activity, "Game has just finished", Toast.LENGTH_SHORT).show()
val action = GameFragmentDirections.actionGameToScore()
action.score = viewModel.score
NavHostFragment.findNavController(this).navigate(action)
}
- Trong phương thức
onEndGame()
, hãy gọi phương thứcgameFinished()
.
private fun onEndGame() {
gameFinished()
}
- Chạy ứng dụng, chơi trò chơi và chuyển đổi qua một số từ. Nhấn vào nút Kết thúc trò chơi . Lưu ý rằng ứng dụng sẽ chuyển đến màn hình điểm số, nhưng điểm số cuối cùng không xuất hiện. Bạn sẽ khắc phục vấn đề này trong nhiệm vụ tiếp theo.
Khi người dùng kết thúc trò chơi, ScoreFragment
sẽ không hiển thị điểm số. Bạn muốn ViewModel
giữ điểm số mà ScoreFragment
sẽ hiển thị. Bạn sẽ truyền giá trị điểm trong quá trình khởi chạy ViewModel
bằng cách sử dụng mẫu phương thức ban đầu.
Mẫu phương thức ban đầu là một mẫu thiết kế sáng tạo sử dụng các phương thức ban đầu để tạo đối tượng. Phương thức nhà máy là một phương thức trả về một thực thể của cùng một lớp.
Trong nhiệm vụ này, bạn sẽ tạo một ViewModel
có hàm khởi tạo được tham số hoá cho mảnh điểm số và một phương thức của nhà máy để tạo thực thể ViewModel
.
- Trong gói
score
, hãy tạo một lớp Kotlin mới có tên làScoreViewModel
. Lớp này sẽ làViewModel
cho đoạn điểm số. - Mở rộng lớp
ScoreViewModel
từViewModel.
Thêm một tham số hàm khởi tạo cho điểm số cuối cùng. Thêm một khốiinit
bằng câu lệnh nhật ký. - Trong lớp
ScoreViewModel
, hãy thêm một biến có tên làscore
để lưu điểm số cuối cùng.
class ScoreViewModel(finalScore: Int) : ViewModel() {
// The final score
var score = finalScore
init {
Log.i("ScoreViewModel", "Final score is $finalScore")
}
}
- Trong gói
score
, hãy tạo một lớp Kotlin khác có tên làScoreViewModelFactory
. Lớp này sẽ chịu trách nhiệm khởi tạo đối tượngScoreViewModel
. - Mở rộng lớp
ScoreViewModelFactory
từViewModelProvider.Factory
. Thêm một tham số hàm khởi tạo cho điểm số cuối cùng.
class ScoreViewModelFactory(private val finalScore: Int) : ViewModelProvider.Factory {
}
- Trong
ScoreViewModelFactory
, Android Studio sẽ hiện lỗi về một thành phần trừu tượng chưa được triển khai. Để khắc phục lỗi này, hãy ghi đè phương thứccreate()
. Trong phương thứccreate()
, hãy trả về đối tượngScoreViewModel
mới tạo.
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(ScoreViewModel::class.java)) {
return ScoreViewModel(finalScore) as T
}
throw IllegalArgumentException("Unknown ViewModel class")
}
- Trong
ScoreFragment
, hãy tạo các biến lớp choScoreViewModel
vàScoreViewModelFactory
.
private lateinit var viewModel: ScoreViewModel
private lateinit var viewModelFactory: ScoreViewModelFactory
- Trong
ScoreFragment
, bên trongonCreateView()
, sau khi khởi động biếnbinding
, hãy khởi độngviewModelFactory
. DùngScoreViewModelFactory
. Truyền điểm số cuối cùng từ gói đối số, dưới dạng tham số hàm khởi tạo choScoreViewModelFactory()
.
viewModelFactory = ScoreViewModelFactory(ScoreFragmentArgs.fromBundle(arguments!!).score)
- Trong
onCreateView(
), sau khi khởi độngviewModelFactory
, hãy khởi động đối tượngviewModel
. Gọi phương thứcViewModelProviders.of()
, truyền vào ngữ cảnh mảnh điểm số được liên kết vàviewModelFactory
. Thao tác này sẽ tạo đối tượngScoreViewModel
bằng phương thức tạo được xác định trong lớpviewModelFactory
.
viewModel = ViewModelProviders.of(this, viewModelFactory)
.get(ScoreViewModel::class.java)
- Trong phương thức
onCreateView()
, sau khi khởi chạyviewModel
, hãy đặt văn bản của khung hiển thịscoreText
thành điểm số cuối cùng được xác định trongScoreViewModel
.
binding.scoreText.text = viewModel.score.toString()
- Chạy ứng dụng và chơi trò chơi. Lần lượt xem một số hoặc tất cả các từ rồi nhấn vào Kết thúc trò chơi. Xin lưu ý rằng phân đoạn điểm số hiện hiển thị điểm số chung cuộc.
- Không bắt buộc: Kiểm tra nhật ký
ScoreViewModel
trong Logcat bằng cách lọc theoScoreViewModel
. Giá trị điểm số phải được hiển thị.
2019-02-07 10:50:18.328 com.example.android.guesstheword I/ScoreViewModel: Final score is 15
Trong nhiệm vụ này, bạn đã triển khai ScoreFragment
để sử dụng ViewModel
. Bạn cũng đã tìm hiểu cách tạo hàm khởi tạo được tham số hoá cho một ViewModel
bằng giao diện ViewModelFactory
.
Xin chúc mừng! Bạn đã thay đổi cấu trúc của ứng dụng để sử dụng một trong các Bộ thành phần cấu trúc Android, ViewModel
. Bạn đã giải quyết vấn đề về vòng đời của ứng dụng và giờ đây, dữ liệu của trò chơi vẫn tồn tại sau khi thay đổi cấu hình. Bạn cũng đã tìm hiểu cách tạo hàm khởi tạo có tham số để tạo ViewModel
bằng giao diện ViewModelFactory
.
Dự án Android Studio: GuessTheWord
- 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.
- Đơ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 chứa dữ liệu xuất hiện trên giao diện người dùng. Đặt dữ liệu đó vào mộtViewModel
. - 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 được đề xuất.ViewModelProvider.Factory
là một giao diện mà bạn có thể dùng để tạo đối tượngViewModel
.
Bảng dưới đây so sánh các bộ điều khiển giao diện người dùng với các thực thể ViewModel
lưu trữ dữ liệu cho các bộ điều khiển đó:
Trình điều khiển giao diện người dùng | ViewModel |
Ví dụ về một bộ điều khiển giao diện người dùng là | Ví dụ về |
Không chứa dữ liệu nào để hiển thị trong giao diện người dùng. | Chứa dữ liệu mà bộ điều khiển giao diện người dùng hiển thị trong giao diện người dùng. |
Chứa mã để hiển thị dữ liệu và mã sự kiện người dùng, chẳng hạn như trình nghe lượt nhấp. | Chứa mã để xử lý dữ liệu. |
Bị huỷ và tạo lại trong mỗi lần thay đổi cấu hình. | Chỉ bị huỷ khi trình điều khiển giao diện người dùng được liên kết biến mất vĩnh viễn – đối với một hoạt động, khi hoạt động kết thúc hoặc đối với một mảnh, khi mảnh bị tách ra. |
Có chứa khung hiển thị. | Không được chứa nội dung tham chiếu đến các hoạt động, mảnh hoặc khung hiển thị, vì các thành phần này không tồn tại sau khi thay đổi cấu hình, nhưng |
Chứa một thông tin tham chiếu đến | Không chứa bất kỳ thông tin tham chiếu nào đến bộ điều khiển giao diện người dùng được liên kết. |
Khoá học của Udacity:
Tài liệu dành cho nhà phát triển Android:
- Tổng quan về ViewModel
- Điều khiển vòng đời bằng các thành phần nhận biết vòng đời
- Hướng dẫn về cấu trúc ứng dụng
ViewModelProvider
ViewModelProvider.Factory
Khác:
- Mẫu cấu trúc MVVM (model-view-viewmodel).
- Nguyên tắc thiết kế phân tách các mối lo ngại (SoC)
- Mẫu phương thức nhà máy
Phần này liệt kê các bài tập về nhà cho học viên của lớp học lập trình này trong phạm vi khoá học có người hướng dẫn. Người hướng dẫn phải thực hiện các việc sau đây:
- Giao bài tập về nhà nếu cần.
- Trao đổi với học viên về cách nộp bài tập về nhà.
- Chấm điểm bài tập về nhà.
Người hướng dẫn có thể sử dụng các đề xuất này ít hoặc nhiều tuỳ ý và nên giao cho học viên bất kỳ bài tập về nhà nào khác mà họ cảm thấy phù hợp.
Nếu bạn đang tự học các lớp học lập trình, hãy sử dụng những bài tập về nhà này để kiểm tra kiến thức của mình.
Trả lời các câu hỏi sau
Câu hỏi 1
Để tránh bị mất dữ liệu trong quá trình thay đổi cấu hình thiết bị, bạn nên lưu dữ liệu ứng dụng vào lớp nào?
ViewModel
LiveData
Fragment
Activity
Câu hỏi 2
ViewModel
không được chứa nội dung tham chiếu đến mảnh, hoạt động hoặc chế độ xem. Đúng hay sai?
- Đúng
- Sai
Câu hỏi 3
Khi nào ViewModel
bị huỷ bỏ?
- Khi trình điều khiển giao diện người dùng liên kết bị huỷ và được tạo lại trong lúc đổi hướng thiết bị.
- Trong lúc thay đổi hướng.
- Khi trình điều khiển giao diện người dùng liên kết đã hoàn tất (nếu là một hoạt động) hoặc được tách ra (nếu là một mảnh).
- Khi người dùng nhấn vào nút Quay lại.
Câu hỏi 4
Giao diện ViewModelFactory
dùng để làm gì?
- Tạo bản sao của đối tượng trong
ViewModel
. - Giữ lại dữ liệu trong khi thay đổi hướng.
- Làm mới dữ liệu đang hiển thị trên màn hình.
- Nhận thông báo khi dữ liệu ứng dụng thay đổi.
Bắt đầu bài học tiếp theo:
Để biết đường liên kết đến các lớp học lập trình khác trong khoá học này, hãy xem trang đích của lớp học lập trình Kiến thức cơ bản về cách tạo ứng dụng Android bằng Kotlin.