ViewModel dùng chung giữa các mảnh

1. Trước khi bắt đầu

Bạn đã tìm hiểu cách sử dụng các hoạt động (activity), mảnh (fragment), ý định (intent), mối liên kết dữ liệu (data binding) và thành phần điều hướng (navigation) cũng như nắm được kiến thức cơ bản về các thành phần kiến trúc. Trong lớp học lập trình này, bạn sẽ tổng hợp toàn bộ kiến thức và xử lý một mẫu nâng cao, đó là ứng dụng đặt bánh cupcake.

Bạn sẽ tìm hiểu cách sử dụng ViewModel dùng chung để chia sẻ dữ liệu giữa các mảnh của cùng một hoạt động đồng thời tìm hiểu các khái niệm mới như phép biến đổi LiveData.

Điều kiện tiên quyết

  • Dễ dàng đọc hiểu bố cục Android dạng XML
  • Quen thuộc với các khái niệm cơ bản về thành phần Điều hướng (Navigation) của Jetpack
  • Có thể tạo biểu đồ điều hướng thể hiện đích đến của mảnh trong ứng dụng
  • Từng sử dụng các mảnh trong một hoạt động
  • Có thể tạo ViewModel để lưu trữ dữ liệu ứng dụng
  • Có thể sử dụng tính năng liên kết dữ liệu với LiveData để đảm bảo giao diện người dùng đồng bộ với dữ liệu ứng dụng trong ViewModel

Kiến thức bạn sẽ học được

  • Cách triển khai các phương pháp kiến trúc ứng dụng được đề xuất cho trường hợp sử dụng nâng cao hơn
  • Cách sử dụng ViewModel dùng chung giữa các mảnh hoạt động
  • Cách áp dụng phép biến đổi LiveData

Sản phẩm bạn sẽ tạo ra

  • Ứng dụng Cupcake thể hiện quy trình đặt bánh cupcake, cho phép người dùng lựa chọn hương vị, số lượng và ngày lấy bánh.

Bạn cần có

  • Một máy tính đã cài đặt Android Studio.
  • Mã khởi động cho ứng dụng Cupcake.

2. Tổng quan về ứng dụng khởi động

Tổng quan về ứng dụng Cupcake

Ứng dụng đặt bánh cupcake cho thấy cách thiết kế và triển khai một ứng dụng đặt hàng trực tuyến. Kết thúc khoá học này, bạn sẽ có được ứng dụng Cupcake hoàn thiện với các màn hình sau. Người dùng có thể chọn số lượng, hương vị và các tuỳ chọn khác cho đơn đặt hàng bánh cupcake.

732881cfc463695d.png

Tải mã khởi động cho lớp học lập trình này

Lớp học lập trình này sẽ cung cấp mã khởi động, cho phép bạn mở rộng các tính năng được dạy trong lớp học lập trình này. Mã khởi động có chứa mã mà bạn đã quen thuộc trong các lớp học lập trình trước đây.

Nếu tải mã khởi động xuống qua GitHub, hãy lưu ý tên thư mục của dự án là android-basics-kotlin-cupcake-app-starter. Chọn thư mục này khi bạn mở dự án trong Android Studio.

Để lấy mã cho lớp học lập trình này và mở trong Android Studio, hãy thực hiện các bước sau.

Lấy mã

  1. Nhấp vào URL được cung cấp. Thao tác này sẽ mở trang GitHub của dự án trong một trình duyệt.
  2. Trên trang GitHub của dự án, hãy nhấp vào nút Code (Mã), một hộp thoại sẽ xuất hiện.

5b0a76c50478a73f.png

  1. Trong hộp thoại này, hãy nhấp vào nút Download ZIP (Tải tệp ZIP xuống) để lưu dự án vào máy tính. Chờ quá trình tải xuống hoàn tất.
  2. Xác định vị trí của tệp trên máy tính (thường nằm trong thư mục Downloads (Tệp đã tải xuống)).
  3. Nhấp đúp vào tệp ZIP để giải nén. Thao tác này sẽ tạo một thư mục mới chứa các tệp dự án.

Mở dự án trong Android Studio

  1. Khởi động Android Studio.
  2. Trong cửa sổ Welcome to Android Studio (Chào mừng bạn đến với Android Studio), hãy nhấp vào Open an existing Android Studio project (Mở một dự án hiện có trong Android Studio).

36cc44fcf0f89a1d.png

Lưu ý: Nếu Android Studio đã mở sẵn thì thay vào đó, hãy chọn tuỳ chọn sau đây trong trình đơn File > New > Import Project (Tệp > Mới > Nhập dự án).

21f3eec988dcfbe9.png

  1. Trong hộp thoại Import Project (Nhập dự án), hãy chuyển đến nơi chứa thư mục dự án đã giải nén (thường nằm trong thư mục Downloads (Tệp đã tải xuống)).
  2. Nhấp đúp vào thư mục dự án đó.
  3. Chờ Android Studio mở dự án.
  4. Nhấp vào nút Run (Chạy) 11c34fc5e516fb1c.png để xây dựng và chạy ứng dụng. Hãy đảm bảo ứng dụng được dựng như mong đợi.
  5. Duyệt qua các tệp dự án trong cửa sổ công cụ Project (Dự án) để xem cách ứng dụng được thiết lập.

Tìm hiểu mã khởi động

  1. Mở dự án đã tải xuống trong Android Studio. Tên thư mục của dự án là android-basics-kotlin-cupcake-app-starter. Sau đó, hãy chạy ứng dụng.
  2. Duyệt qua các tệp để tìm hiểu mã khởi động. Đối với các tệp bố cục, bạn có thể sử dụng tuỳ chọn Split (Chia tách) ở góc trên cùng bên phải để cùng lúc xem trước bố cục và mã XML.
  3. Khi biên dịch và chạy ứng dụng, bạn sẽ nhận thấy ứng dụng chưa hoàn thiện. Các nút trên ứng dụng chưa có nhiều tác dụng (ngoại trừ việc hiện thông báo Toast), đồng thời bạn không thể di chuyển đến các mảnh khác.

Sau đây là hướng dẫn từng bước về các tệp quan trọng trong dự án.

MainActivity:

MainActivity có mã tương tự với mã được tạo mặc định, dùng để thiết lập khung hiển thị nội dung của hoạt động dưới dạng activity_main.xml. Mã này sử dụng một hàm khởi tạo có tham số AppCompatActivity(@LayoutRes int contentLayoutId) với chức năng lấy một bố cục được tăng cường trong super.onCreate(savedInstanceState).

Mã trong lớp (class) MainActivity

class MainActivity : AppCompatActivity(R.layout.activity_main)

giống với mã sau đây khi sử dụng hàm khởi tạo mặc định AppCompatActivity:

class MainActivity : AppCompatActivity() {

   override fun onCreate(savedInstanceState: Bundle?) {
       super.onCreate(savedInstanceState)
       setContentView(R.layout.activity_main)
   }
}

Bố cục (thư mục res/layout):

Thư mục tài nguyên layout chứa các tệp bố cục mảnh (fragment layout) và hoạt động (activity). Đây là các tệp bố cục đơn giản và cú pháp XML cũng quen thuộc như bạn từng thấy trong các lớp học lập trình trước đây.

  • fragment_start.xml là màn hình xuất hiện đầu tiên trên ứng dụng. Trên màn hình này có hình ảnh bánh cupcake và 3 nút (button) để chọn số lượng bánh cần đặt: 1 bánh cupcake, 6 bánh cupcake và 12 bánh cupcake.
  • fragment_flavor.xml cho thấy danh sách hương vị bánh cupcake dưới dạng các nút chọn (radio button) và một nút (button) Next (Tiếp theo).
  • fragment_pickup.xml cung cấp một tuỳ chọn để chọn ngày lấy hàng và nút Next (Tiếp theo) để chuyển đến màn hình tóm tắt đơn đặt hàng.
  • fragment_summary.xml thể hiện thông tin tóm tắt về đơn đặt hàng, chẳng hạn như số lượng, hương vị và nút để gửi đơn đặt hàng đến một ứng dụng khác.

Lớp mảnh (fragment class):

  • StartFragment.kt là màn hình xuất hiện đầu tiên trong ứng dụng. Lớp này chứa mã liên kết khung hiển thị (view binding code) và một trình xử lý lượt nhấp (click handler) cho 3 nút.
  • Các lớp FlavorFragment.kt, PickupFragment.ktSummaryFragment.kt chủ yếu chứa mã nguyên mẫu và trình xử lý lượt nhấp cho nút Next (Tiếp theo) hoặc Send Order to Another App (Gửi đơn đặt hàng đến một ứng dụng khác) để cho thấy các thông báo ngắn (toast message).

Tài nguyên (thư mục res):

  • Thư mục drawable chứa tài nguyên để hiện chiếc bánh cupcake trên màn hình đầu tiên của ứng dụng cũng như chứa các tệp biểu tượng trình chạy (launcher icon).
  • navigation/nav_graph.xml chứa 4 đích đến cho các mảnh (startFragment, flavorFragment, pickupFragmentsummaryFragment) không có Actions (Thao tác). Đối tượng này sẽ được định nghĩa ở các phần sau này trong lớp học lập trình này.
  • Thư mục values chứa thông tin về màu sắc, kích thước, chuỗi, kiểu định dạng và giao diện (theme) dùng để tuỳ chỉnh giao diện ứng dụng. Chắc là bạn đã quen với những loại tài nguyên này trong các lớp học lập trình trước đây.

3. Hoàn thiện Biểu đồ điều hướng (Navigation Graph)

Trong nhiệm vụ này, bạn sẽ kết nối các màn hình của ứng dụng Cupcake với nhau và hoàn thiện phần điều hướng ứng dụng.

Bạn còn nhớ chúng ta cần những gì để sử dụng thành phần Điều hướng (Navigation) không? Hãy làm theo hướng dẫn này để ôn lại cách thiết lập dự án và ứng dụng của bạn để:

  • Sử dụng thư viện Jetpack Navigation
  • Thêm NavHost vào hoạt động (activity)
  • Tạo biểu đồ điều hướng
  • Thêm đích đến của mảnh vào biểu đồ điều hướng

Kết nối các đích đến trong biểu đồ điều hướng

  1. Trong Android Studio, trong cửa sổ Project (Dự án), hãy mở tệp res > navigation > nav_map.xml. Chuyển sang thẻ Design (Thiết kế) nếu chưa chọn.

28c2c94eb97e2f0.png

  1. Thao tác này sẽ mở ra Navigation Editor (Trình chỉnh sửa chế độ điều hướng) để thể hiện biểu đồ điều hướng cho ứng dụng của bạn. Bạn sẽ thấy ứng dụng có sẵn 4 mảnh.

fdce89b318218ea6.png

  1. Kết nối các đích đến của mảnh trong biểu đồ điều hướng. Tạo một thao tác từ startFragment đến flavorFragment, một kết nối từ flavorFragment đến pickupFragment và một kết nối từ pickupFragment đến summaryFragment. Làm theo các bước tiếp theo nếu bạn cần hướng dẫn chi tiết hơn.
  2. Di chuột qua mảnh startFragment cho đến khi bạn thấy đường viền màu xám xung quanh mảnh này và vòng tròn màu xám xuất hiện ở điểm giữa cạnh bên phải mảnh. Nhấp vào vòng tròn đó, kéo đến mảnh flavorFragment rồi nhả chuột.

d014c1b710c1088d.png

  1. Mũi tên giữa hai mảnh cho biết kết nối được thiết lập thành công, nghĩa là bạn có thể điều hướng từ startFragment đến flavorFragment. Đây được gọi là Thao tác điều hướng (Navigation action), một thao tác mà bạn từng tìm hiểu trong một lớp học lập trình trước đây.

65c7d993b98c9dea.png

  1. Tương tự như vậy, hãy thêm các thao tác điều hướng từ flavorFragment đến pickupFragment và từ pickupFragment đến summaryFragment. Khi tạo xong các thao tác điều hướng, biểu đồ điều hướng hoàn thiện sẽ có dạng như sau.

724eb8992a1a9381.png

  1. Ba thao tác bạn mới tạo cũng phải được phản ánh trong ngăn Component Tree (Cây thành phần).

e4ee54469f5ff1a4.png

  1. Khi định nghĩa biểu đồ điều hướng, bạn cũng nên chỉ định đích đến bắt đầu (start destination). Hiện tại, bạn có thể thấy startFragment có biểu tượng ngôi nhà nhỏ bên cạnh.

739d4ddac561c478.png

Tức là startfragment sẽ là mảnh xuất hiện đầu tiên trong NavHost. Hãy xem đây là hành vi chúng ta muốn ứng dụng thực hiện. Về sau, bạn luôn có thể thay đổi đích bắt đầu bằng cách nhấp chuột phải vào một mảnh rồi chọn tuỳ chọn trình đơn Set as Start Destination (Đặt làm đích đến bắt đầu).

bf3cfa7841476892.png

Tiếp theo, bạn sẽ thêm mã để điều hướng từ startFragment đến flavorFragment bằng cách nhấn vào các nút trong mảnh đầu tiên, thay vì hiện thông báo Toast. Dưới đây là mã tham khảo cho bố cục mảnh bắt đầu (start fragment). Bạn sẽ chuyển số lượng bánh cupcake đến mảnh hương vị (flavor fragment) trong nhiệm vụ tiếp theo.

867d8e4c72078f76.png

  1. Trong cửa sổ Project (Dự án), hãy mở tệp Kotlin trong app > robots > com.example.cake > StartFragment
  2. Trong phương thức onViewCreated(), hãy lưu ý rằng trình nghe lượt nhấp được đặt trên 3 nút. Khi mỗi nút được nhấn, phương thức orderCupcake() sẽ được gọi với tham số là số lượng bánh cupcake (1, 6 hoặc 12 bánh cupcake).

Mã tham chiếu:

orderOneCupcake.setOnClickListener { orderCupcake(1) }
orderSixCupcakes.setOnClickListener { orderCupcake(6) }
orderTwelveCupcakes.setOnClickListener { orderCupcake(12) }
  1. Trong phương thức orderCupcake(), hãy thay thế mã để hiện thông báo ngắn bằng mã để điều hướng đến mảnh hương vị. Tìm phương thức NavController thông qua phương thức findNavController() rồi gọi navigate() trên phương thức này, với tham số truyền vào là mã nhận dạng thao tác R.id.action_startFragment_to_flavorFragment. Đảm bảo rằng mã nhận dạng thao tác này khớp với thao tác bạn đã khai báo trong nav_graph.xml.

Thay thế

fun orderCupcake(quantity: Int) {
    Toast.makeText(activity, "Ordered $quantity cupcake(s)", Toast.LENGTH_SHORT).show()
}

bằng

fun orderCupcake(quantity: Int) {
   findNavController().navigate(R.id.action_startFragment_to_flavorFragment)
}
  1. Thêm lệnh import androidx.navigation.fragment.findNavController hoặc bạn có thể chọn trong số các tuỳ chọn mà Android Studio đưa ra.

2a087f53a77765a6.png

Thêm thành phần Điều hướng (Navigation) vào các mảnh hương vị và lấy hàng

Tương tự như nhiệm vụ trước, trong nhiệm vụ này, bạn sẽ thêm thành phần điều hướng vào các mảnh: hương vị và lấy hàng.

3b351067bf4926b7.png

  1. Mở app > java > com.example.cupcake > FlavorFragment.kt. Hãy lưu ý rằng phương thức được gọi trong trình nghe lượt nhấp trên nút Next (Tiếp theo) là phương thức goToNextScreen().
  2. Trong tệp FlavorFragment.kt, bên trong phương thức goToNextScreen(), hãy thay thế mã cho thấy thông báo ngắn để điều hướng đến mảnh lấy hàng (pickup fragment). Sử dụng mã nhận dạng thao tác R.id.action_flavorFragment_to_pickupFragment. Hãy nhớ rằng mã này phải khớp với thao tác bạn đã khai báo trong nav_graph.xml.
fun goToNextScreen() {
    findNavController().navigate(R.id.action_flavorFragment_to_pickupFragment)
}

Hãy nhớ import androidx.navigation.fragment.findNavController.

  1. Tương tự như PickupFragment.kt, bên trong phương thức goToNextScreen(), hãy thay thế mã hiện tại để điều hướng đến mảnh tóm tắt (summary fragment).
fun goToNextScreen() {
    findNavController().navigate(R.id.action_pickupFragment_to_summaryFragment)
}

Nhập androidx.navigation.fragment.findNavController.

  1. Chạy ứng dụng. Đảm bảo các nút hoạt động đúng khi di chuyển giữa các màn hình. Thông tin hiện trên mỗi mảnh có thể chưa đầy đủ. Đừng lo, bạn sẽ điền dữ liệu chính xác vào các mảnh đó trong các bước tiếp theo.

96b33bf7a5bd8050.png

Cập nhật tiêu đề trong thanh ứng dụng

Khi bạn thao tác trong ứng dụng, hãy chú ý đến tiêu đề trên thanh ứng dụng. Bạn luôn thấy tiêu đề Cupcake.

Để cải thiện trải nghiệm người dùng, bạn nên cung cấp tiêu đề phù hợp hơn dựa trên chức năng của mảnh hiện tại.

Thay đổi tiêu đề trong thanh ứng dụng (còn gọi là thanh thao tác) cho từng mảnh bằng cách sử dụng NavController và hiện nút Up (Mũi tên lên) (←).

b7657cdc50cfeab0.png

  1. Trong MainActivity.kt, hãy ghi đè phương thức onCreate() để thiết lập bộ điều khiển điều hướng. Tạo thực thể của lớp NavController qua NavHostFragment.
  2. Gọi hàm setupActionBarWithNavController(navController) với tham số truyền vào là thực thể của NavController. Thao tác này sẽ thực hiện những việc sau: Hiện tiêu đề trong thanh ứng dụng dựa trên nhãn của đích đến và hiện nút Up (Mũi tên lên) mỗi khi bạn không ở đích đến trên cùng.
class MainActivity : AppCompatActivity(R.layout.activity_main) {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        val navHostFragment = supportFragmentManager
                .findFragmentById(R.id.nav_host_fragment) as NavHostFragment
        val navController = navHostFragment.navController

        setupActionBarWithNavController(navController)
    }
}
  1. Thêm các lệnh nhập cần thiết theo gợi ý của Android Studio.
import android.os.Bundle
import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.ui.setupActionBarWithNavController
  1. Đặt tiêu đề thanh ứng dụng cho mỗi mảnh. Mở navigation/nav_graph.xml rồi chuyển sang thẻ Code (Mã).
  2. Trong tệp nav_graph.xml, hãy chỉnh sửa thuộc tính android:label cho từng đích đến của mảnh. Sử dụng các tài nguyên chuỗi đã được khai báo trong ứng dụng khởi động như dưới đây.

Với mảnh bắt đầu (start fragment), hãy sử dụng @string/app_name với giá trị Cupcake.

Với mảnh hương vị (flavor fragment), hãy sử dụng @string/choose_flavor với giá trị Choose Flavor.

Với mảnh lấy hàng (pickup fragment), hãy sử dụng @string/choose_pickup_date với giá trị Choose Pickup Date.

Với mảnh tóm tắt (summary fragment), hãy sử dụng @string/order_summary với giá trị Order Summary.

<navigation ...>
    <fragment
        android:id="@+id/startFragment"
        ...
        android:label="@string/app_name" ... >
        <action ... />
    </fragment>
    <fragment
        android:id="@+id/flavorFragment"
        ...
        android:label="@string/choose_flavor" ... >
        <action ... />
    </fragment>
    <fragment
        android:id="@+id/pickupFragment"
        ...
        android:label="@string/choose_pickup_date" ... >
        <action ... />
    </fragment>
    <fragment
        android:id="@+id/summaryFragment"
        ...
        android:label="@string/order_summary" ... />
</navigation>
  1. Chạy ứng dụng. Lưu ý rằng tiêu đề trong thanh ứng dụng sẽ thay đổi khi bạn di chuyển đến từng đích đến của mảnh. Ngoài ra, hãy lưu ý rằng giờ đây nút Up (Trước) (mũi tên ←) đang xuất hiện trong thanh ứng dụng. Nếu bạn nhấn vào nút này, sẽ không có điều gì xảy ra. Bạn sẽ triển khai hành vi của nút Up (Trước) trong lớp học lập trình tiếp theo.

89e0ea37d4146271.png

4. Tạo ViewModel dùng chung

Hãy chuyển sang phần điền dữ liệu chính xác cho từng mảnh. Bạn sẽ sử dụng một ViewModel dùng chung để lưu dữ liệu của ứng dụng vào một ViewModel duy nhất. Nhiều mảnh trong ứng dụng sẽ truy cập ViewModel dùng chung này theo phạm vi hoạt động của các mảnh đó.

Đây là một cách dùng phổ biến nhằm chia sẻ dữ liệu giữa các mảnh trong hầu hết ứng dụng sản xuất. Ví dụ: trong phiên bản cuối cùng (của lớp học lập trình này) cho ứng dụng Cupcake (xem ảnh chụp màn hình bên dưới), người dùng chọn số lượng bánh cupcake trong màn hình đầu tiên. Trong màn hình thứ hai, giá tiền sẽ được tính và hiển thị dựa trên số lượng bánh đã chọn. Tương tự như vậy, các dữ liệu khác của ứng dụng như dữ liệu về hương vị và ngày lấy hàng sẽ được sử dụng trong màn hình tóm tắt đơn đặt hàng.

3b6a68cab0b9ee2.png

Sau khi xem xét các tính năng của ứng dụng, bạn sẽ thấy rằng thông tin đặt hàng nên được lưu trữ trong một ViewModel duy nhất, để có thể được chia sẻ giữa các mảnh trong hoạt động này. Hãy nhớ rằng ViewModel là một phần của Bộ thành phần cấu trúc Android. Dữ liệu ứng dụng lưu trong ViewModel được lưu giữ lại trong quá trình thay đổi cấu hình. Để thêm ViewModel vào ứng dụng, bạn cần tạo một lớp mới được mở rộng từ lớp ViewModel.

Tạo OrderViewModel

Trong nhiệm vụ này, bạn sẽ tạo một ViewModel dùng chung cho ứng dụng Cupcake với tên gọi OrderViewModel. Đồng thời, bạn sẽ thêm dữ liệu ứng dụng dưới dạng các thuộc tính bên trong ViewModel cũng như thêm các phương thức để cập nhật và chỉnh sửa dữ liệu. Sau đây là các thuộc tính của lớp:

  • Số lượng đặt hàng (Integer)
  • Hương vị bánh cupcake (String)
  • Ngày lấy hàng (String)
  • Giá (Double)

Làm theo các phương pháp hay nhất về ViewModel

Trong ViewModel, bạn không nên khai báo dữ liệu mô hình xem (mô hình xem) dưới dạng biến public. Nếu không, các lớp bên ngoài có thể sửa đổi dữ liệu ứng dụng theo cách bất thường mà từ đó phát sinh các tình huống hiếm gặp không mong đợi trong ứng dụng. Thay vào đó, hãy đặt các thuộc tính có thể thay đổi (mutable) này dưới dạng private, triển khai thuộc tính dự phòng và hiện phiên bản bất biến cho mỗi thuộc tính dưới dạng public (nếu cần). Theo quy ước, cần sử dụng dấu gạch dưới (_) trước tên của các thuộc tính có thể biến đổi private.

Sau đây là các phương thức để cập nhật các thuộc tính trên, tuỳ thuộc vào lựa chọn của người dùng:

  • setQuantity(numberCupcakes: Int)
  • setFlavor(desiredFlavor: String)
  • setDate(pickupDate: String)

Bạn không cần phương thức setter cho phần giá cả vì giá sẽ được tính trong OrderViewModel bằng các thuộc tính khác. Dưới đây là các bước hướng dẫn cách triển khai ViewModel dùng chung.

Bạn sẽ tạo một gói mới trong dự án có tên model và bổ sung lớp OrderViewModel. Thao tác này sẽ giúp tách biệt mã của mô hình xem với phần còn lại của mã giao diện người dùng (các mảnh và hoạt động). Phương pháp lập trình hay nhất là tách mã thành các gói theo chức năng.

  1. Trong cửa sổ Project (Dự án) của Android Studio, hãy nhấp chuột phải vào com.example.cake > New (Mới) > Package (Gói).
  2. Hộp thoại New Package (Gói mới) sẽ xuất hiện, hãy đặt tên gói là com.example.cupcake.model.

d958ee5f3d2aef5a.png

  1. Tạo lớp Kotlin OrderViewModel trong gói model. Trong cửa sổ Project (Dự án), hãy nhấp chuột phải vào gói model rồi chọn New > Kotlin File/Class (Mới > Lớp/Tệp Kotlin). Trong hộp thoại mới này, hãy đặt tên tệp là OrderViewModel.

fc68c1d3861f1cca.png

  1. Trong tệp OrderViewModel.kt, hãy thay đổi chữ ký của lớp để mở rộng ViewModel.
import androidx.lifecycle.ViewModel

class OrderViewModel : ViewModel() {

}
  1. Bên trong lớp OrderViewModel, hãy thêm các thuộc tính đã thảo luận ở trên dưới dạng private val.
  2. Thay đổi loại thuộc tính thành LiveData và bổ sung các trường dự phòng để có thể quan sát các thuộc tính này và cập nhật giao diện người dùng khi dữ liệu nguồn trong mô hình hiển thị thay đổi.
private val _quantity = MutableLiveData<Int>(0)
val quantity: LiveData<Int> = _quantity

private val _flavor = MutableLiveData<String>("")
val flavor: LiveData<String> = _flavor

private val _date = MutableLiveData<String>("")
val date: LiveData<String> = _date

private val _price = MutableLiveData<Double>(0.0)
val price: LiveData<Double> = _price

Bạn sẽ cần nhập các lớp sau:

import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
  1. Trong lớp OrderViewModel, hãy thêm các phương thức đã thảo luận ở trên. Bên trong các phương thức này, hãy gán đối số được truyền vào cho các thuộc tính có thể biến đổi.
  2. Do các phương thức setter này cần được gọi từ bên ngoài mô hình hiển thị, nên bạn hãy giữ nguyên các phương thức này dưới dạng public (tức là không cần private hoặc đối tượng sửa đổi chế độ hiển thị khác trước từ khoá fun). Chỉ định chế độ hiển thị mặc định trong Kotlin là public.
fun setQuantity(numberCupcakes: Int) {
    _quantity.value = numberCupcakes
}

fun setFlavor(desiredFlavor: String) {
    _flavor.value = desiredFlavor
}

fun setDate(pickupDate: String) {
    _date.value = pickupDate
}
  1. Tạo và chạy ứng dụng để đảm bảo không gặp vấn đề về lỗi biên dịch. Hiện chưa có thay đổi nào trên giao diện người dùng.

Bạn làm tốt lắm! Bây giờ bạn đã có điểm khởi đầu cho mô hình xem. Bạn sẽ dần thêm vào lớp này các thuộc tính và phương thức mà bạn nhận thấy là cần thiết để tạo thêm tính năng cho ứng dụng.

Nếu bạn thấy tên lớp (class), tên thuộc tính (property) hoặc tên phương thức (method) có phông chữ màu xám trong Android Studio, thì đó là lỗi đã lường trước. Tức là lớp, thuộc tính hoặc phương thức đó hiện chưa được sử dụng, những sẽ được sử dụng về sau! Nội dung này sẽ được đề cập trong phần tiếp theo.

5. Sử dụng ViewModel để cập nhật giao diện người dùng

Trong nhiệm vụ này, bạn sẽ sử dụng mô hình xem dùng chung đã tạo để cập nhật giao diện người dùng của ứng dụng. Điểm khác biệt chính trong việc triển khai mô hình xem dùng chung là cách truy cập mô hình xem qua bộ điều khiển giao diện người dùng. Bạn sẽ sử dụng thực thể của hoạt động thay vì thực thể của mảnh và sẽ tìm hiểu cách thực hiện điều này trong các phần tiếp theo.

Như vậy tác là bạn có thể chia sẻ mô hình xem giữa các mảnh. Mỗi mảnh đều có thể truy cập vào mô hình xem để kiểm tra thông tin của đơn đặt hàng hoặc cập nhật một số dữ liệu trong mô hình xem đó.

Cập nhật StartFragment để sử dụng mô hình xem

Để sử dụng mô hình xem dùng chung trong StartFragment, bạn sẽ khởi động OrderViewModel bằng cách sử dụng activityViewModels() thay vì dùng lớp uỷ quyền (delegate class) viewModels().

  • viewModels() cung cấp cho bạn thực thể của ViewModel trong phạm vi mảnh hiện tại. Thực thể này thay đổi tuỳ theo mảnh.
  • activityViewModels() cung cấp cho bạn thực thể của ViewModel trong phạm vi hoạt động hiện tại. Do đó, thực thể này sẽ giống nhau giữa nhiều mảnh trong cùng một hoạt động.

Sử dụng tính năng uỷ quyền thuộc tính của Kotlin

Trong Kotlin, mỗi thuộc tính có thể thay đổi (var) đều có các hàm getter và setter mặc định được tạo tự động cho thuộc tính đó. Hàm setter và getter được gọi khi bạn gán giá trị hoặc đọc giá trị của thuộc tính. (Đối với thuộc tính chỉ đọc (val), chỉ có hàm getter được tạo mặc định. Hàm getter này được gọi khi bạn đọc giá trị của thuộc tính chỉ đọc.)

Tính năng uỷ quyền thuộc tính trong Kotlin sẽ giúp bạn chuyển giao nhiệm vụ của hàm getter-setter cho một lớp khác.

Lớp này (được gọi là lớp uỷ quyền (delegate class)) cung cấp các hàm getter và setter của thuộc tính đó và xử lý các thay đổi trên thuộc tính đó.

Một thuộc tính uỷ quyền được xác định bằng mệnh đề by và một thực thể của lớp uỷ quyền:

// Syntax for property delegation
var <property-name> : <property-type> by <delegate-class>()
  1. Trong lớp StartFragment, hãy tham chiếu đến mô hình xem dùng chung dưới dạng một biến của lớp. Sử dụng đối tượng uỷ quyền thuộc tính by activityViewModels() Kotlin trong thư viện fragment-ktx.
private val sharedViewModel: OrderViewModel by activityViewModels()

Bạn có thể cần các lệnh nhập sau:

import androidx.fragment.app.activityViewModels
import com.example.cupcake.model.OrderViewModel
  1. Lặp lại bước trên cho các lớp FlavorFragment, PickupFragmentSummaryFragment. Bạn sẽ sử dụng thực thể của sharedViewModel này trong các phần sau của lớp học lập trình.
  2. Trở lại với lớp StartFragment, bây giờ bạn có thể sử dụng mô hình xem. Ở phần đầu của phương thức orderCupcake(), hãy gọi phương thức setQuantity() trong mô hình xem dùng chung để cập nhật số lượng trước khi chuyển đến mảnh chọn hương vị.
fun orderCupcake(quantity: Int) {
    sharedViewModel.setQuantity(quantity)
    findNavController().navigate(R.id.action_startFragment_to_flavorFragment)
}
  1. Trong lớp OrderViewModel, hãy thêm phương thức dưới đây để kiểm tra xem bạn đã thiết lập hương vị cho đơn đặt hàng hay chưa. Bạn sẽ sử dụng phương thức này trong lớp StartFragment ở bước sau.
fun hasNoFlavorSet(): Boolean {
    return _flavor.value.isNullOrEmpty()
}
  1. Trong lớp StartFragment, bên trong phương thức orderCupcake(), sau khi đặt số lượng, hãy đặt hương vị mặc định là Vanilla nếu chưa có hương vị nào được chọn, trước khi chuyển đến mảnh hương vị. Phương thức hoàn chỉnh của bạn sẽ có dạng như sau:
fun orderCupcake(quantity: Int) {
    sharedViewModel.setQuantity(quantity)
    if (sharedViewModel.hasNoFlavorSet()) {
        sharedViewModel.setFlavor(getString(R.string.vanilla))
    }
    findNavController().navigate(R.id.action_startFragment_to_flavorFragment)
}
  1. Chạy ứng dụng để đảm bảo không có lỗi biên dịch. Tuy nhiên, sẽ chưa có thay đổi gì trên giao diện người dùng.

6. Dùng ViewModel để liên kết dữ liệu

Tiếp theo, bạn sẽ sử dụng tính năng liên kết dữ liệu để liên kết dữ liệu của mô hình xem với giao diện người dùng. Đồng thời, bạn sẽ cập nhật mô hình xem dùng chung dựa trên lựa chọn của người dùng trên giao diện ứng dụng.

Làm mới mối liên kết dữ liệu

Hãy lưu ý rằng Data Binding Library (Thư viện liên kết dữ liệu) là một phần của Android Jetpack. Cơ chế liên kết dữ liệu mang mục đích liên kết các thành phần giao diện người dùng trong bố cục với các nguồn dữ liệu trong ứng dụng đang sử dụng định dạng khai báo. Nói đơn giản thì liên kết dữ liệu là quá trình liên kết dữ liệu (từ mã) với khung hiển thị (view) + liên kết khung hiển thị (liên kết khung hiển thị với mã). Việc thiết lập và cập nhật những mối liên kết này được thực hiện tự động, giúp giảm khả năng xảy ra lỗi nếu bạn quên cập nhật giao diện người dùng theo cách thủ công qua mã.

Cập nhật hương vị theo lựa chọn của người dùng

  1. Trong layout/fragment_flavor.xml, hãy thêm một thẻ <data> vào trong thẻ <layout> gốc. Thêm biến bố cục có tên là viewModel thuộc loại com.example.cupcake.model.OrderViewModel. Đảm bảo tên gói trong thuộc tính type (loại) khớp với tên gói của lớp mô hình hiển thị dùng chung OrderViewModel trong ứng dụng.
<layout ...>

    <data>
        <variable
            name="viewModel"
            type="com.example.cupcake.model.OrderViewModel" />
    </data>

    <ScrollView ...>

    ...
  1. Tương tự như vậy, hãy lặp lại bước trên cho fragment_pickup.xmlfragment_summary.xml để thêm biến bố cục viewModel. Bạn sẽ sử dụng biến này trong các phần sau. Bạn không cần thêm mã này trong fragment_start.xml vì bố cục này không sử dụng mô hình hiển thị dùng chung.
  2. Trong lớp FlavorFragment, bên trong onViewCreated(), hãy liên kết thực thể của mô hình xem này với thực thể mô hình xem dùng chung trong bố cục. Thêm mã dưới đây vào bên trong khối binding?.apply.
binding?.apply {
    viewModel = sharedViewModel
    ...
}

Áp dụng hàm phạm vi

Đây có thể là lần đầu tiên bạn thấy hàm apply trong Kotlin. apply là một hàm phạm vi trong thư viện Kotlin tiêu chuẩn. Hàm này thực thi một khối mã lệnh trong ngữ cảnh của một đối tượng. Hàm này tạo ra một phạm vi tạm thời và trong phạm vi đó, bạn có thể truy cập vào đối tượng mà không cần dùng tên. Cách sử dụng phổ biến cho hàm apply là để định cấu hình một đối tượng. Những lệnh gọi như vậy có thể hiểu là "áp dụng các phép gán sau cho đối tượng".

Ví dụ:

clark.apply {
    firstName = "Clark"
    lastName = "James"
    age = 18
}

// The equivalent code without apply scope function would look like the following.

clark.firstName = "Clark"
clark.lastName = "James"
clark.age = 18
  1. Lặp lại bước này cho phương thức onViewCreated() bên trong các lớp PickupFragmentSummaryFragment.
binding?.apply {
    viewModel = sharedViewModel
    ...
}
  1. Trong fragment_flavor.xml, hãy sử dụng biến bố cục mới viewModel để đặt thuộc tính checked của các nút chọn dựa trên giá trị flavor trong mô hình hiển thị này. Nếu hương vị thể hiện trên nút chọn giống với hương vị lưu trong mô hình xem, hãy cho thấy nút chọn ở trạng thái đã chọn (checked = true). Biểu thức liên kết thể hiện trạng thái đã đánh dấu của Vanilla RadioButton sẽ có dạng như sau:

@{viewModel.flavor.equals(@string/vanilla)}

Về cơ bản, bạn đang so sánh thuộc tính viewModel.flavor với tài nguyên chuỗi tương ứng thông qua hàm equals để xác định xem trạng thái đã đánh dấu là true (đúng) hay false (sai).

<RadioGroup
   ...>

   <RadioButton
       android:id="@+id/vanilla"
       ...
       android:checked="@{viewModel.flavor.equals(@string/vanilla)}"
       .../>

   <RadioButton
       android:id="@+id/chocolate"
       ...
       android:checked="@{viewModel.flavor.equals(@string/chocolate)}"
       .../>

   <RadioButton
       android:id="@+id/red_velvet"
       ...
       android:checked="@{viewModel.flavor.equals(@string/red_velvet)}"
       .../>

   <RadioButton
       android:id="@+id/salted_caramel"
       ...
       android:checked="@{viewModel.flavor.equals(@string/salted_caramel)}"
       .../>

   <RadioButton
       android:id="@+id/coffee"
       ...
       android:checked="@{viewModel.flavor.equals(@string/coffee)}"
       .../>
</RadioGroup>

Liên kết trình nghe

Liên kết trình nghe (listener binding) là các biểu thức lambda sẽ chạy khi xảy ra một sự kiện, chẳng hạn như sự kiện onClick. Các biểu thức này tương tự các tham chiếu phương thức như textview.setOnClickListener(clickListener) nhưng tính năng liên kết trình nghe cho phép bạn chạy các biểu thức liên kết dữ liệu tuỳ ý.

  1. Trong fragment_flavor.xml, hãy thêm trình nghe sự kiện vào các nút chọn bằng cách sử dụng tính năng liên kết trình nghe. Sử dụng biểu thức lambda không có tham số rồi gọi phương thức viewModel.setFlavor() với tham số truyền vào là tài nguyên chuỗi cho hương vị tương ứng.
<RadioGroup
   ...>

   <RadioButton
       android:id="@+id/vanilla"
       ...
       android:onClick="@{() -> viewModel.setFlavor(@string/vanilla)}"
       .../>

   <RadioButton
       android:id="@+id/chocolate"
       ...
       android:onClick="@{() -> viewModel.setFlavor(@string/chocolate)}"
       .../>

   <RadioButton
       android:id="@+id/red_velvet"
       ...
       android:onClick="@{() -> viewModel.setFlavor(@string/red_velvet)}"
       .../>

   <RadioButton
       android:id="@+id/salted_caramel"
       ...
       android:onClick="@{() -> viewModel.setFlavor(@string/salted_caramel)}"
       .../>

   <RadioButton
       android:id="@+id/coffee"
       ...
       android:onClick="@{() -> viewModel.setFlavor(@string/coffee)}"
       .../>
</RadioGroup>
  1. Chạy ứng dụng và lưu ý về cách chọn mặc định hương vị Vanilla trong mảnh hương vị.

3095e824b4817b98.png

Tuyệt vời! Bây giờ, bạn có thể chuyển sang các mảnh tiếp theo.

7. Cập nhật các mảnh lấy hàng và tóm tắt để sử dụng mô hình xem

Khi di chuyển trong ứng dụng, bạn sẽ thấy rằng trong mảnh lấy hàng, các nhãn tuỳ chọn trong nút chọn đang để trống. Trong nhiệm vụ này, bạn sẽ tính ra 4 ngày người dùng có thể lấy hàng rồi cho thấy trong mảnh lấy hàng. Có nhiều cách để hiện ngày theo định dạng. Dưới đây là một số tiện ích hữu ích để định dạng mà Android cung cấp.

Tạo danh sách cách lấy hàng

Trình định dạng ngày

Nền tảng Android cung cấp một lớp có tên SimpleDateFormat, dùng để định dạng và phân tích cú pháp ngày tháng phân biệt theo ngôn ngữ. Lớp này cho phép định dạng (ngày → văn bản) và phân tích cú pháp ngày tháng (văn bản → ngày).

Bạn có thể tạo một thực thể của lớp SimpleDateFormat với tham số truyền vào là một chuỗi mẫu (pattern string) và miền bản địa (locale):

SimpleDateFormat("E MMM d", Locale.getDefault())

Một chuỗi mẫu như "E MMM d" thể hiện định dạng Ngày và Giờ. Các chữ cái từ 'A' đến 'Z' và từ 'a' đến 'z' là chữ cái đại diện cho các thành phần của chuỗi ngày hoặc giờ. Ví dụ: d đại diện cho ngày trong tháng, y cho năm và M cho tháng. Nếu ngày là ngày 4 tháng 1 năm 2018, chuỗi theo mẫu "EEE, MMM d" sẽ được phân tích cú pháp thành "Wed, Jul 4". Để nắm được danh sách đầy đủ chữ cái mẫu, vui lòng xem tài liệu.

Đối tượng Locale đại diện cho một khu vực địa lý, chính trị hoặc văn hoá cụ thể. Đối tượng này đại diện cho tổ hợp ngôn ngữ/quốc gia/biến thể. Ngôn ngữ được dùng để thay đổi cách trình bày thông tin, chẳng hạn như con số hoặc ngày cho phù hợp với quy ước trong khu vực. Ngày và giờ tuỳ theo miền bản địa vì mỗi khu vực trên thế giới lại có những cách viết ngày và giờ riêng. Bạn sẽ sử dụng phương thức Locale.getDefault() để truy xuất thông tin ngôn ngữ được thiết lập trên thiết bị của người dùng và truyền thông tin đó vào hàm khởi tạo SimpleDateFormat.

Miền bản địa (locale) trong Android là sự kết hợp giữa ngôn ngữ (language) và mã quốc gia (country code). Mã ngôn ngữ là mã số ngôn ngữ theo tiêu chuẩn ISO gồm hai chữ cái viết thường, chẳng hạn như "en" thể hiện tiếng Anh. Mã quốc gia là mã số quốc gia gồm hai chữ cái viết hoa theo tiêu chuẩn ISO, chẳng hạn như "US" thể hiện Hoa Kỳ.

Bây giờ hãy sử dụng SimpleDateFormatLocale để xác định ngày có thể lấy hàng cho ứng dụng Cupcake.

  1. Trong lớp OrderViewModel, hãy thêm hàm dưới đây với tên gọi getPickupOptions() để tạo và trả về danh sách ngày lấy hàng. Trong phương thức này, hãy tạo một biến val có tên là options và khởi động biến đó qua mutableListOf<String>().
private fun getPickupOptions(): List<String> {
   val options = mutableListOf<String>()
}
  1. Tạo một chuỗi định dạng sử dụng SimpleDateFormat với tham số truyền vào là chuỗi mẫu "E MMM d" và ngôn ngữ. Trong chuỗi mẫu, E là viết tắt tên ngày trong tuần và được phân tích cú pháp thành "Tue Dec 10".
val formatter = SimpleDateFormat("E MMM d", Locale.getDefault())

Nhập java.text.SimpleDateFormatjava.util.Locale khi Android Studio nhắc.

  1. Lấy thực thể Calendar rồi gán cho một biến mới. Khai báo biến này là một val. Biến này sẽ chứa ngày và giờ hiện tại. Đồng thời, hãy nhập java.util.Calendar.
val calendar = Calendar.getInstance()
  1. Xây dựng danh sách ngày bắt đầu từ ngày hiện tại và 3 ngày tiếp theo. Do bạn sẽ cần 4 tuỳ chọn ngày lấy hàng, hãy lặp lại khối mã này 4 lần. Khối repeat này sẽ định dạng một ngày, thêm ngày đó vào danh sách tuỳ chọn ngày, sau đó chuyển đến ngày dương lịch tiếp theo.
repeat(4) {
    options.add(formatter.format(calendar.time))
    calendar.add(Calendar.DATE, 1)
}
  1. Trả về options đã cập nhật ở cuối phương thức. Sau đây là phương thức hoàn chỉnh:
private fun getPickupOptions(): List<String> {
   val options = mutableListOf<String>()
   val formatter = SimpleDateFormat("E MMM d", Locale.getDefault())
   val calendar = Calendar.getInstance()
   // Create a list of dates starting with the current date and the following 3 dates
   repeat(4) {
       options.add(formatter.format(calendar.time))
       calendar.add(Calendar.DATE, 1)
   }
   return options
}
  1. Trong lớp OrderViewModel, hãy thêm thuộc tính lớp có tên dateOptions và là một val. Hãy khởi động biến này bằng phương thức getPickupOptions() bạn vừa tạo.
val dateOptions = getPickupOptions()

Cập nhật bố cục để hiện các cách lấy hàng

Giờ đây, bạn đã có 4 ngày có thể lấy hàng trong mô hình xem, hãy cập nhật bố cục fragment_pickup.xml để cho thấy những ngày này. Bạn cũng sẽ sử dụng tính năng liên kết dữ liệu để hiện trạng thái của mỗi nút chọn cũng như để cập nhật ngày trong mô hình xem khi lựa chọn nút chọn khác. Cách triển khai này tương tự như việc liên kết dữ liệu trong mảnh hương vị.

Trong fragment_pickup.xml:

Nút chọn option0 thể hiện dateOptions[0] trong viewModel (hôm nay)

Nút chọn option1 thể hiện dateOptions[1] trong viewModel (ngày mai)

Nút chọn option2 thể hiện dateOptions[2] trong viewModel (ngày kia)

Nút chọn option3 thể hiện dateOptions[3] trong viewModel (3 ngày sau)

  1. Trong fragment_pickup.xml, đối với nút chọn option0, hãy sử dụng biến bố cục mới viewModel để thiết lập thuộc tính checked dựa trên giá trị date trong mô hình hiển thị. So sánh thuộc tính viewModel.date với chuỗi đầu tiên trong danh sách dateOptions. Đây chính là ngày hiện tại. Sử dụng hàm equals để so sánh và biểu thức liên kết cuối cùng sẽ có dạng như sau:

@{viewModel.date.equals(viewModel.dateOptions[0])}

  1. Đối với cùng một nút chọn, hãy thêm trình nghe sự kiện bằng cách sử dụng tính năng liên kết trình nghe cho thuộc tính onClick. Khi nhấp vào tuỳ chọn của nút chọn này, hãy gọi hàm setDate() trên viewModel, truyền vào tham số dateOptions[0].
  2. Đối với cùng một nút chọn, hãy thiết lập giá trị thuộc tính text là chuỗi đầu tiên trong danh sách dateOptions.
<RadioButton
   android:id="@+id/option0"
   ...
   android:checked="@{viewModel.date.equals(viewModel.dateOptions[0])}"
   android:onClick="@{() -> viewModel.setDate(viewModel.dateOptions[0])}"
   android:text="@{viewModel.dateOptions[0]}"
   ...
   />
  1. Lặp lại các bước trên cho các nút chọn khác, sau đó thay đổi chỉ mục của dateOptions cho phù hợp.
<RadioButton
   android:id="@+id/option1"
   ...
   android:checked="@{viewModel.date.equals(viewModel.dateOptions[1])}"
   android:onClick="@{() -> viewModel.setDate(viewModel.dateOptions[1])}"
   android:text="@{viewModel.dateOptions[1]}"
   ... />

<RadioButton
   android:id="@+id/option2"
   ...
   android:checked="@{viewModel.date.equals(viewModel.dateOptions[2])}"
   android:onClick="@{() -> viewModel.setDate(viewModel.dateOptions[2])}"
   android:text="@{viewModel.dateOptions[2]}"
   ... />

<RadioButton
   android:id="@+id/option3"
   ...
   android:checked="@{viewModel.date.equals(viewModel.dateOptions[3])}"
   android:onClick="@{() -> viewModel.setDate(viewModel.dateOptions[3])}"
   android:text="@{viewModel.dateOptions[3]}"
   ... />
  1. Chạy ứng dụng và bạn sẽ thấy các tuỳ chọn ngày lấy hàng trong vài ngày tới. Ảnh chụp màn hình của bạn sẽ thay đổi tuỳ vào ngày hiện tại trên thiết bị của bạn. Hãy lưu ý rằng hiện chưa thiết lập tuỳ chọn mặc định. Bạn sẽ thực hiện việc này trong bước tiếp theo.

b55b3a36e2aa7be6.png

  1. Trong lớp OrderViewModel, hãy tạo một hàm có tên resetOrder() để thiết lập lại các thuộc tính MutableLiveData trong mô hình xem. Gán giá trị ngày hiện tại trong danh sách dateOptions cho _date.value.
fun resetOrder() {
   _quantity.value = 0
   _flavor.value = ""
   _date.value = dateOptions[0]
   _price.value = 0.0
}
  1. Thêm một khối init vào lớp này và gọi phương thức mới resetOrder() qua đó.
init {
   resetOrder()
}
  1. Xoá các giá trị ban đầu khỏi phần khai báo về các thuộc tính trong lớp. Bây giờ, bạn đang sử dụng khối init để khởi tạo các thuộc tính khi một thực thể của lớp OrderViewModel được tạo ra.
private val _quantity = MutableLiveData<Int>()
val quantity: LiveData<Int> = _quantity

private val _flavor = MutableLiveData<String>()
val flavor: LiveData<String> = _flavor

private val _date = MutableLiveData<String>()
val date: LiveData<String> = _date

private val _price = MutableLiveData<Double>()
val price: LiveData<Double> = _price
  1. Chạy lại ứng dụng, lưu ý rằng ngày hôm nay sẽ được chọn theo mặc định.

bfe4f1b82977b4bc.png

Cập nhật mảnh tóm tắt để sử dụng mô hình xem

Tiếp theo, hãy chuyển đến mảnh cuối cùng. Mục đích của mảnh tóm tắt đơn đặt hàng là cho thấy thông tin tóm tắt về đơn đặt hàng. Trong nhiệm vụ này, bạn sẽ tận dụng toàn bộ thông tin đơn đặt hàng từ mô hình khung hiển thị dùng chung rồi cập nhật chi tiết đơn đặt hàng trên màn hình bằng tính năng liên kết dữ liệu.

78f510e10d848dd2.png

  1. Trong fragment_summary.xml, hãy đảm bảo bạn đã khai báo biến dữ liệu viewModel.
<layout ...>

    <data>
        <variable
            name="viewModel"
            type="com.example.cupcake.model.OrderViewModel" />
    </data>

    <ScrollView ...>

    ...
  1. Trong SummaryFragment, trong hàm onViewCreated(), hãy đảm bảo binding.viewModel đã được khởi động.
  2. Trên fragment_summary.xml, hãy đọc từ mô hình xem để cập nhật nội dung tóm tắt thông tin đơn đặt hàng trên màn hình. Cập nhật số lượng, hương vị và ngày TextViews bằng cách thêm các thuộc tính văn bản sau đây. Biến số lượng thuộc kiểu Int nên bạn cần đổi biến này thành kiểu chuỗi.
<TextView
   android:id="@+id/quantity"
   ...
   android:text="@{viewModel.quantity.toString()}"
   ... />
<TextView
   android:id="@+id/flavor"
   ...
   android:text="@{viewModel.flavor}"
   ... />
<TextView
   android:id="@+id/date"
   ...
   android:text="@{viewModel.date}"
   ... />
  1. Chạy và kiểm thử ứng dụng để chắc chắn rằng các tuỳ chọn đơn đặt hàng đã chọn sẽ xuất hiện trong phần tóm tắt đơn đặt hàng.

7091453fa817b55.png

8. Tính giá tiền theo chi tiết đơn hàng

Nhìn vào ảnh chụp màn hình cuối cùng của ứng dụng trong lớp học lập trình này, bạn sẽ thấy giá tiền đã thực sự hiện trên từng mảnh (ngoại trừ StartFragment) để người dùng biết được giá tiền khi tạo đơn đặt hàng.

3b6a68cab0b9ee2.png

Sau đây là các quy tắc tính giá tiền của cửa hàng bánh cupcake.

  • Mỗi bánh cupcake có giá 2 USD
  • Lấy hàng trong ngày sẽ cộng thêm 3 USD

Do đó, với đơn đặt hàng gồm 6 bánh, giá tiền sẽ là 6 bánh x 2 USD mỗi bánh = 12 USD. Nếu người dùng muốn lấy hàng trong ngày thì cộng thêm phí 3 USD, tổng tiền sẽ là 15 USD.

Cập nhật giá tiền trong mô hình xem

Để hỗ trợ chức năng này trong ứng dụng, trước tiên, hãy xử lý giá cho mỗi bánh cupcake và tạm thời bỏ qua chi phí lấy hàng trong ngày.

  1. Mở OrderViewModel.kt rồi tạo một biến để lưu giá của mỗi chiếc bánh cupcake. Khai báo biến này là một biến hằng số riêng trên phần đầu tệp, bên ngoài định nghĩa lớp (nhưng sau các lệnh nhập). Sử dụng chỉ định const và thiết lập chế độ chỉ đọc val.
package ...

import ...

private const val PRICE_PER_CUPCAKE = 2.00

class OrderViewModel : ViewModel() {
    ...

Hãy nhớ rằng giá trị hằng số (thể hiện bằng từ khoá const trong Kotlin) sẽ không thay đổi và là giá trị xác định trong thời gian biên dịch. Để tìm hiểu thêm về hằng số, tham khảo tài liệu này.

  1. Bây giờ bạn đã xác định được giá của mỗi chiếc bánh cupcake, hãy tạo một phương thức trợ giúp để tính giá tiền. Phương thức này có thể là private vì chỉ dùng trong lớp này. Bạn sẽ thay đổi logic tính giá để tính cả phí lấy hàng trong ngày trong nhiệm vụ tiếp theo.
private fun updatePrice() {
    _price.value = (quantity.value ?: 0) * PRICE_PER_CUPCAKE
}

Dòng lệnh này nhân giá bánh với số lượng bánh đã đặt. Đối với mã trong dấu ngoặc đơn, vì giá trị của quantity.value có thể rỗng (null) nên bạn hãy sử dụng toán tử elvis (?:). Với toán tử elvis (?:), nếu biểu thức bên trái của toán tử này khác rỗng thì hãy sử dụng giá trị của biểu thức này. Ngược lại, nếu biểu thức ở bên trái là rỗng, hãy sử dụng biểu thức ở bên phải toán tử elvis (là 0 trong trường hợp này).

  1. Cũng trong lớp OrderViewModel, hãy cập nhật biến giá tiền với số lượng bánh được đặt. Gọi đến hàm mới này trong hàm setQuantity().
fun setQuantity(numberCupcakes: Int) {
    _quantity.value = numberCupcakes
    updatePrice()
}

Liên kết thuộc tính giá tiền với giao diện người dùng

  1. Trong các bố cục của fragment_flavor.xml, fragment_pickup.xmlfragment_summary.xml, hãy đảm bảo bạn đã định nghĩa biến dữ liệu viewModel có kiểu com.example.cupcake.model.OrderViewModel.
<layout ...>

    <data>
        <variable
            name="viewModel"
            type="com.example.cupcake.model.OrderViewModel" />
    </data>

    <ScrollView ...>

    ...
  1. Trong phương thức onViewCreated() của mỗi lớp mảnh (fragment class), hãy nhớ liên kết thực thể của đối tượng mô hình xem trong mảnh này với biến dữ liệu mô hình xem trong bố cục.
binding?.apply {
    viewModel = sharedViewModel
    ...
}
  1. Trong mỗi bố cục mảnh, hãy sử dụng biến viewModel để thiết lập giá tiền thể hiện trong bố cục. Bắt đầu chỉnh sửa tệp fragment_flavor.xml. Đối với khung hiển thị văn bản subtotal, hãy đặt giá trị cho thuộc tính android:text"@{@string/subtotal_price(viewModel.price)}". Biểu thức bố cục liên kết dữ liệu này sử dụng tài nguyên chuỗi @string/subtotal_price với tham số truyền vào là giá tiền trong mô hình hiển thị. Ví dụ: kết quả sẽ hiện Subtotal 12.0 (Đơn giá 12.0).
...

<TextView
    android:id="@+id/subtotal"
    android:text="@{@string/subtotal_price(viewModel.price)}"
    ... />

...

Bạn đang sử dụng tài nguyên chuỗi đã được khai báo trong tệp strings.xml:

<string name="subtotal_price">Subtotal %s</string>
  1. Chạy ứng dụng. Nếu bạn chọn One cupcake (Một bánh cupcake) trong mảnh bắt đầu, thì mảnh hương vị sẽ cho thấy Subtotal 2.0 (Đơn giá 2.0). Nếu bạn chọn Six cupcakes (6 bánh cupcake), thì mảnh hương vị sẽ hiện Subtotal 12.0 (Đơn giá 12.0), v.v. Bạn sẽ định dạng giá tiền thành định dạng tiền tệ phù hợp trong các bước sau. Hiện tại, hành vi này đã được lường trước.

  1. Bây giờ, hãy thay đổi tương tự cho các mảnh lấy hàng và tóm tắt đơn đặt hàng. Trong các bố cục fragment_pickup.xmlfragment_summary.xml, hãy chỉnh sửa các chế độ xem văn bản để dùng cả thuộc tính viewModel price.

fragment_pickup.xml

...

<TextView
    android:id="@+id/subtotal"
    ...
    android:text="@{@string/subtotal_price(viewModel.price)}"
    ... />

...

fragment_summary.xml

...

<TextView
   android:id="@+id/total"
   ...
   android:text="@{@string/total_price(viewModel.price)}"
   ... />

...

  1. Chạy ứng dụng. Hãy chắc chắn rằng giá tiền hiện trong phần tóm tắt đơn đặt hàng được tính toán chính xác tương ứng với số lượng bánh là 1, 6 và 12. Như đã đề cập ở trên, giá tiền hiện chưa được định dạng chính xác (hiện dưới dạng 2.0 thay vì 2 USD hoặc 12.0 thay vì 12 USD).

Tính thêm phí lấy hàng trong ngày

Trong nhiệm vụ này, bạn sẽ triển khai quy tắc thứ hai về việc tính giá tiền. Nếu lấy hàng trong ngày, đơn đặt hàng sẽ tính thêm 3 USD.

  1. Trong lớp OrderViewModel, hãy định nghĩa một hằng số riêng mới ở cấp cao nhất cho chi phí lấy hàng trong ngày.
private const val PRICE_FOR_SAME_DAY_PICKUP = 3.00
  1. Trong hàm updatePrice(), hãy kiểm tra xem người dùng có sử dụng tuỳ chọn lấy hàng trong ngày hay không. Kiểm tra xem ngày trong mô hình xem (_date.value) có giống với ngày đầu tiên trong danh sách dateOptions hay không. Ngày này luôn là ngày hiện tại.
private fun updatePrice() {
    _price.value = (quantity.value ?: 0) * PRICE_PER_CUPCAKE
    if (dateOptions[0] == _date.value) {

    }
}
  1. Để các bước tính toán trở nên đơn giản hơn, hãy tạo một biến tạm thời có tên calculatedPrice. Tính giá tiền mới nhất rồi gán lại cho _price.value.
private fun updatePrice() {
    var calculatedPrice = (quantity.value ?: 0) * PRICE_PER_CUPCAKE
    // If the user selected the first option (today) for pickup, add the surcharge
    if (dateOptions[0] == _date.value) {
        calculatedPrice += PRICE_FOR_SAME_DAY_PICKUP
    }
    _price.value = calculatedPrice
}
  1. Gọi phương thức trợ giúp updatePrice() qua phương thức setDate() để thêm phí lấy hàng trong ngày.
fun setDate(pickupDate: String) {
    _date.value = pickupDate
    updatePrice()
}
  1. Chạy ứng dụng và di chuyển qua các bước trong ứng dụng. Bạn sẽ thấy rằng khi thay đổi ngày lấy hàng, phí lấy hàng trong ngày không được trừ đi trong tổng số tiền. Lý do là giá tiền đã thay đổi trong mô hình xem nhưng chưa được thông báo cho bố cục ràng buộc.

2ea8e000fb4e6ec8.png

Thiết lập chủ sở hữu vòng đời (Lifecycle owner) để quan sát LiveData

LifecycleOwner là một lớp có vòng đời trên Android, chẳng hạn như một hoạt động hoặc một mảnh. Trình quan sát (observer) LiveData chỉ theo dõi thay đổi đối với dữ liệu của ứng dụng khi chủ sở hữu vòng đời ở trạng thái đang hoạt động (STARTED hoặc RESUMED).

Trong ứng dụng của bạn, đối tượng LiveData hoặc dữ liệu quan sát được là thuộc tính price mô hình xem. Chủ sở hữu vòng đời là các mảnh hương vị, lấy hàng và tóm tắt. Trình quan sát LiveData là biểu thức liên kết trong tệp bố cục có dữ liệu quan sát được, chẳng hạn như giá tiền. Với tính năng Liên kết dữ liệu (Data Binding), khi một giá trị quan sát được thay đổi, các thành phần trên giao diện người dùng có liên kết với giá trị đó sẽ cập nhật tự động.

Ví dụ về biểu thức liên kết: android:text="@{@string/subtotal_price(viewModel.price)}"

Để các thành phần giao diện người dùng tự động cập nhật, bạn phải liên kết binding.lifecycleOwner

với chủ sở hữu vòng đời trong ứng dụng. Bạn sẽ thực hiện việc này trong bước tiếp theo.

  1. Trong các lớp FlavorFragment, PickupFragmentSummaryFragment, bên trong phương thức onViewCreated(), hãy thêm nội dung sau vào khối lệnh binding?.apply. Thao tác này sẽ thiết lập chủ sở hữu vòng đời trên đối tượng liên kết. Việc thiết lập chủ sở hữu vòng đời sẽ cho phép ứng dụng quan sát các đối tượng LiveData.
binding?.apply {
    lifecycleOwner = viewLifecycleOwner
    ...
}
  1. Chạy lại ứng dụng. Trên màn hình lấy hàng, hãy thay đổi ngày lấy hàng và lưu ý điểm khác biệt trong cách giá tiền tự động thay đổi. Và khoản phí lấy hàng được thể hiện chính xác trong màn hình tóm tắt đơn đặt hàng.
  2. Lưu ý rằng khi chọn ngày lấy hàng là ngày hiện tại, giá tiền của đơn đặt hàng sẽ tăng thêm 3 USD. Nếu chọn một ngày bất kỳ trong tương lai, giá tiền vẫn được tính bằng số lượng bánh x 2 USD.

  1. Hãy kiểm thử nhiều trường hợp khác bằng cách thay đổi số lượng, hương vị và ngày lấy bánh. Bây giờ, bạn sẽ thấy giá tiền đã được cập nhật từ mô hình xem cho từng mảnh. Điều tuyệt vời nhất là bạn không cần viết thêm mã Kotlin để cập nhật giao diện người dùng mỗi khi giá tiền thay đổi.

f4c0a3c5ea916d03.png

Để hoàn thiện chức năng tính giá, bạn cần định dạng giá tiền theo nội tệ.

Dùng phép biến đổi LiveData để định dạng giá tiền

(Các) phương thức biến đổi LiveData đem đến một cách để thao tác dữ liệu trên nguồn LiveData và trả về một đối tượng LiveData. Nói một cách đơn giản, phương thức này sẽ biến giá trị của LiveData thành một giá trị khác. Các phép biến đổi này sẽ không được thực hiện trừ phi trình quan sát đang theo dõi đối tượng LiveData.

Transformations.map() là một trong các hàm biến đổi. Hàm này sẽ lấy nguồn LiveData và một hàm khác làm tham số. Hàm này thao tác trên nguồn LiveData và trả về một giá trị được cập nhật và cũng quan sát được.

Sau đây là một số ví dụ theo thời gian thực có thể sử dụng phép biến đổi LiveData:

  • Định dạng cách thể hiện chuỗi ngày, giờ
  • Sắp xếp các thành phần trong danh sách
  • Lọc hoặc nhóm các thành phần
  • Tính kết quả từ một danh sách, chẳng hạn như tổng của tất cả thành phần, số lượng thành phần, trả về thành phần cuối cùng, v.v.

Trong nhiệm vụ này, bạn sẽ sử dụng phương thức Transformations.map() để định dạng giá tiền theo nội tệ. Bạn sẽ đổi giá gốc dưới dạng một số thập phân (LiveData<Double>) thành một giá trị chuỗi (LiveData<String>).

  1. Trong lớp OrderViewModel, hãy thay đổi kiểu thuộc tính dự phòng thành LiveData<String> thay vì LiveData<Double>. Giá sau khi định dạng sẽ là một chuỗi có ký hiệu tiền tệ, chẳng hạn như "$". Bạn sẽ khắc phục lỗi khởi động trong bước tiếp theo.
private val _price = MutableLiveData<Double>()
val price: LiveData<String>
  1. Sử dụng Transformations.map() để khởi tạo biến mới, tham số truyền vào là _price và một hàm lambda. Sử dụng phương thức getCurrencyInstance() trong lớp NumberFormat để đổi giá tiền sang định dạng nội tệ. Mã biến đổi có dạng như sau.
private val _price = MutableLiveData<Double>()
val price: LiveData<String> = Transformations.map(_price) {
   NumberFormat.getCurrencyInstance().format(it)
}

Bạn cần nhập androidx.lifecycle.Transformationsjava.text.NumberFormat.

  1. Chạy ứng dụng. Bây giờ, bạn sẽ thấy chuỗi giá tiền đã được định dạng cho phần đơn giá và thành tiền. Giao diện người dùng trở nên thân thiện hơn rất nhiều!

1853bd13a07f1bc7.png

  1. Kiểm tra lại để đảm bảo tính năng này hoạt động như mong đợi. Kiểm thử các trường hợp như: Đặt 1 bánh, 6 bánh, 12 bánh. Hãy đảm bảo giá tiền đã được cập nhật chính xác trên mỗi màn hình. Giá tiền nên thể hiện Subtotal $2.00 (Đơn giá $2) trên các mảnh hương vị và lấy hàng, và Total $2.00 (Thành tiền $12) trên mảnh tóm tắt. Ngoài ra, hãy đảm bảo phần tóm tắt đơn đặt hàng thể hiện chính xác thông tin chi tiết về đơn đặt hàng.

9. Dùng tính năng liên kết trình nghe để thiết lập trình nghe lượt nhấp

Trong nhiệm vụ này, bạn sẽ sử dụng tính năng liên kết trình nghe để liên kết trình nghe lượt nhấp nút trong các lớp mảnh với bố cục.

  1. Trong tệp bố cục fragment_start.xml, hãy thêm biến dữ liệu có tên là startFragment thuộc kiểu com.example.cupcake.StartFragment. Đảm bảo tên gói của mảnh khớp với tên gói của ứng dụng.
<layout ...>

    <data>
        <variable
            name="startFragment"
            type="com.example.cupcake.StartFragment" />
    </data>
    ...
    <ScrollView ...>
  1. Trong StartFragment.kt, trong phương thức onViewCreated(), hãy liên kết biến dữ liệu mới vào thực thể của mảnh. Bạn có thể truy cập vào thực thể của mảnh bên trong mảnh đó bằng cách sử dụng từ khoá this. Xoá khối binding?.apply cùng với mã trong đó. Phương thức hoàn chỉnh sẽ có dạng như sau.
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)
    binding?.startFragment = this
}
  1. Trong fragment_start.xml, hãy thêm trình nghe sự kiện bằng cách sử dụng tính năng liên kết trình nghe vào thuộc tính onClick của các nút và gọi hàm orderCupcake() trên startFragment, với tham số là số lượng bánh.
<Button
    android:id="@+id/order_one_cupcake"
    android:onClick="@{() -> startFragment.orderCupcake(1)}"
    ... />

<Button
    android:id="@+id/order_six_cupcakes"
    android:onClick="@{() -> startFragment.orderCupcake(6)}"
    ... />

<Button
    android:id="@+id/order_twelve_cupcakes"
    android:onClick="@{() -> startFragment.orderCupcake(12)}"
    ... />
  1. Chạy ứng dụng. Hãy lưu ý rằng trình xử lý lượt nhấp nút trong mảnh bắt đầu đang hoạt động như dự kiến.
  2. Tương tự, hãy thêm các biến dữ liệu ở trên trong các bố cục khác để liên kết thực thể của mảnh, fragment_flavor.xml, fragment_pickup.xmlfragment_summary.xml.

Trong fragment_flavor.xml

<layout ...>

    <data>
        <variable
            ... />

        <variable
            name="flavorFragment"
            type="com.example.cupcake.FlavorFragment" />
    </data>

    <ScrollView ...>

Trong fragment_pickup.xml:

<layout ...>

    <data>
        <variable
            ... />

        <variable
            name="pickupFragment"
            type="com.example.cupcake.PickupFragment" />
    </data>

    <ScrollView ...>

Trong fragment_summary.xml:

<layout ...>

    <data>
        <variable
            ... />

        <variable
            name="summaryFragment"
            type="com.example.cupcake.SummaryFragment" />
    </data>

    <ScrollView ...>
  1. Trong các lớp mảnh còn lại, trong phương thức onViewCreated(), hãy xoá mã thiết lập trình nghe lượt nhấp trên các nút theo cách thủ công.
  2. Trong phương thức onViewCreated(), hãy liên kết biến dữ liệu mảnh với thực thể của mảnh đó. Ở đây, bạn sẽ sử dụng từ khoá this theo một cách khác. Lý do là vì bên trong khối binding?.apply, từ khoá this tham chiếu đến thực thể liên kết chứ không tham chiếu đến thực thể mảnh. Sử dụng @ và chỉ định rõ tên lớp mảnh, ví dụ: this@FlavorFragment. Các phương thức onViewCreated() đã hoàn chỉnh sẽ có dạng như sau:

Phương thức onViewCreated() trong lớp FlavorFragment sẽ có dạng như sau:

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)

    binding?.apply {
        lifecycleOwner = viewLifecycleOwner
        viewModel = sharedViewModel
        flavorFragment = this@FlavorFragment
    }
}

Phương thức onViewCreated() trong lớp PickupFragment sẽ có dạng như sau:

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
   super.onViewCreated(view, savedInstanceState)

   binding?.apply {
       lifecycleOwner = viewLifecycleOwner
       viewModel = sharedViewModel
       pickupFragment = this@PickupFragment
   }
}

Phương thức onViewCreated() thu được trong phương thức lớp SummaryFragment sẽ có dạng như sau:

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
   super.onViewCreated(view, savedInstanceState)

   binding?.apply {
       lifecycleOwner = viewLifecycleOwner
       viewModel = sharedViewModel
       summaryFragment = this@SummaryFragment
   }
}
  1. Tương tự như vậy, trong các tệp bố cục khác, hãy thêm biểu thức liên kết trình nghe vào thuộc tính onClick của các nút.

Trong fragment_flavor.xml:

<Button
    android:id="@+id/next_button"
    android:onClick="@{() -> flavorFragment.goToNextScreen()}"
    ... />

Trong fragment_pickup.xml:

<Button
    android:id="@+id/next_button"
    android:onClick="@{() -> pickupFragment.goToNextScreen()}"
    ... />

Trong fragment_summary.xml:

<Button
    android:id="@+id/send_button"
    android:onClick="@{() -> summaryFragment.sendOrder()}"
    ...>
  1. Chạy ứng dụng để chắc chắn rằng các nút vẫn hoạt động như dự kiến. Có thể thấy rằng không có sự thay đổi rõ ràng nào về hành vi, nhưng hiện giờ bạn đã sử dụng được tính năng liên kết trình nghe để thiết lập trình nghe lượt nhấp!

Chúc mừng bạn đã hoàn thành lớp học lập trình này và xây dựng được ứng dụng Cupcake! Tuy nhiên, ứng dụng vẫn chưa hoàn chỉnh. Trong lớp học lập trình tiếp theo, bạn sẽ thêm nút Cancel (Huỷ) và chỉnh sửa ngăn xếp quay lại (backstack). Bạn cũng sẽ tìm hiểu khái niệm ngăn xếp quay lại và các chủ đề mới khác. Hẹn gặp bạn ở lớp học này nhé!

10. Mã giải pháp

Mã giải pháp cho lớp học lập trình này nằm trong dự án dưới đây. Hãy sử dụng nhánh viewmodel để lấy hoặc tải mã.

Để lấy mã cho lớp học lập trình này và mở trong Android Studio, hãy thực hiện các bước sau.

Lấy mã

  1. Nhấp vào URL được cung cấp. Thao tác này sẽ mở trang GitHub của dự án trong một trình duyệt.
  2. Trên trang GitHub của dự án, hãy nhấp vào nút Code (Mã), một hộp thoại sẽ xuất hiện.

5b0a76c50478a73f.png

  1. Trong hộp thoại này, hãy nhấp vào nút Download ZIP (Tải tệp ZIP xuống) để lưu dự án vào máy tính. Chờ quá trình tải xuống hoàn tất.
  2. Xác định vị trí của tệp trên máy tính (thường nằm trong thư mục Downloads (Tệp đã tải xuống)).
  3. Nhấp đúp vào tệp ZIP để giải nén. Thao tác này sẽ tạo một thư mục mới chứa các tệp dự án.

Mở dự án trong Android Studio

  1. Khởi động Android Studio.
  2. Trong cửa sổ Welcome to Android Studio (Chào mừng bạn đến với Android Studio), hãy nhấp vào Open an existing Android Studio project (Mở một dự án hiện có trong Android Studio).

36cc44fcf0f89a1d.png

Lưu ý: Nếu Android Studio đã mở sẵn thì thay vào đó, hãy chọn tuỳ chọn sau đây trong trình đơn File > New > Import Project (Tệp > Mới > Nhập dự án).

21f3eec988dcfbe9.png

  1. Trong hộp thoại Import Project (Nhập dự án), hãy chuyển đến nơi chứa thư mục dự án đã giải nén (thường nằm trong thư mục Downloads (Tệp đã tải xuống)).
  2. Nhấp đúp vào thư mục dự án đó.
  3. Chờ Android Studio mở dự án.
  4. Nhấp vào nút Run (Chạy) 11c34fc5e516fb1c.png để xây dựng và chạy ứng dụng. Hãy đảm bảo ứng dụng được dựng như mong đợi.
  5. Duyệt qua các tệp dự án trong cửa sổ công cụ Project (Dự án) để xem cách ứng dụng được thiết lập.

11. Tóm tắt

  • ViewModel là một phần của Bộ thành phần cấu trúc Android và dữ liệu ứng dụng lưu trong ViewModel được lưu giữ trong quá trình thay đổi cấu hình. Để thêm ViewModel vào ứng dụng, hãy tạo một lớp mới được mở rộng từ lớp ViewModel.
  • ViewModel dùng chung dùng để lưu dữ liệu ứng dụng giữa nhiều mảnh trong một ViewModel. Nhiều mảnh trong ứng dụng sẽ truy cập ViewModel dùng chung này theo phạm vi hoạt động của các mảnh đó.
  • LifecycleOwner là một lớp có vòng đời trên Android, chẳng hạn như một hoạt động hoặc một mảnh.
  • Trình quan sát LiveData chỉ theo dõi các thay đổi đối với dữ liệu của ứng dụng khi chủ sở hữu vòng đời ở trạng thái đang hoạt động (STARTED hoặc RESUMED).
  • Liên kết trình nghe là các biểu thức lambda sẽ chạy khi xảy ra một sự kiện, chẳng hạn như sự kiện onClick. Các biểu thức này tương tự các tham chiếu phương thức như textview.setOnClickListener(clickListener) nhưng tính năng liên kết trình nghe cho phép bạn chạy các biểu thức liên kết dữ liệu tuỳ ý.
  • (Các) phương thức biến đổi LiveData đem đến một cách để thao tác dữ liệu trên nguồn LiveData và trả về một đối tượng LiveData.
  • Nền tảng Android cung cấp một lớp có tên SimpleDateFormat, dùng để định dạng và phân tích cú pháp ngày tháng theo miền bản địa. Lớp này cho phép định dạng (ngày → văn bản) và phân tích cú pháp ngày tháng (văn bản → ngày).

12. Tìm hiểu thêm