Dự án: Ứng dụng Lunch Tray

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

Lớp học lập trình này giới thiệu một ứng dụng mới có tên là Lunch Tray (Bữa ăn trưa) mà bạn sẽ tự mình xây dựng. Trong lớp học lập trình này, bạn sẽ được hướng dẫn từng bước hoàn thành dự án ứng dụng Lunch Tray, bao gồm cả việc thiết lập và kiểm thử dự án trong Android Studio.

Lớp học lập trình này khác với các lớp học khác trong khoá học này. Không giống các lớp học lập trình trước đây, lớp học lập trình này không nhằm mục đích hướng dẫn từng bước về cách xây dựng ứng dụng. Thay vào đó, lớp học hướng đến việc thiết lập một dự án mà bạn sẽ hoàn tất một cách độc lập, đồng thời đưa ra hướng dẫn về cách tự hoàn tất ứng dụng cũng như tự kiểm tra.

Thay vì đưa ra mã nguồn giải pháp, chúng tôi sẽ cung cấp một bộ công cụ kiểm thử trong ứng dụng mà bạn sẽ tải xuống. Bạn sẽ chạy các bài kiểm thử này trong Android Studio (chúng tôi sẽ hướng dẫn bạn cách thực hiện việc này sau trong lớp học lập trình) để xem mã nguồn của bạn có vượt qua được các bài kiểm thử hay không. Bạn có thể sẽ phải thử vài lần, kể cả các nhà phát triển chuyên nghiệp cũng hiếm khi vượt qua được ngay từ lượt kiểm thử đầu tiên! Sau khi mã nguồn của bạn vượt qua mọi bài kiểm thử, bạn có thể coi như dự án này đã hoàn tất.

Chúng tôi hiểu rằng có thể bạn muốn có mã nguồn giải pháp chỉ để kiểm tra lại. Chúng tôi cố ý không cung cấp mã nguồn giải pháp vì muốn bạn thực hành như một nhà phát triển chuyên nghiệp. Để làm được như vậy, có thể bạn sẽ phải vận dụng một số kỹ năng mà bạn chưa thực hành nhiều, chẳng hạn như:

  • Dùng Google để tra cứu các thuật ngữ, thông báo lỗi và các đoạn mã trong ứng dụng mà bạn thấy lạ;
  • Kiểm thử mã, đọc lỗi, sau đó điều chỉnh mã rồi kiểm thử lại;
  • Quay lại nội dung trước đó trong Android Basics (Thông tin cơ bản về Android) để ôn lại kiến thức đã học;
  • So sánh đoạn mã mà bạn biết là hoạt động tốt (chẳng hạn như đoạn mã được cung cấp trong dự án hoặc đoạn mã giải pháp trước đây trong các ứng dụng khác ở Bài 3) với đoạn mã mà bạn đang viết.

Việc này thoạt nhìn có vẻ sẽ rất khó khăn, nhưng chúng tôi chắc chắn 100% rằng nếu bạn có thể hoàn thành Bài 3 thì bạn đã sẵn sàng cho dự án này. Hãy bình tĩnh và đừng bỏ cuộc. Bạn có thể làm được!

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

  • Dự án này dành cho những người dùng đã hoàn thành Bài 3 của khoá học Android cơ bản: Kotlin.

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

  • Bạn sẽ tải một ứng dụng đặt món ăn có tên Lunch Tray, triển khai ViewModel có liên kết dữ liệu và thêm tính năng điều hướng giữa các phân đoạn.

Bạn cần có

  • Một máy tính đã cài đặt Android Studio.

2. Tổng quan về ứng dụng đã hoàn thiện

Chào mừng bạn đến với Dự án: Lunch Tray!

Như bạn đã biết, điều hướng là một phần cơ bản của quá trình phát triển cho Android. Cho dù đang sử dụng một ứng dụng để duyệt xem công thức nấu ăn, tìm đường đến nhà hàng yêu thích hay quan trọng nhất là đặt món ăn, chắc chắn bạn đều phải di chuyển giữa nhiều màn hình nội dung. Trong dự án này, bạn sẽ tận dụng các kỹ năng đã học ở Bài 3 để xây dựng ứng dụng đặt đồ ăn trưa có tên Lunch Tray, triển khai mô hình hiển thị, liên kết dữ liệu và di chuyển giữa các màn hình.

Dưới đây là ảnh chụp màn hình khi ứng dụng đã hoàn thiện. Khi mở ứng dụng Lunch Tray lần đầu, người dùng sẽ được chào đón bằng màn hình có duy nhất một nút với nội dung "Start Order" (Bắt đầu đặt hàng).

20fa769d4ba93ef3.png

Sau khi nhấp vào Start Order, người dùng có thể chọn một món chính trong các lựa chọn có sẵn. Người dùng có thể thay đổi lựa chọn; thao tác thay đổi sẽ cập nhật Subtotal (Thành tiền) xuất hiện ở dưới cùng.

438b61180d690b3a.png

Màn hình tiếp theo cho phép người dùng thêm một món phụ.

768352680759d3e2.png

Màn hình sau đó cho phép người dùng chọn một món ăn kèm (accompaniment) cho đơn gọi món của họ.

8ee2bf41e9844614.png

Cuối cùng, người dùng sẽ thấy một bản tóm tắt chi phí đơn gọi món gồm các phần như thành tiền (subtotal), thuế bán hàng (sales tax) và tổng chi phí (total cost). Người dùng cũng có thể gửi hoặc huỷ đơn.

61c883c34d94b7f7.png

Cả hai tuỳ chọn đều đưa người dùng trở về màn hình đầu tiên. Nếu người dùng đã gửi đơn gọi món, một thông báo ngắn sẽ xuất hiện ở cuối màn hình cho họ biết là đơn gọi món đã được gửi.

acb7d7a5d9843bac.png

3. Bắt đầu

Tải mã dự án xuống

Lưu ý tên thư mục là android-basics-kotlin-lunch-tray-app. Chọn thư mục này khi bạn mở dự án trong Android Studio.

  1. Chuyển đến trang kho lưu trữ GitHub được cung cấp cho dự án.
  2. Xác minh rằng tên nhánh khớp với tên nhánh được chỉ định trong lớp học lập trình. Ví dụ: trong ảnh chụp màn hình sau đây, tên nhánh là main.

1e4c0d2c081a8fd2.png

  1. Trên trang GitHub cho dự án này, nhấp vào nút Code (Mã). Thao tác này sẽ khiến một cửa sổ bật lên.

1debcf330fd04c7b.png

  1. Trong cửa sổ bật lên, nhấp vào nút Download ZIP (Tải tệp ZIP xuống) để lưu dự án vào máy tính. Chờ quá trình tải xuống hoàn tất.
  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 (Mở).

d8e9dbdeafe9038a.png

Lưu ý: Nếu Android Studio đã mở thì chuyển sang chọn tuỳ chọn File (Tệp) > Open (Mở) trong trình đơn.

8d1fda7396afe8e5.png

  1. Trong trình duyệt tệp, hãy chuyển đến vị trí của thư mục dự án chưa giải nén (thường nằm trong thư mục Downloads (Tệp đã tải xuống)).
  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) 8de56cba7583251f.png để tạo và chạy ứng dụng. Hãy đảm bảo ứng dụng được tạo như mong đợi.

Trước khi bắt đầu triển khai ViewModel và hoạt động điều hướng, bạn hãy dành một chút thời gian để đảm bảo dự án được tạo thành công và làm quen với dự án đó. Trong lần đầu chạy ứng dụng, bạn sẽ thấy một màn hình trống. MainActivity sẽ không trình bày phân đoạn nào vì bạn chưa thiết lập biểu đồ điều hướng.

Cấu trúc dự án sẽ tương tự như các dự án khác mà bạn đã xử lý. Có các gói riêng biệt dành cho dữ liệu, mô hình và giao diện người dùng, cũng như các thư mục riêng cho tài nguyên.

a19fd8a4bc92f2fc.png

Tất cả các tuỳ chọn món mà người dùng có thể đặt (món chính, món phụ và món ăn kèm) đều được biểu thị bằng lớp MenuItem trong gói model (mô hình). Các đối tượng MenuItem có tên, thông tin mô tả, giá và loại.

data class MenuItem(
    val name: String,
    val description: String,
    val price: Double,
    val type: Int
) {
    fun getFormattedPrice(): String = NumberFormat.getCurrencyInstance().format(price)
}

Loại được biểu thị bằng một số nguyên lấy từ đối tượng ItemType trong gói constants (hằng số).

object ItemType {
    val ENTREE = 1
    val SIDE_DISH = 2
    val ACCOMPANIMENT = 3
}

Bạn có thể tìm thấy từng đối tượng MenuItem trong DataSource.kt trong gói dữ liệu.

object DataSource {
    val menuItems = mapOf(
        "cauliflower" to
        MenuItem(
            name = "Cauliflower",
            description = "Whole cauliflower, brined, roasted, and deep fried",
            price = 7.00,
            type = ItemType.ENTREE
        ),
    ...
}

Đối tượng này chỉ chứa một mô hình ánh xạ bao gồm một khoá và một MenuItem tương ứng. Trước tiên, bạn sẽ truy cập vào DataSource trong ObjectViewModel (phần bạn sẽ triển khai đầu tiên)

Xác định ViewModel

Như đã thấy trong ảnh chụp màn hình ở trang trước, ứng dụng cần người dùng cung cấp 3 mục: 1 món chính, 1 món phụ và 1 món ăn kèm. Sau đó, màn hình tóm tắt đơn gọi món cho biết thành tiền và tính thuế bán hàng dựa trên các món đã chọn (thuế này dùng để tính tổng chi phí).

Trong gói model, hãy mở OrderViewModel.kt và bạn sẽ thấy một vài biến đã được xác định. Thuộc tính menuItems cho phép bạn truy cập vào DataSource qua ViewModel.

val menuItems = DataSource.menuItems

Trước tiên, cũng có một số biến cho previousEntreePrice, previousSidePricepreviousAccompanimentPrice. Vì giá trị "thành tiền" được cập nhật khi người dùng đưa ra lựa chọn (thay vì được thêm vào ở bước cuối), nên các biến này được dùng để theo dõi lựa chọn trước của người dùng nếu họ thay đổi lựa chọn trước khi chuyển sang màn hình tiếp theo. Bạn sẽ sử dụng các biến này để đảm bảo "thành tiền" tính đến phần chênh lệch giữa giá của mặt hàng trước và mặt hàng đang chọn.

private var previousEntreePrice = 0.0
private var previousSidePrice = 0.0
private var previousAccompanimentPrice = 0.0

Ngoài ra, còn có các biến private như _entree, _side_accompaniment để lưu trữ lựa chọn hiện tại. Các biến này thuộc kiểu MutableLiveData<MenuItem?>. Mỗi biến đi kèm một thuộc tính sao lưu công khai, entree, sideaccompaniment (thuộc kiểu dữ liệu không thể thay đổi LiveData<MenuItem?>). Bố cục của phân đoạn sẽ truy cập các biến này để hiển thị các mặt hàng đã chọn lên màn hình. MenuItem bên trong đối tượng LiveData cũng có thể ở trạng thái rỗng vì người dùng có thể không chọn một món chính, món phụ và/hoặc món ăn kèm.

// Entree for the order
private val _entree = MutableLiveData<MenuItem?>()
val entree: LiveData<MenuItem?> = _entree

// Side for the order
private val _side = MutableLiveData<MenuItem?>()
val side: LiveData<MenuItem?> = _side

// Accompaniment for the order.
private val _accompaniment = MutableLiveData<MenuItem?>()
val accompaniment: LiveData<MenuItem?> = _accompaniment

Ngoài ra còn có các biến LiveData cho phần thành tiền, tổng và thuế. Các biến này dùng định dạng số để có thể xuất hiện dưới dạng tiền tệ.

// Subtotal for the order
private val _subtotal = MutableLiveData(0.0)
val subtotal: LiveData<String> = Transformations.map(_subtotal) {
    NumberFormat.getCurrencyInstance().format(it)
}

// Total cost of the order
private val _total = MutableLiveData(0.0)
val total: LiveData<String> = Transformations.map(_total) {
    NumberFormat.getCurrencyInstance().format(it)
}

// Tax for the order
private val _tax = MutableLiveData(0.0)
val tax: LiveData<String> = Transformations.map(_tax) {
    NumberFormat.getCurrencyInstance().format(it)
}

Cuối cùng, giá trị của thuế suất được cố định trong mã là 0,08 (8%).

private val taxRate = 0.08

Có 6 phương thức trong OrderViewModel mà bạn sẽ cần triển khai.

setEntree(), setSide() và setAccompaniment()

Tất cả các phương thức này phải hoạt động như nhau cho món chính, món phụ và món ăn kèm. Ví dụ: setEntree() phải hoạt động như sau:

  1. Nếu _entree không có giá trị null (tức là người dùng đã chọn một món chính nhưng đã thay đổi lựa chọn), hãy đặt previousEntreePrice thành giá của current _entree.
  2. Nếu _subtotal không phải là null, hãy trừ previousEntreePrice khỏi tổng giá trị thành tiền.
  3. Cập nhật giá trị của _entree thành món chính đã truyền vào hàm (truy cập vào MenuItem bằng menuItems).
  4. Gọi updateSubtotal(), truyền vào giá của món chính mới chọn.

Logic của setSide()setAccompaniment() hoàn toàn giống với setEntree().

updateSubtotal()

updateSubtotal() được gọi với một đối số cho giá mới. Giá này sẽ được thêm vào thành tiền. Phương thức này sẽ thực hiện 3 việc sau:

  1. Nếu _subtotal không phải là null, thêm itemPrice vào _subtotal.
  2. Nếu _subtotalnull, đặt _subtotal thành itemPrice.
  3. Sau khi đặt (hoặc cập nhật) _subtotal, gọi calculateTaxAndTotal() để các giá trị này được cập nhật nhằm phản ánh giá trị thành tiền mới.

calculateTaxAndTotal()

calculateTaxAndTotal() phải cập nhật các biến cho thuế và tổng chi phí dựa trên giá trị thành tiền. Triển khai phương thức như sau:

  1. Đặt _tax bằng thuế suất nhân với thành tiền.
  2. Đặt _total bằng thành tiền cộng với thuế.

resetOrder()

resetOrder() sẽ được gọi khi người dùng gửi hoặc huỷ đơn gọi món. Bạn cần đảm bảo ứng dụng không còn dữ liệu khi người dùng bắt đầu một đơn gọi món mới.

Triển khai resetOrder() bằng cách đặt tất cả các biến mà bạn đã sửa đổi trong OrderViewModel về giá trị ban đầu (0.0 hoặc null).

Tạo biến liên kết dữ liệu

Triển khai chức năng liên kết dữ liệu trong các tệp bố cục. Mở tệp bố cục và thêm biến liên kết dữ liệu thuộc kiểu OrderViewModel và/hoặc lớp của phân đoạn tương ứng.

Bạn sẽ cần triển khai tất cả các nhận xét TODO để thiết lập văn bản và trình nghe sự kiện nhấp trong 4 tệp bố cục:

  1. fragment_entree_menu.xml
  2. fragment_side_menu.xml
  3. fragment_accompaniment_menu.xml
  4. fragment_checkout.xml

Mỗi nhiệm vụ cụ thể được ghi chú trong một nhận xét TODO trong tệp bố cục. Các bước được tóm tắt như sau:

  1. Trong fragment_entree_menu.xml, trong thẻ <data>, hãy thêm một biến liên kết cho EntreeMenuFragment. Đối với mỗi nút chọn, bạn cần đặt món chính vào ViewModel khi người dùng chọn món đó. Văn bản trong khung hiển thị văn bản của "thành tiền" phải được cập nhật tương ứng. Bạn cũng cần đặt thuộc tính onClick cho cancel_buttonnext_button để huỷ đơn gọi món hoặc chuyển đến màn hình tiếp theo.
  2. Làm tương tự trong fragment_side_menu.xml, hãy thêm một biến liên kết cho SideMenuFragment, ngoại trừ việc đặt món phụ vào khung hiển thị khi người dùng nhấn nút chọn. Văn bản trong phần thành tiền cũng cần được cập nhật và bạn cũng cần đặt thuộc tính onClick cho nút huỷ và nút "tiếp theo".
  3. Làm tương tự một lần nữa nhưng trong fragment_accompaniment_menu.xml, lần này với một biến liên kết cho AccompanimentMenuFragment và thiết lập món ăn kèm (accompaniment) khi mỗi nút chọn được chọn. Xin nhắc lại, bạn cũng cần đặt các thuộc tính cho văn bản thành tiền (subtotal), nút huỷ (cancel) và nút tiếp theo (next).
  4. Trong fragment_checkout.xml, bạn cần thêm một thẻ <data> để có thể xác định các biến liên kết. Trong thẻ <data>, hãy thêm 2 biến liên kết, một biến cho OrderViewModel và một biến cho CheckoutFragment. Trong khung hiển thị văn bản, bạn cần đặt tên và giá của món chính, món phụ và món ăn kèm trong OrderViewModel. Bạn cũng cần đặt các biến thành tiền, thuế và tổng trong OrderViewModel. Sau đó, hãy đặt onClickAttributes để xác định thời điểm gửi đơn gọi món và thời điểm huỷ đơn gọi món bằng các hàm phù hợp trong CheckoutFragment.

.

Khởi động các biến liên kết dữ liệu trong phân đoạn

Hãy khởi động các biến liên kết dữ liệu trong các tệp phân đoạn tương ứng bên trong phương thức, onViewCreated().

  1. EntreeMenuFragment
  2. SideMenuFragment
  3. AccompanimentMenuFragment
  4. CheckoutFragment

Tạo biểu đồ điều hướng

Như bạn đã tìm hiểu trong Bài 3, một biểu đồ điều hướng được lưu trữ trong FragmentContainerView và nằm trong một hoạt động. Mở activity_main.xml rồi thay thế TODO bằng mã sau để khai báo FragmentContainerView.

<androidx.fragment.app.FragmentContainerView
   android:id="@+id/nav_host_fragment"
   android:name="androidx.navigation.fragment.NavHostFragment"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   app:defaultNavHost="true"
   app:navGraph="@navigation/mobile_navigation"
   app:layout_constraintBottom_toBottomOf="parent"
   app:layout_constraintLeft_toLeftOf="parent"
   app:layout_constraintRight_toRightOf="parent"
   app:layout_constraintTop_toTopOf="parent" />

Bạn có thể tìm thấy biểu đồ điều hướng mobile_navigation.xml trong gói res.navigation.

e3381215c35c1726.png

Đây là biểu đồ điều hướng cho ứng dụng này. Tuy nhiên, tệp này đang trống. Nhiệm vụ của bạn là thêm đích đến vào biểu đồ điều hướng và mô hình hoá hoạt động điều hướng sau đây giữa các màn hình.

  1. Điều hướng từ StartOrderFragment đến EntreeMenuFragment
  2. Điều hướng từ EntreeMenuFragment đến SideMenuFragment
  3. Điều hướng từ SideMenuFragment đến AccompanimentMenuFragment
  4. Điều hướng từ AccompanimentMenuFragment đến CheckoutFragment
  5. Điều hướng từ CheckoutFragment đến StartOrderFragment
  6. Điều hướng từ EntreeMenuFragment đến StartOrderFragment
  7. Điều hướng từ SideMenuFragment đến StartOrderFragment
  8. Điều hướng từ AccompanimentMenuFragment đến StartOrderFragment
  9. Điểm bắt đầu (Start Destination) phải là StartOrderFragment

Sau khi đã thiết lập biểu đồ điều hướng, bạn sẽ cần triển khai hoạt động điều hướng trong các lớp phân đoạn. Triển khai các nhận xét TODO còn lại trong các phân đoạn cũng như trong MainActivity.kt.

  1. Đối với phương thức goToNextScreen() trong EntreeMenuFragment, SideMenuFragmentAccompanimentMenuFragment, hãy điều hướng đến màn hình tiếp theo trong ứng dụng.
  2. Đối với phương thức cancelOrder() trong EntreeMenuFragment, SideMenuFragment, AccompanimentMenuFragmentCheckoutFragment, đầu tiên hãy gọi resetOrder() trên sharedViewModel, sau đó điều hướng đến StartOrderFragment.
  3. Trong StartOrderFragment, hãy triển khai setOnClickListener() để điều hướng đến EntreeMenuFragment.
  4. Trong CheckoutFragment, hãy triển khai phương thức submitOrder(). Gọi resetOrder() trên sharedViewModel, sau đó điều hướng đến StartOrderFragment.
  5. Cuối cùng, trong MainActivity.kt, hãy đặt navController thành navController trong NavHostFragment.

4. Kiểm thử ứng dụng

Dự án Lunch Tray chứa mục tiêu "androidTest" với một số trường hợp kiểm thử: MenuContentTests, NavigationTestsOrderFunctionalityTests.

Chạy bài kiểm thử

Để chạy kiểm thử, bạn có thể làm theo một trong những cách sau:

Đối với trường hợp kiểm thử đơn lẻ, hãy mở một lớp trường hợp kiểm thử (test case) rồi nhấp vào mũi tên màu xanh lục ở bên trái phần khai báo về lớp đó. Sau đó, chọn tuỳ chọn Run (Chạy) trên trình đơn. Thao tác này sẽ chạy tất cả các hoạt động kiểm thử trong trường hợp kiểm thử đó.

8ddcbafb8ec14f9b.png

Thông thường, bạn sẽ chỉ muốn chạy một chương trình kiểm thử, chẳng hạn như khi một kiểm thử không đạt còn các kiểm thử khác thì đạt. Bạn có thể chạy một kiểm thử duy nhất tương tự như cách thực hiện trên toàn bộ trường hợp kiểm thử. Sử dụng mũi tên màu xanh lục rồi chọn tuỳ chọn Run (Chạy).

335664b7fc8b4fb5.png

Nếu có nhiều trường hợp kiểm thử, bạn cũng có thể chạy toàn bộ bộ kiểm thử. Tương tự như cách chạy ứng dụng, bạn có thể thấy tuỳ chọn này trên trình đơn Run (Chạy).

80312efedf6e4dd3.png

Vui lòng lưu ý Android Studio sẽ mặc định chuyển đến mục tiêu cuối cùng mà bạn đã chạy (ứng dụng, mục tiêu kiểm thử, v.v.). Do đó, nếu trình đơn vẫn hiển thị Run > Run 'app' (Chạy > Chạy "ứng dụng"), thì bạn có thể chạy mục tiêu kiểm thử này bằng cách chọn Run > Run (Chạy > Chạy).

95aacc8f749dee8e.png

Sau đó chọn mục tiêu kiểm thử trong trình đơn bật lên.

8b702efbd4d21d3d.png

5. Không bắt buộc: Hãy cho chúng tôi biết ý kiến phản hồi của bạn!

Chúng tôi rất mong nhận được ý kiến của bạn về dự án này. Trả lời khảo sát ngắn này để góp ý cho chúng tôi. Ý kiến phản hồi của bạn sẽ giúp chúng tôi định hình các dự án trong tương lai.