Android Kotlin Fundamentals 05.1: ViewModel và ViewModelFactory

Lớp học lập trình này nằm trong khóa học về Khái niệm cơ bản về Android Kotlin. Bạn sẽ nhận được nhiều giá trị nhất từ khóa học này nếu bạn làm việc qua các lớp học lập trình theo trình tự. Tất cả các lớp học lập trình trong khóa học đều có trên trang đích của các lớp học lập trình cơ bản về Android Kotlin.

Màn hình tiêu đề

Màn hình trò chơi

Màn hình điểm

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 chú trọng đến vòng đời. Lớp ViewModel cho phép dữ liệu tồn tại sau khi 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 để tạo thực thể và trả về đối tượng ViewModel còn hiệu lực sau các thay đổi về 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 sơ đồ điều hướng để triển khai hệ thống điều hướng trong ứng dụng của bạn.
  • Cách thêm mã để chuyển giữa các đích của ứng dụng và chuyển dữ liệu giữa các đích điều hướng.
  • Cách hoạt động của vòng đời của hoạt động và mảnh.
  • Cách thêm thông tin ghi nhật ký vào một ứ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 giữ lại dữ liệu giao diện người dùng thông qua các thay đổi về cấu hình thiết bị.
  • Mẫu thiết kế phương thức ban đầu là gì và cách sử dụng mẫu đó.
  • 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 để dữ liệu có hiệu lực sau khi cấu hình thay đổi.
  • Dùng ViewModelFactory và mẫu thiết kế phương thức gốc để tạo thực thể cho đối tượng ViewModel bằng các tham số hàm dựng.

Trong các lớp học lập trình của Bài học 5, bạn phát triển ứng dụng GuessTheWord, bắt đầu bằng mã dành cho người mới bắt đầu. GuessTheWord là trò chơi theo phong cách charades Hai người chơi, trong đó người chơi cộng tác để đạt được điểm số cao nhất có thể.

Người chơi đầu tiên nhìn vào các từ trong ứng dụng và lần lượt thực hiện từng từ, đảm bảo không hiển thị từ đó cho người chơi thứ hai. 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ừ, ví dụ như "guitar," như minh họa trong ảnh chụp màn hình dưới đây.

Người chơi đầu tiên thực hiện từ này, cẩn thận để không thực sự nói từ đó.

  • Khi người chơi thứ hai đoán đúng từ, người chơi đầu tiên nhấn nút Tôi hiểu để tăng số lượng và tính từ tiếp theo.
  • Nếu người chơi thứ hai không thể đoán từ, người chơi đầu tiên sẽ nhấn nút Bỏ qua để giảm một từ và bỏ qua từ tiếp theo.
  • Để kết thúc trò chơi, hãy nhấn nút Kết thúc trò chơi. (Chức năng này không có trong mã bắt đầu cho lớp học lập trình đầu tiên trong chuỗi.)

Trong nhiệm vụ này, bạn tải xuống và chạy ứng dụng dành cho người mới bắt đầu và kiểm tra mã.

Bước 1: Bắt đầu

  1. Tải mã khởi động GuessTheWord xuống và 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 Bỏ qua hiển thị từ tiếp theo và giảm điểm số một, đồng thời nút Tôi hiểu hiển thị từ tiếp theo và tăng điểm số một. Nút Trò chơi kết thúc không được triển khai, vì vậy không có gì xảy ra khi bạn nhấn vào.

Bước 2: Thực hiện hướng dẫn bằng mã

  1. Trong Android Studio, hãy khám phá mã này để biết cách hoạt động của ứng dụng.
  2. Đảm bảo xem các tệp mô tả bên dưới, đặc biệt quan trọng.

MainActivity.kt

Tệp này chỉ chứa mã mặc định do mẫu tạo.

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 qua ứng dụng.

Các mảnh trên giao diện người dùng

Mã dành cho người mới bắt đầ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ố

screen/title/TitleFragment.kt

Mảnh tiêu đề là màn hình đầu tiên hiển thị khi người dùng chạy ứng dụng. Trình xử lý lượt nhấp được đặt thành nút Chơi để chuyển đến màn hình trò chơi.

màn hình/trò chơi/GameFragment.kt

Đây là mảnh chính, nơi hầu hết hành động của trò chơi diễn ra:

  • 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 các từ sẽ được sử dụng trong trò chơi.
  • Phương thức onSkip() là trình xử lý lượt nhấp cho nút Bỏ qua. Điểm này giảm 1 điểm, 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 Tôi 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 thêm 1 vào điểm số thay vì trừ đi.

màn hình/score/ScoreFragment.kt

ScoreFragment là màn hình cuối cùng trong trò chơi và hiển thị điểm cuối cùng của người chơi. Trong lớp học lập trình này, bạn thêm phương thức triển khai để hiển thị màn hình này và hiển thị điểm cuối cùng.

res/navigation/main_navigation.xml

Sơ đồ điều hướng cho thấy cách các mảnh được kết nối thông qua trình đơn điều hướng:

  • Từ mảnh tiêu đề, người dùng có thể chuyển đến mảnh trò chơi.
  • Từ mảnh trò chơi, người dùng có thể chuyển đến mảnh điểm.
  • Từ mảnh điểm, 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 đề liên quan đến ứng dụng Bắt đầu đoán từ.

  1. Chạy mã bắt đầu và chơi trò chơi qua một vài từ, nhấn vào Bỏ qua hoặc Tôi hiểu sau mỗi từ.
  2. Giờ đây, màn hình trò chơi sẽ hiện một từ và tỷ 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. Hãy lưu ý rằng điểm số hiện tại bị mất.
  3. Chơi trò chơi qua một vài từ nữa. Khi màn hình trò chơi hiển thị với một số điểm, hãy đóng rồi mở lại ứng dụng. Xin lưu ý rằng trò chơi sẽ khởi động lại từ đầu vì trạng thái ứng dụng không được lưu.
  4. Chơi trò chơi qua một vài từ, sau đó nhấn vào nút Kết thúc trò chơi. Xin lưu ý rằng không có gì xảy ra.

Các vấn đề trong ứng dụng:

  • Ứng dụng khởi động không lưu và khôi phục trạng thái ứng dụng trong quá trình thay đổi cấu hình, chẳng hạn như khi hướng thiết bị thay đổi hoặc khi ứng dụng tắt và khởi động lạ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 mã bổ sung để lưu trạng thái trong một gói và triển khai logic để truy xuất trạng thái đó. Ngoài ra, lượng dữ liệu có thể 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 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 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à cách thiết kế ứng dụng của bạn\39; các lớp và mối quan hệ giữa chúng, sao cho mã được sắp xếp, thực hiện tốt trong các tình huống cụ thể và dễ sử dụng. Trong tập hợp bốn 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 đối với ứng dụng GuessTheWord tuân theo các nguyên tắc cấu trúc ứng dụng Android và bạn sử dụng Các thành phần cấu trúc của Android. Cấu trúc ứng dụng Android tương tự như cấu trúc cấu trúc MVVM (model-view-viewmodel).

Ứng dụng GuessTheWord tuân theo nguyên tắc thiết kế tách biệt mối quan ngại và được chia thành các lớp, trong đó mỗi lớp giải quyết một mối quan tâm 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 học bạn làm việc cùng 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

Bộ đ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. Bộ điều khiển giao diện người dùng chỉ được chứa logic xử lý giao diện người dùng và các hoạt động tương tác với hệ điều hành như hiển thị chế độ xem và thu thập dữ liệu đầu vào của người dùng. Đừng dùng logic quyết định, chẳng hạn như logic xác định văn bản sẽ hiển thị, vào bộ điều khiển giao diện người dùng.

Trong mã điều kiện khởi động GuessTheWord, bộ điều khiển giao diện người dùng là ba mảnh: GameFragment, ScoreFragment,TitleFragment. Tuân theo nguyên tắc về thiết kế và nguyên tắc thiết kế; GameFragment chỉ chịu trách nhiệm vẽ các yếu tố trong trò chơi lên màn hình và biết được khi nào người dùng nhấn vào nút và không thực hiện thao tác nào khác. Khi người dùng nhấn vào một nút, thông tin này được chuyển đến GameViewModel.

ViewModel

ViewModel lưu giữ dữ liệu sẽ 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 và phép biến đổi đơn giản trên dữ liệu để chuẩn bị dữ liệu mà bộ điều khiển giao diện người dùng hiển thị. Trong cấu trúc này, ViewModel thực hiện 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, bởi vì đây là dữ liệu sẽ hiển thị trên màn hình. GameViewModel cũng chứa logic kinh doanh để thực hiện các phép tính đơn giản nhằm quyết định trạng thái hiện tại của dữ liệu.

Vé ViewModelFactory

ViewModelFactory tạo thực thể ViewModel cho đối tượng, có hoặc không có thông số hàm dựng.

Trong các lớp học lập trình sau này, bạn tìm hiểu về các Thành phần cấu 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 việc cần làm này, bạn thêm ViewModel đầu tiên vào ứng dụng của mình, GameViewModel cho GameFragment. Bạn cũng hiểu được ý nghĩa của ViewModel khi nhận biết 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 vào 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 cách này không giải quyết được, 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 screens/game/ của gói, 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 nhật ký

ViewModel bị hủy bỏ khi mảnh liên kết được tách ra hoặc khi hoạt động kết thúc. Ngay trước khi hủy lệnh gọi lại ViewModel, lệnh gọi lại onCleared() sẽ được gọi để dọn dẹp 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 cả hai, bạn tạo một mục tham chiếu đến ViewModel bên trong bộ điều khiển giao diện người dùng.

Trong bước này, bạn tạo tệp tham chiếu đến GameViewModel bên trong bộ điều khiển giao diện người dùng tương ứng, đó 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 làm biến lớp.
private lateinit var viewModel: GameViewModel

Bước 4: Khởi chạy 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, ViewModel bản sao 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 một phiên bản 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 một mã tồn tại hoặc tạo một mã mới nếu mã đó chưa tồn tại.
  • ViewModelProvider tạo một thực thể ViewModel liên kết với một phạm vi nhất định (một hoạt động hoặc một mảnh).
  • ViewModel đã tạo vẫn được giữ lại chừng nào phạm vi vẫn còn. Ví dụ: nếu phạm vi là một mảnh, thì ViewModel sẽ được giữ lại cho đến khi mảnh được tách ra.

Khởi chạy ViewModel, sử dụng phương thức ViewModelProviders.of() để tạo ViewModelProvider:

  1. Trong lớp GameFragment, hãy chạy biến viewModel. Đặt mã này 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() và chuyển trong bối cảnh GameFragment được liên kết và lớp GameViewModel.
  2. Phía trên quá trình khởi tạo đối tượng ViewModel, hãy thêm một câu lệnh nhật ký để ghi nhật ký 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 rồi lọc theo 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ư đã nêu trong Logcat, phương thức onCreateView() của GameFragment sẽ gọi phương thức ViewModelProviders.of() để tạo GameViewModel. Câu lệnh ghi nhật ký mà bạn thêm vào GameFragmentGameViewModel sẽ hiển thị 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ị hủy bỏ và được tạo lại mỗi lần do đó, ViewModelProviders.of() được gọi mỗi lần. Tuy nhiên, GameViewModel chỉ được tạo một lần và sẽ không được tạo lại hoặc hủy bỏ cho mỗi cuộc 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 di chuyển khỏi mảnh trò chơi. GameFragment bị hủy bỏ. GameViewModel liên kết cũng bị hủy và lệnh gọi lại onCleared() sẽ được 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
I/GameViewModel: GameViewModel destroyed!

ViewModel tồn tại sau khi thay đổi cấu hình, vì vậy, đây là vị trí tốt để dữ liệu cần tồn tại sau các thay đổi về cấu hình:

  • Đặt dữ liệu hiển thị trên màn hình và mã để xử lý dữ liệu đó trong ViewModel.
  • ViewModel không được chứa các thông tin tham chiếu đến các mảnh, hoạt động hoặc chế độ xem vì các hoạt động, mảnh và chế độ xem không tồn tại để thay đổi cấu hình.

Để có thông tin so sánh, dưới đây là cách dữ liệu giao diện người dùng GameFragment được xử lý trong ứng dụng dành cho người mới bắt đầu 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, mảnh trò chơi sẽ bị hủy bỏ và được 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:
    Tất cả dữ liệu mà mảnh cần hiển thị hiện là ViewModel. Khi ứng dụng trải qua sự thay đổi về cấu hình, ViewModel sẽ vẫn tồn tại và dữ liệu sẽ được giữ lại.

Trong tác vụ này, bạn sẽ di chuyển dữ liệu giao diện người dùng của ứng dụng sang lớp GameViewModel, cùng với các phương thức để xử lý dữ liệu. Thao tác này được thực hiện để 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à xử lý dữ liệu vào ViewModel

Di chuyển các trường dữ liệu và phương thức sau từ GameFragment sang GameViewModel:

  1. Di chuyển các trường dữ liệu word, scorewordList. Hãy đảm bảo 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 thông tin tham chiếu đến chế độ xem. Biến này được dùng để tăng cường bố cục, thiết lập trình xử lý lượt nhấp và hiển thị dữ liệu trên màn hình – trách nhiệm của mảnh.
  2. Di chuyển phương thức resetList()nextWord(). Các phương thức này sẽ quyết định từ nào sẽ hiển thị 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ể xóa 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ần cập nhật giao diện người dùng sẽ nằm trong mảnh, nhưng cần phải di chuyển mã để xử lý dữ liệu vào ViewModel.

Hiện tại, hãy đặt các phương thức giống 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ã của 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ã của 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 tệp tham chiếu đến trình xử lý lượt nhấp và các trường dữ liệu trong GameFragment

  1. Trong GameFragment, hãy cập nhật phương thức onSkip()onCorrect(). Hãy xóa mã này để cập nhật điểm số và 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 thể truy cập vào phương thức đó nữa.

    Trong GameFragment, trong phương thức onSkip()onCorrect(), hãy thay thế lệnh gọi thành 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 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 xóa 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. Xây dựng ứng dụng và đảm bảo ứng dụng không có lỗi. Nếu bạn gặp lỗi, hãy dọn dẹp và xây dựng lại dự án.
  3. Chạy ứng dụng và chơi trò chơi thông qua một số từ. Khi bạn đang ở màn hình trò chơi, hãy xoay thiết bị. Xin lưu ý rằng điểm hiện tại và từ hiện tại được giữ lại sau khi có thay đổi về hướng.

Tuyệt vời! Giờ đây, tất cả dữ liệu của ứng dụng được lưu trữ trong ViewModel, do đó, dữ liệu này sẽ được giữ lại trong quá trình thay đổi cấu hình.

Trong nhiệm vụ này, bạn triển khai trình xử lý lượt nhấp cho nút Trò chơi kết thúc.

  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 Trò chơi kết thúc.
private fun onEndGame() {
   }
  1. Trong GameFragment, bên trong phương thức onCreateView(), hãy tìm mã đặt trình xử lý lượt nhấp cho các nút GotBỏ qua. Ngay bên dưới hai dòng này, hãy đặt trình xử lý lượt nhấp cho nút Trò chơi kết thúc. Hãy dùng biến liên kết binding. Bên 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. Chuyển điểm số làm đối số, 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à đạp xe qua một số từ. Nhấn vào nút Kết thúc trò chơi. Xin lưu ý rằng ứng dụng sẽ chuyển đến màn hình điểm, nhưng điểm cuối cùng không hiển thị. Bạn khắc phục sự cố này trong tác vụ tiếp theo.

Khi người dùng kết thúc trò chơi, ScoreFragment không hiển thị điểm số. Bạn muốn ViewModel giữ điểm số do ScoreFragment hiển thị. Bạn sẽ chuyển giá trị điểm trong quá trình khởi tạo ViewModel bằng cách sử dụng mẫu phương thức gốc.

Mẫu phương thức nhà máymẫu thiết kế có tính sáng tạo sử dụng các phương thức ban đầu để tạo đối tượng. Phương thức gốc là một phương thức trả về một thực thể của cùng một lớp.

Trong tác vụ này, bạn tạo một ViewModel có một hàm dựng đã tạo thông số cho mảnh điểm và một phương thức gốc để tạo thực thể cho 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 mảnh điểm.
  2. Mở rộng lớp ScoreViewModel từ ViewModel. Thêm thông số hàm dựng cho điểm số cuối cùng. Thêm một khối init bằng một 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 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 tạo đối tượng ScoreViewModel.
  2. Mở rộng lớp ScoreViewModelFactory từ ViewModelProvider.Factory. Thêm một tham số hàm dựng cho điểm số cuối cùng.
class ScoreViewModelFactory(private val finalScore: Int) : ViewModelProvider.Factory {
}
  1. Trong ScoreViewModelFactory, Android Studio hiển thị lỗi về một thành viê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 được 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 tạo biến binding, hãy khởi chạy viewModelFactory. Hãy dùng ScoreViewModelFactory. Chuyển điểm số cuối cùng từ gói đối số làm thông số hàm dựng cho ScoreViewModelFactory().
viewModelFactory = ScoreViewModelFactory(ScoreFragmentArgs.fromBundle(arguments!!).score)
  1. Trong onCreateView(), sau khi khởi chạy viewModelFactory, hãy khởi tạo đối tượng viewModel. Gọi phương thức ViewModelProviders.of(), chuyển bối cảnh của điểm số liên kết và viewModelFactory. Thao tác này sẽ tạo đối tượng ScoreViewModel bằng phương thức gốc đượ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 chế độ xem 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 của bạn và chơi trò chơi. Chuyển qua 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 mảnh điểm hiện hiển thị điểm cuối cùng.

  1. Không bắt buộc: Kiểm tra nhật ký của ScoreViewModel trong Logcat bằng cách lọc trên ScoreViewModel. Giá trị điểm phải 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 một hàm dựng đã tạo thông số cho 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 Thành phần cấu trúc Android, ViewModel. Bạn đã giải quyết được vấn đề trong 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 một hàm dựng đã tạo thông số để tạo ViewModel bằng cách sử dụng giao diện ViewModelFactory.

Dự án Android Studio: GuessTheWord

  • Nguyên tắc cấu trúc ứng dụng của Android khuyên bạn nên phân tách các lớp có trách nhiệm khác nhau.
  • Bộ đ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 như Activity hoặc Fragment. Bộ điều khiển giao diện người dùng chỉ được chứa logic xử lý các lượt tương tác với giao diện người dùng và hệ điều hành; các bộ điều khiển đó không được chứa dữ liệu hiển thị trong giao diện người dùng. Bạn cần đặt dữ liệu đó trong 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 cho phép dữ liệu tồn tại 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 Thành phần cấu trúc Android được đề xuất.
  • ViewModelProvider.Factory là giao diện mà bạn có thể sử 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 bản sao ViewModel lưu giữ dữ liệu của bộ điều khiển:

Bộ điều khiển giao diện người dùng

ViewModel

Một ví dụ về 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 sẽ 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 xử lý lượt nhấp.

Chứa mã để xử lý dữ liệu.

Bị hủy bỏ và được tạo lại trong mỗi lần thay đổi cấu hình.

Chỉ bị hủy bỏ khi bộ điều khiển giao diện người dùng liên kết biến mất vĩnh viễn – cho một hoạt động, khi hoạt động kết thúc hoặc cho một mảnh, khi mảnh được tách ra.

Chứa lượt xem.

Không được bao gồm mục tham chiếu đến hoạt động, mảnh hoặc chế độ xem vì chúng không tồn tại thay đổi cấu hình, nhưng ViewModel làm được như vậy.

Chứa mục tham chiếu đến ViewModel được liên kết.

Không chứa bất kỳ tham chiếu nào đến bộ điều khiển giao diện người dùng được liên kết.

Khóa học từ Udacity:

Tài liệu dành cho nhà phát triển Android:

Các tài liệu khác:

Phần này liệt kê các bài tập về nhà có thể được giao cho học viên đang làm việc qua lớp học lập trình này trong khóa học do người hướng dẫn tổ chức. Người hướng dẫn có thể làm những việc sau:

  • Giao bài tập về nhà nếu được yêu cầu.
  • Trao đổi với học viên 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 những đề xuất này ít hay nhiều tùy ý. Do đó, họ có thể thoải mái giao 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ự mình làm việc qua lớp học lập trình này, hãy thoải mái sử dụng các bài tập về nhà này để kiểm tra kiến thức của bạn.

Trả lời những câu hỏi này

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à những người 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 khóa học này, hãy xem trang đích của các lớp học lập trình cơ bản về Android Kotlin.