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).
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.
Màn hình tiếp theo cho phép người dùng thêm một món phụ (side).
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ọ.
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.
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.
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.
- Chuyển đến trang kho lưu trữ GitHub được cung cấp cho dự án.
- 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).
- 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.
- 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.
- 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)).
- 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
- Khởi động Android Studio.
- 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ở).
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.
- 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)).
- Nhấp đúp vào thư mục dự án đó.
- Chờ Android Studio mở dự án.
- Nhấp vào nút Run (Chạy)
để 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.
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
, previousSidePrice
và previousAccompanimentPrice
. 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
và _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
, side
và accompaniment
(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:
- 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 đặtpreviousEntreePrice
thành giá củacurrent _entree
. - Nếu
_subtotal
không phải lànull
, hãy trừpreviousEntreePrice
khỏi tổng giá trị thành tiền. - 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àoMenuItem
bằngmenuItems
). - Gọi
updateSubtotal()
, chuyển vào giá của món chính mới chọn.
Logic của setSide()
và 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:
- Nếu
_subtotal
không phải lànull
thì thêmitemPrice
vào_subtotal
. - Còn nếu
_subtotal
lànull
thì đặt_subtotal
thànhitemPrice
. - Sau khi đặt (hoặc cập nhật)
_subtotal
thì gọicalculateTaxAndTotal()
để 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:
- Đặt
_tax
bằng thuế suất nhân với thành tiền. - Đặ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:
fragment_entree_menu.xml
fragment_side_menu.xml
fragment_accompaniment_menu.xml
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:
- Trong
fragment_entree_menu.xml
, trong thẻ<data>
, hãy thêm một biến liên kết choEntreeMenuFragment
. Đối với mỗi nút chọn, bạn cần đặt giá trị cho entree (món chính) trongViewModel
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ínhonClick
chocancel_button
vànext_button
để huỷ đơn đặt hàng hoặc chuyển đến màn hình tiếp theo tương ứng. - Làm tương tự trong
fragment_side_menu.xml
, thêm một biến liên kết choSideMenuFragment
, 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ínhonClick
cho nút huỷ và nút tiếp theo. - 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 choAccompanimentMenuFragment
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). - 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 choOrderViewModel
và một biến choCheckoutFragment
. 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 trongOrderViewModel
. Bạn cũng cần đặt các biến thành tiền, thuế và tổng trongOrderViewModel
. Sau đó, hãy đặtonClickAttributes
để 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 trongCheckoutFragment
.
.
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()
.
EntreeMenuFragment
SideMenuFragment
AccompanimentMenuFragment
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.
Đâ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.
- Điều hướng từ
StartOrderFragment
đếnEntreeMenuFragment
- Điều hướng từ
EntreeMenuFragment
đếnSideMenuFragment
- Điều hướng từ
SideMenuFragment
đếnAccompanimentMenuFragment
- Điều hướng từ
AccompanimentMenuFragment
đếnCheckoutFragment
- Điều hướng từ
CheckoutFragment
đếnStartOrderFragment
- Điều hướng từ
EntreeMenuFragment
đếnStartOrderFragment
- Điều hướng từ
SideMenuFragment
đếnStartOrderFragment
- Điều hướng từ
AccompanimentMenuFragment
đếnStartOrderFragment
- Đ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
.
- Đối với phương thức
goToNextScreen()
trongEntreeMenuFragment
,SideMenuFragment
vàAccompanimentMenuFragment
, hãy điều hướng đến màn hình tiếp theo trong ứng dụng. - Đối với phương thức
cancelOrder()
trongEntreeMenuFragment
,SideMenuFragment
,AccompanimentMenuFragment
vàCheckoutFragment
, đầu tiên hãy gọiresetOrder()
trênsharedViewModel
, sau đó điều hướng đếnStartOrderFragment
. - Trong
StartOrderFragment
, hãy triển khaisetOnClickListener()
để điều hướng đếnEntreeMenuFragment
. - Trong
CheckoutFragment
, hãy triển khai phương thứcsubmitOrder()
. GọiresetOrder()
trênsharedViewModel
, sau đó điều hướng đếnStartOrderFragment
. - Cuối cùng, trong
MainActivity.kt
, hãy đặtnavController
thànhnavController
trongNavHostFragment
.
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
, NavigationTests
và OrderFunctionalityTests
.
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ử đó.
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).
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).
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).
Sau đó chọn mục tiêu kiểm thử trong trình đơn bật lên.
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.