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 Lunch Tray (Bữa ăn trưa) mà bạn sẽ tự 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 trước đây, mục đích của lớp học lập trình này là không 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ể 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ã 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 mã nguồn mà bạn biết là có tác dụng (chẳng hạn như mã nguồn được cung cấp trong dự án hoặc mã nguồn giải pháp trước đây trong các ứng dụng khác ở Bài 3) với mã nguồn 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 Kiến thức cơ bản về Kotlin trên Android (Android Basics in 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 quá trình phát triển cho Android. Cho dù bạn đ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, khả năng cao là bạn đang duyệt xem 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 xem, liên kết dữ liệu và điều hướng giữa các màn hình.

Dưới đây là ảnh chụp màn hình về ứ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 (entree) 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ụ (side).

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 đặt hàng 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 đặt hàng gồm các phần thành tiền (subtotal), thuế bán hàng (tax) và tổng chi phí (total). Người dùng cũng có thể gửi hoặc hủy đơn đặt hàng.

61c883c34d94b7f7.png

Cả hai tùy 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 đặt hàng, 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 đặt hàng đã được gửi.

acb7d7a5d9843bac.png

3. Bắt đầu

Tải mã nguồn 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 chi nhánh khớp với tên chi 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 (chính).

1e4c0d2c081a8fd2.png

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

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 (có thể trong thư mục Tải xuống (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ở sẵn thì hãy chuyển sang chọn tuỳ chọn File (Tệp) > Open (Mở) trên 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 (có thể 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. Đảm bảo ứng dụng được xây dựng 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 xây dựng 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 được cung cấp 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 tùy 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. 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 này được biểu thị bằng một số nguyên bắt nguồn từ đối tượng ItemType trong gói constants.

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 đặt hàng cho biết giá trị thành tiền và tính thuế bán hàng dựa trên các mặt hàng đã chọn (dùng để tính tổng giá trị đơn đặt hàng).

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 chỉ 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 giá trị 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 riêng tư là _entree, _side_accompaniment để lưu trữ lựa chọn hiện tại. Các biến này thuộc loại 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 loại LiveData<MenuItem?> không thay đổi được). Bố cục của phân đoạn sẽ truy cập các biến này để cho thấy các mặt hàng đã chọn trên màn hình. MenuItem bên trong đối tượng LiveData cũng có thể có tính chất 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, thuế suất được mã hoá cứng với giá trị 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 phải là 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 vào món chính được chuyển vào hàm (truy cập vào MenuItem bằng menuItems).
  4. Gọi updateSubtotal(), chuyển vào giá của món chính mới chọn.

Logic của setSide()setAccompaniment() giống hệt với quá trình triển khai cho setEntree().

updateSubtotal()

updateSubtotal() được gọi với một đối số cho giá mới 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ì thêm itemPrice vào _subtotal.
  2. Còn nếu _subtotalnull thì đặt _subtotal thành itemPrice.
  3. Sau khi đặt (hoặc cập nhật) _subtotal thì 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 đặt hàng. 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 đặt hàng 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 loại 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 rồi nhấp vào trình nghe 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 giá trị cho entree (món chính) trong ViewModel khi đã chọn nút đó. Văn bản trong chế độ xem 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 đặt hàng hoặc chuyển đến màn hình tiếp theo tương ứng.
  2. Làm tương tự trong fragment_side_menu.xml, thêm một biến liên kết cho SideMenuFragment, ngoại trừ đặt giá trị cho side (món phụ) trong mô hình chế độ xem khi chọn từng 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 hai biến liên kết, một biến cho OrderViewModel và một biến cho CheckoutFragment. Trong chế độ xem 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 đặt hàng và thời điểm huỷ đơn đặt hàng bằng các chức năng 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 hiện đ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() trongEntreeMenuFragment, 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ử 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 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 kiểm thử, ví dụ, nếu chỉ một kiểm thử không đạt và các kiểm thử khác đạ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 tùy 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 tùy chọn này trên trình đơn Run (Chạy).

80312efedf6e4dd3.png

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 cho thấy Run (Chạy) > Run 'app' (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 (Chạy) > Run (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.