Kiến thức cơ bản về Kotlin cho Android 05.1: ViewModel và ViewModelFactory

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ớp ViewModel 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ượng ViewModel 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, ViewModelViewModelFactory 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ện ViewModelProvider.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ượng ViewModel 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

  1. Tải mã khởi đầu GuessTheWord xuống rồi mở dự án trong Android Studio.
  2. Chạy ứng dụng trên thiết bị chạy Android hoặc trên trình mô phỏng.
  3. 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ã

  1. Trong Android Studio, hãy khám phá đoạn mã để hiểu cách ứng dụng hoạt động.
  2. 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ơi
  • score/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ức resetList() 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ức nextWord().
  • 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ức onSkip(). Đ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.

  1. 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ừ.
  2. 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.
  3. 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.
  4. 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ạ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 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,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

  1. Mở tệp build.gradle(module:app). Bên trong khối dependencies, hãy thêm phần phụ thuộc Gradle cho ViewModel.

    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'
  1. Trong thư mục gói screens/game/, hãy tạo một lớp Kotlin mới có tên là GameViewModel.
  2. Đặt lớp GameViewModel mở rộng lớp trừu tượng ViewModel.
  3. Để 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ối init bằng câu lệnh log.
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.

  1. Trong lớp GameViewModel, hãy ghi đè phương thức onCleared().
  2. Thêm câu lệnh nhật ký vào trong onCleared() để theo dõi vòng đời của GameViewModel.
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.

  1. Trong lớp GameFragment, hãy thêm một trường thuộc loại GameViewModel ở 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ột ViewModel hiện có (nếu có) hoặc tạo một ViewModel 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:

  1. Trong lớp GameFragment, hãy khởi động biến viewModel. Đặt mã này vào bên trong onCreateView(), sau định nghĩa của biến liên kết. Sử dụng phương thức ViewModelProviders.of(), đồng thời truyền ngữ cảnh GameFragment và lớp GameViewModel được liên kết.
  2. 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ức ViewModelProviders.of().
Log.i("GameFragment", "Called ViewModelProviders.of")
viewModel = ViewModelProviders.of(this).get(GameViewModel::class.java)
  1. 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ức onCreateView() của GameFragment gọi phương thức ViewModelProviders.of() để tạo GameViewModel. Các câu lệnh ghi nhật ký mà bạn đã thêm vào GameFragmentGameViewModel sẽ xuất hiện trong Logcat.

  1. 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ậy ViewModelProviders.of() được gọi mỗi lần. Nhưng GameViewModel 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
  1. 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ại onCleared().
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ào ViewModel:
    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:

  1. Di chuyển các trường dữ liệu word, scorewordList. Đảm bảo rằng wordscore không phải là private.

    Đừng di chuyển biến liên kết GameFragmentBinding 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.
  2. Di chuyển các phương thức resetList()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.
  3. Từ bên trong phương thức onCreateView(), hãy di chuyển các lệnh gọi phương thức đến resetList()nextWord() đến khối init của GameViewModel.

    Các phương thức này phải nằm trong khối init, vì bạn nên đặt lại danh sách từ khi ViewModel đượ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ối init của GameFragment.

Trình xử lý lượt nhấp onSkip()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:

  1. Sao chép các phương thức onSkip()onCorrect() từ GameFragment sang GameViewModel.
  2. Trong GameViewModel, hãy đảm bảo các phương thức onSkip()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

  1. Trong GameFragment, hãy cập nhật các phương thức onSkip()onCorrect(). Xoá mã để cập nhật điểm số và thay vào đó, hãy gọi các phương thức onSkip()onCorrect() tương ứng trên viewModel.
  2. Vì bạn đã di chuyển phương thức nextWord() sang ViewModel, 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.

    Trong GameFragment, trong phương thức onSkip()onCorrect(), hãy thay thế lệnh gọi đến nextWord() bằng updateScoreText()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()
}
  1. Trong GameFragment, hãy cập nhật các biến scoreword để sử dụng các biến GameViewModel, vì các biến này hiện nằm trong GameViewModel.
private fun updateWordText() {
   binding.wordText.text = viewModel.word
}

private fun updateScoreText() {
   binding.scoreText.text = viewModel.score.toString()
}
  1. Trong GameViewModel, bên trong phương thức nextWord(), hãy xoá các lệnh gọi đến phương thức updateWordText()updateScoreText(). Các phương thức này hiện đang được gọi từ GameFragment.
  2. 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.
  3. 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).

  1. Trong GameFragment, hãy thêm một phương thức có tên là onEndGame(). Phương thức onEndGame() 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() {
   }
  1. Trong GameFragment, bên trong phương thức onCreateView(), 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ết binding. Trong trình nghe lượt nhấp, hãy gọi phương thức onEndGame().
binding.endGameButton.setOnClickListener { onEndGame() }
  1. 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)
}
  1. Trong phương thức onEndGame(), hãy gọi phương thức gameFinished().
private fun onEndGame() {
   gameFinished()
}
  1. 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.

  1. 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ố.
  2. 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ối init bằng câu lệnh nhật ký.
  3. 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")
   }
}
  1. 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ượng ScoreViewModel.
  2. 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 {
}
  1. 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ức create(). Trong phương thức create(), hãy trả về đối tượng ScoreViewModel 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")
}
  1. Trong ScoreFragment, hãy tạo các biến lớp cho ScoreViewModelScoreViewModelFactory.
private lateinit var viewModel: ScoreViewModel
private lateinit var viewModelFactory: ScoreViewModelFactory
  1. Trong ScoreFragment, bên trong onCreateView(), sau khi khởi động biến binding, hãy khởi động viewModelFactory. Dùng ScoreViewModelFactory. 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 cho ScoreViewModelFactory().
viewModelFactory = ScoreViewModelFactory(ScoreFragmentArgs.fromBundle(arguments!!).score)
  1. Trong onCreateView(), sau khi khởi động viewModelFactory, hãy khởi động đối tượng viewModel. Gọi phương thức ViewModelProviders.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ượng ScoreViewModel bằng phương thức tạo được xác định trong lớp viewModelFactory.
viewModel = ViewModelProviders.of(this, viewModelFactory)
       .get(ScoreViewModel::class.java)
  1. Trong phương thức onCreateView(), sau khi khởi chạy viewModel, 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 trong ScoreViewModel.
binding.scoreText.text = viewModel.score.toString()
  1. 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.

  1. Không bắt buộc: Kiểm tra nhật ký ScoreViewModel trong Logcat bằng cách lọc theo ScoreViewModel. 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ặ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 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ột ViewModel.
  • 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ớp ViewModel 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ượng ViewModel.

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à ScoreFragment mà bạn đã tạo trong lớp học lập trình này.

Ví dụ về ViewModelScoreViewModel mà bạn đã tạo trong lớp học lập trình này.

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 ViewModel thì có.

Chứa một thông tin tham chiếu đến ViewModel được liên kết.

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:

Khác:

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: 5.2: LiveData và trình quan sát LiveData

Để 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.