1. Trước khi bắt đầu
Trong lớp học lập trình này, bạn sẽ viết đoạn mã cho công cụ tính tiền boa để dùng với giao diện người dùng mà bạn đã tạo trong lớp học lập trình trước đó (Tạo bố cục XML cho Android).
Điều kiện tiên quyết
- Có mã từ lớp học lập trình Tạo bố cục XML cho Android.
- Biết cách chạy ứng dụng Android qua Android Studio trong trình mô phỏng hoặc trên thiết bị.
Kiến thức bạn sẽ học được
- Cấu trúc cơ bản của ứng dụng Android.
- Cách đọc giá trị trong giao diện người dùng để nhập vào mã và thao tác với các giá trị đó.
- Cách sử dụng tính năng liên kết khung hiển thị (view binding) thay vì
findViewById()
để dễ dàng viết mã tương tác với các khung hiển thị. - Cách xử lý số thập phân trong Kotlin qua loại dữ liệu
Double
. - Cách định dạng số dưới dạng đơn vị tiền tệ.
- Cách sử dụng tham số chuỗi để tự động tạo chuỗi.
- Cách dùng Logcat trong Android Studio để phát hiện vấn đề trong ứng dụng.
Sản phẩm bạn sẽ tạo ra
- Ứng dụng tính tiền boa có nút Calculate (Tính toán) hoạt động được.
Bạn cần có
- Máy tính đã cài đặt phiên bản Android Studio ổn định mới nhất
- Mã khởi động cho ứng dụng Tip Time (Tính tiền boa) chứa bố cục cho công cụ tính tiền boa.
2. Tổng quan về ứng dụng khởi động
Ứng dụng Tip Time (Tính tiền boa) trong lớp học lập trình trước có toàn bộ giao diện người dùng cần thiết để tính tiền boa, nhưng không có mã để tính tiền boa. Có nút Calculate (Tính toán) nhưng nút này chưa hoạt động được. EditText
Cost of Service (Phí dịch vụ) cho phép người dùng nhập chi phí của dịch vụ. Danh sách RadioButtons
cho phép người dùng chọn tỷ lệ phần trăm cho tiền boa và Switch
cho phép người dùng chọn xem có làm tròn số tiền boa hay không. Số tiền boa xuất hiện trong TextView
và cuối cùng một Button
là Calculate (Tính toán) sẽ yêu cầu ứng dụng lấy dữ liệu qua các trường khác rồi tính số tiền boa. Lớp học lập trình này sẽ hướng dẫn phần này.
Cấu trúc dự án ứng dụng
Một dự án ứng dụng trong IDE bao gồm một số bộ phận, trong đó có mã Kotlin, bố cục XML và các tài nguyên khác như chuỗi và hình ảnh. Trước khi thay đổi ứng dụng, bạn nên tìm hiểu cách thực hiện.
- Mở dự án Tip Time (Tính tiền boa) trong Android Studio.
- Nếu cửa sổ Project (Dự án) không xuất hiện, hãy nhấp vào thẻ Project (Dự án) ở phía bên trái Android Studio.
- Nếu chưa chọn, hãy chọn chế độ xem Android trên trình đơn thả xuống.
- Thư mục java cho tệp Kotlin (hoặc tệp Java)
MainActivity
– lớp dữ liệu mà tại đó tất cả mã Kotlin cho logic tính tiền boa sẽ được chuyển đến- Thư mục res cho tài nguyên ứng dụng
activity_main.xml
– tệp bố cục cho ứng dụng Androidstrings.xml
– chứa tài nguyên chuỗi cho ứng dụng Android- Thư mục Gradle Scripts (Tập lệnh Gradle)
Gradle là một hệ thống xây dựng tự động mà Android Studio sử dụng. Bất cứ khi nào bạn thay đổi mã, thêm tài nguyên hoặc thực hiện các thay đổi khác đối với ứng dụng, Gradle sẽ tìm hiểu xem những gì đã thay đổi và thực hiện các bước cần thiết để tạo lại ứng dụng. Gradle cũng cài đặt ứng dụng của bạn trong trình mô phỏng hoặc thiết bị thực và kiểm soát việc thực thi ứng dụng.
Có các thư mục và tệp khác liên quan đến quá trình xây dựng ứng dụng của bạn, nhưng đây là những thư mục chính mà bạn sẽ sử dụng trong lớp học lập trình này và các lớp học sau.
3. Liên kết thành phần hiển thị
Để tính tiền boa, mã của bạn sẽ cần truy cập vào mọi yếu tố giao diện người dùng để đọc thông tin từ người dùng. Từ các lớp học lập trình trước, bạn có lẽ còn nhớ rằng mã của bạn cần tìm tham chiếu đến View
như Button
hoặc TextView
thì mã đó mới gọi được các phương thức trên View
hoặc truy cập được các thuộc tính. Khung Android cung cấp phương thức findViewById()
. Phương thức này thực hiện đúng những gì bạn cần, đó là khi có giá trị nhận dạng của View
thì phương thức này sẽ trả về tham chiếu đến nó. Phương pháp này có tác dụng. Tuy nhiên, khi bạn thêm nhiều thành phần hiển thị vào ứng dụng và giao diện người dùng trở nên phức tạp hơn, việc sử dụng findViewById()
có thể trở nên rườm rà.
Để thuận tiện, Android cũng cung cấp tính năng tên là liên kết khung hiển thị (view binding). Khi bạn thực hiện nhiều thao tác hơn, tính năng liên kết thành phần hiển thị sẽ giúp bạn dễ dàng và nhanh chóng gọi phương thức trên các thành phần hiển thị trong giao diện người dùng. Bạn sẽ phải bật tính năng liên kết thành phần hiển thị cho ứng dụng trong Gradle và thực hiện một số thay đổi trong mã lập trình.
Bật liên kết thành phần hiển thị
- Mở tệp
build.gradle
của ứng dụng ( Gradle Scripts > build.gradle (Module: Tip_Time.app) ) - Trong phần
android
, hãy thêm các dòng sau:
buildFeatures { viewBinding = true }
- Bạn sẽ thấy thông báo Gradle files have changed since last project sync (Các tệp Gradle đã thay đổi kể từ lần đồng bộ hoá dự án gần nhất).
- Nhấn vào Sync Now (Đồng bộ hoá ngay).
Sau vài phút, bạn sẽ thấy thông báo ở cuối cửa sổ Android Studio, Gradle sync finished (Đã đồng bộ hoá Gradle). Bạn có thể đóng tệp build.gradle
nếu muốn.
Khởi chạy đối tượng liên kết
Trong các lớp học lập trình trước, bạn từng gặp phương thức onCreate()
trong lớp MainActivity
. Đây là một trong những lệnh gọi đầu tiên khi ứng dụng khởi động và MainActivity
được khởi chạy. Thay vì gọi findViewById()
cho từng View
trong ứng dụng, bạn sẽ tạo và khởi chạy đối tượng liên kết chỉ một lần.
- Mở
MainActivity.kt
(app > java > com.example.tiptime > MainActivity). - Thay thế tất cả mã hiện tại của lớp
MainActivity
bằng mã này để thiết lậpMainActivity
cho việc sử dụng liên kết thành phần hiển thị:
class MainActivity : AppCompatActivity() {
lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
}
}
- Dòng dưới đây khai báo biến cấp cao nhất trong lớp này cho đối tượng liên kết. Biến này được định nghĩa ở cấp này vì sẽ được sử dụng trên nhiều phương thức trong lớp
MainActivity
.
lateinit var binding: ActivityMainBinding
Từ khoá lateinit
là kiến thức mới. Từ khoá này đảm bảo mã của bạn sẽ khởi chạy biến trước khi sử dụng biến đó. Nếu bạn không làm như vậy, ứng dụng của bạn sẽ gặp sự cố.
- Dòng này sẽ khởi chạy đối tượng
binding
mà bạn sẽ dùng để truy cập vàoViews
trong bố cụcactivity_main.xml
.
binding = ActivityMainBinding.inflate(layoutInflater)
- Thiết lập thành phần hiển thị nội dung của hoạt động (activity). Thay vì truyền giá trị nhận dạng tài nguyên của bố cục,
R.layout.activity_main
, thao tác này sẽ chỉ định gốc của hệ phân cấp khung hiển thị trong ứng dụngbinding.root
.
setContentView(binding.root)
Bạn có thể nhớ lại khái niệm khung hiển thị mẹ và khung hiển thị con; gốc liên kết toàn bộ khung hiển thị.
Giờ đây, khi cần tham chiếu đến một View
trong ứng dụng, bạn có thể lấy tệp đó từ đối tượng binding
thay vì gọi findViewById()
. Đối tượng binding
tự động định nghĩa tham chiếu cho mỗi View
có giá trị nhận dạng trong ứng dụng của bạn. Việc sử dụng tính năng liên kết khung hiển thị sẽ đơn giản hơn nhiều nên bạn thường không cần tạo biến để chứa tham chiếu cho View
, mà chỉ cần sử dụng biến này trực tiếp từ đối tượng liên kết.
// Old way with findViewById()
val myButton: Button = findViewById(R.id.my_button)
myButton.text = "A button"
// Better way with view binding
val myButton: Button = binding.myButton
myButton.text = "A button"
// Best way with view binding and no extra variable
binding.myButton.text = "A button"
Thật tuyệt vời phải không?
4. Tính tiền boa
Quá trình tính toán tiền boa sẽ bắt đầu khi người dùng nhấn vào nút Calculate (Tính toán). Quá trình này sẽ kiểm tra giao diện người dùng để xem chi phí dịch vụ và tỷ lệ phần trăm tiền boa mà người dùng muốn trả. Dựa trên thông tin này, bạn tính tổng số tiền phí dịch vụ và cho hiện số tiền boa.
Thêm trình nghe lượt nhấp vào nút
Bước đầu tiên là thêm trình nghe lượt nhấp để chỉ định chức năng của nút Calculate (Tính toán) khi người dùng nhấn vào nút đó.
- Tại
MainActivity.kt
trongonCreate()
, sau lệnh gọi đếnsetContentView()
, hãy thiết lập trình nghe lượt nhấp trên nút Calculate (Tính toán) và yêu cầu trình nghe lượt nhấp gọicalculateTip()
.
binding.calculateButton.setOnClickListener{ calculateTip() }
- Vẫn bên trong lớp
MainActivity
nhưng bên ngoàionCreate()
, hãy thêm một phương thức trợ giúp có têncalculateTip()
.
fun calculateTip() {
}
Đây là nơi bạn sẽ thêm mã để kiểm tra giao diện người dùng và tính toán tiền boa.
MainActivity.kt
class MainActivity : AppCompatActivity() {
lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.calculateButton.setOnClickListener{ calculateTip() }
}
fun calculateTip() {
}
}
Lấy thông tin về chi phí dịch vụ
Để tính tiền boa, thông tin đầu tiên bạn cần là chi phí dịch vụ. Thông tin dạng văn bản được lưu trữ trong EditText
nhưng bạn cần dạng số để tính toán. Có thể bạn còn nhớ loại Int
trong các lớp học lập trình khác, nhưng Int
chỉ có thể chứa số nguyên. Để sử dụng số thập phân trong ứng dụng, hãy sử dụng loại dữ liệu có tên là Double
thay vì Int
. Bạn có thể đọc thêm về các loại dữ liệu số trong Kotlin qua tài liệu này. Kotlin cung cấp một phương thức để chuyển đổi String
thành Double
, có tên là toDouble()
.
- Trước tiên, hãy xem thông tin văn bản về chi phí dịch vụ. Trong phương thức
calculateTip()
, hãy lấy thuộc tính văn bản củaEditText
Cost of Service (Chi phí dịch vụ) rồi gán thuộc tính đó cho biến tên làstringInTextField
. Hãy nhớ rằng bạn có thể truy cập vào thành phần giao diện người dùng bằng đối tượngbinding
và bạn có thể tham chiếu đến thành phần giao diện người dùng dựa trên tên nhận dạng tài nguyên theo quy ước viết hoa kiểu lạc đà.
val stringInTextField = binding.costOfService.text
Bạn sẽ nhận thấy .text
ở cuối. Phần đầu tiên là binding.costOfService
sẽ tham chiếu đến thành phần giao diện người dùng cho chi phí dịch vụ. Nếu bạn thêm .text
ở cuối, thì hệ thống sẽ nhận kết quả đó (đối tượng EditText
) rồi tải thuộc tính text
qua đối tượng đó. Đây được gọi là chuỗi (chain) thành phần hiển thị và là một mẫu triển khai rất phổ biến trong Kotlin.
- Tiếp theo, hãy chuyển đổi văn bản thành số thập phân. Gọi
toDouble()
trênstringInTextField
rồi lưu trữ nó trong biếncost
.
val cost = stringInTextField.toDouble()
Tuy nhiên, cách này không hiệu quả. Bạn cần gọi toDouble()
trên String
. Hoá ra thuộc tính text
của EditText
là Editable
, vì thuộc tính này biểu thị văn bản có thể thay đổi. Rất may là bạn có thể chuyển đổi Editable
thành String
bằng cách gọi toString()
.
- Gọi
toString()
trênbinding.costOfService.text
để chuyển đổi thànhString
:
val stringInTextField = binding.costOfService.text.toString()
Lúc này, stringInTextField.toDouble()
sẽ hoạt động.
Tại thời điểm này, phương thức calculateTip()
sẽ có dạng như sau:
fun calculateTip() {
val stringInTextField = binding.costOfService.text.toString()
val cost = stringInTextField.toDouble()
}
Xem tỷ lệ phần trăm tiền boa
Đến đấy, bạn đã biết chi phí của dịch vụ. Bây giờ, bạn cần biết tỷ lệ phần trăm mà người dùng đã chọn qua RadioGroup
trong RadioButtons
.
- Trong
calculateTip()
, hãy lấy thuộc tínhcheckedRadioButtonId
củatipOptions
RadioGroup
rồi gán thuộc tính đó cho biến tên làselectedId
.
val selectedId = binding.tipOptions.checkedRadioButtonId
Lúc này, bạn đã biết RadioButton
nào được chọn, liệu đó là R.id.option_twenty_percent
, R.id.option_eighteen_percent
hay R.id.fifteen_percent
, nhưng bạn cần tỷ lệ phần trăm tương ứng. Bạn có thể viết một loạt các câu lệnh if/else
nhưng sẽ dễ hơn nếu bạn sử dụng biểu thức when
.
- Thêm các dòng sau để lấy tỷ lệ phần trăm tiền boa.
val tipPercentage = when (selectedId) {
R.id.option_twenty_percent -> 0.20
R.id.option_eighteen_percent -> 0.18
else -> 0.15
}
Tại thời điểm này, phương thức calculateTip()
sẽ có dạng như sau:
fun calculateTip() {
val stringInTextField = binding.costOfService.text.toString()
val cost = stringInTextField.toDouble()
val selectedId = binding.tipOptions.checkedRadioButtonId
val tipPercentage = when (selectedId) {
R.id.option_twenty_percent -> 0.20
R.id.option_eighteen_percent -> 0.18
else -> 0.15
}
}
Tính số tiền boa và làm tròn giá trị
Hiện tại, bạn đã có chi phí dịch vụ và tỷ lệ phần trăm tiền boa, nên việc tính tiền boa rất đơn giản: tiền boa (tip) bằng chi phí (cost) nhân với tỷ lệ phần trăm tiền boa (tip percentage), tiền boa = chi phí dịch vụ * tỷ lệ phần trăm tiền boa. Bạn có thể làm tròn giá trị này nếu muốn.
- Trong
calculateTip()
sau những mã khác mà bạn đã thêm, hãy nhântipPercentage
vớicost
rồi chỉ định cho một biến tên làtip
.
var tip = tipPercentage * cost
Bạn có thể nhận thấy var
được dùng thay cho val
. Lý do là có thể bạn cần làm tròn giá trị nếu người dùng đã chọn tuỳ chọn đó, vì vậy giá trị có thể sẽ thay đổi.
Đối với thành phần Switch
, bạn có thể kiểm tra thuộc tính isChecked
để xem nút này có "on" (bật) hay không.
- Chỉ định thuộc tính
isChecked
của nút làm tròn cho biếnroundUp
.
val roundUp = binding.roundUpSwitch.isChecked
Làm tròn có nghĩa là tăng hoặc giảm một số thập phân tới giá trị số nguyên gần nhất, nhưng trong trường hợp này, bạn chỉ cần làm tròn lên hoặc tìm giá trị trần. Bạn có thể sử dụng hàm ceil()
để làm việc này. Có một số hàm có tên như vậy, nhưng hàm bạn cần được định nghĩa trong kotlin.math
. Bạn có thể thêm câu lệnh import
. Tuy nhiên, trong trường hợp này, bạn chỉ cần cho Android Studio biết ý định của mình bằng cách sử dụng kotlin.math.ceil()
.
Nếu bạn muốn sử dụng nhiều hàm toán học thì sẽ dễ hơn nếu bạn thêm câu lệnh import
.
- Thêm câu lệnh
if
chỉ định giá trị trần của tiền boa cho biếntip
nếuroundUp
có giá trị true (đúng).
if (roundUp) {
tip = kotlin.math.ceil(tip)
}
Tại thời điểm này, phương thức calculateTip()
sẽ có dạng như sau:
fun calculateTip() {
val stringInTextField = binding.costOfService.text.toString()
val cost = stringInTextField.toDouble()
val selectedId = binding.tipOptions.checkedRadioButtonId
val tipPercentage = when (selectedId) {
R.id.option_twenty_percent -> 0.20
R.id.option_eighteen_percent -> 0.18
else -> 0.15
}
var tip = tipPercentage * cost
val roundUp = binding.roundUpSwitch.isChecked
if (roundUp) {
tip = kotlin.math.ceil(tip)
}
}
Định dạng số tiền boa
Ứng dụng của bạn gần như đã hoàn thiện. Bạn đã tính toán tiền boa, giờ chỉ cần định dạng và cho hiện số tiền đó.
Như bạn kỳ vọng, Kotlin cung cấp phương thức để định dạng các loại số. Tuy nhiên, số tiền boa lại hơi khác một chút — đây là giá trị tiền tệ. Mỗi quốc gia sử dụng đơn vị tiền tệ riêng và có các quy tắc riêng về cách định dạng số thập phân. Ví dụ: đối với đồng đô la Mỹ, định dạng 1234,56 sẽ là $1,234.56, nhưng với đồng Euro, định dạng là €1.234,56. May mắn là khung lập trình Android cung cấp các phương thức định dạng số dưới dạng tiền tệ, vì vậy, bạn không cần phải biết hết mọi cách định dạng. Hệ thống tự động định dạng đơn vị tiền tệ dựa trên ngôn ngữ và các chế độ cài đặt khác mà người dùng đã chọn trên điện thoại. Đọc thêm về NumberFormat trong tài liệu dành cho nhà phát triển Android.
- Trong
calculateTip()
, sau các đoạn mã khác, gọiNumberFormat.getCurrencyInstance()
NumberFormat.getCurrencyInstance()
Thao tác này cung cấp một trình định dạng số mà bạn có thể dùng để định dạng số dưới dạng đơn vị tiền tệ.
- Sử dụng trình định dạng số, liên kết lệnh gọi phương thức
format()
vớitip
thành một chuỗi rồi gán kết quả cho biến tên làformattedTip
.
val formattedTip = NumberFormat.getCurrencyInstance().format(tip)
- Lưu ý
NumberFormat
được viết bằng màu đỏ. Nguyên nhân là do Android Studio không thể tự động xác định phiên bảnNumberFormat
mà bạn muốn. - Di chuột qua
NumberFormat
rồi chọn Import (Nhập) trong cửa sổ bật lên. - Trong danh sách các mục có thể nhập, hãy chọn NumberFormat (java.text). Android Studio thêm câu lệnh
import
ở đầu tệpMainActivity
vàNumberFormat
không còn màu đỏ nữa.
Hiển thị số tiền boa
Bây giờ, đã đến lúc hiển thị tiền boa trong thành phần thể hiện tiền boa TextView
của ứng dụng. Bạn có thể chỉ định formattedTip
cho thuộc tính text
. Tuy nhiên, bạn nên gắn nhãn biểu thị số tiền đó. Ở Hoa Kỳ, khi ngôn ngữ là tiếng Anh, bạn có thể cho hiện Tip Amount: $12.34, nhưng ở các ngôn ngữ khác, có thể con số cần phải xuất hiện ở đầu hoặc thậm chí là giữa chuỗi văn bản. Để giải quyết vấn đề này, khung lập trình Android cung cấp cơ chế gọi là tham số chuỗi, giúp người dịch ứng dụng của bạn có thể thay đổi vị trí xuất hiện của số nếu cần.
- Mở
strings.xml
(app > res > values > strings.xml) - Thay đổi chuỗi
tip_amount
từTip Amount
thànhTip Amount: %s
.
<string name="tip_amount">Tip Amount: %s</string>
%s
là nơi bạn sẽ chèn đơn vị tiền tệ đã định dạng.
- Bây giờ, hãy thiết lập văn bản của
tipResult
. Quay lại phương thứccalculateTip()
trongMainActivity.kt
, hãy gọigetString(R.string.tip_amount, formattedTip)
và gán cho thuộc tínhtext
của kết quả tiền boaTextView
.
binding.tipResult.text = getString(R.string.tip_amount, formattedTip)
Tại thời điểm này, phương thức calculateTip()
sẽ có dạng như sau:
fun calculateTip() {
val stringInTextField = binding.costOfService.text.toString()
val cost = stringInTextField.toDouble()
val selectedId = binding.tipOptions.checkedRadioButtonId
val tipPercentage = when (selectedId) {
R.id.option_twenty_percent -> 0.20
R.id.option_eighteen_percent -> 0.18
else -> 0.15
}
var tip = tipPercentage * cost
val roundUp = binding.roundUpSwitch.isChecked
if (roundUp) {
tip = kotlin.math.ceil(tip)
}
val formattedTip = NumberFormat.getCurrencyInstance().format(tip)
binding.tipResult.text = getString(R.string.tip_amount, formattedTip)
}
Bạn sắp hoàn thành rồi. Khi phát triển ứng dụng (và xem trước ứng dụng), bạn nên có một phần giữ chỗ cho TextView
đó.
- Mở
activity_main.xml
(app > res > layout > activity_main.xml). - Tìm
tip_result
TextView
. - Xoá dòng có thuộc tính
android:text
.
android:text="@string/tip_amount"
- Thêm một dòng cho thuộc tính
tools:text
, thiết lập thànhTip Amount: $10
.
tools:text="Tip Amount: $10"
Vì đây chỉ là một phần giữ chỗ nên bạn không cần trích xuất chuỗi này thành tài nguyên. Phần giữ chỗ sẽ không xuất hiện khi bạn chạy ứng dụng.
- Lưu ý rằng văn bản về công cụ xuất hiện trong Layout Editor.
- Chạy ứng dụng. Nhập số tiền cho chi phí và chọn một số tuỳ chọn, sau đó nhấn nút Calculate (Tính toán).
Chúc mừng bạn, ứng dụng đã hoạt động! Nếu bạn không nhận được số tiền boa chính xác, hãy quay lại bước 1 của mục này và đảm bảo rằng bạn đã thực hiện tất cả thay đổi cần thiết về mã.
5. Kiểm thử và gỡ lỗi
Bạn đã chạy ứng dụng ở nhiều bước để đảm bảo ứng dụng hoạt động như mong muốn, nhưng giờ đã đến lúc kiểm thử sâu hơn.
Bây giờ, hãy suy nghĩ về cách thông tin di chuyển trong ứng dụng của bạn qua phương thức calculateTip()
và những vấn đề có thể xảy ra ở mỗi bước.
Ví dụ: điều gì sẽ xảy ra trong dòng này:
val cost = stringInTextField.toDouble()
nếu stringInTextField
không biểu thị số? Điều gì sẽ xảy ra nếu người dùng không nhập văn bản nào và stringInTextField
trống?
- Chạy ứng dụng của bạn trong trình mô phỏng, nhưng thay vì sử dụng Run > Run ‘app (Chạy > Chạy "ứng dụng"), hãy sử dụng Run > Debug ‘app' (Chạy > Gỡ lỗi "ứng dụng").
- Hãy thử một số giá trị chi phí, số tiền boa và bật hoặc chọn làm tròn hoặc không làm tròn để xác minh rằng bạn sẽ nhận được kết quả dự kiến cho mỗi trường hợp khi nhấn vào Calculate (Tính toán).
- Bây giờ, hãy thử xoá toàn bộ văn bản trong trường Cost of Service (Chi phí dịch vụ) rồi nhấn vào Calculate (Tính toán). Thật không may, chương trình của bạn gặp sự cố.
Khắc phục sự cố
Bước đầu tiên khi xử lý lỗi là tìm hiểu điều gì đã xảy ra. Android Studio lưu giữ nhật ký về những gì đang xảy ra trong hệ thống và bạn có thể sử dụng nhật ký này để tìm hiểu xem đã xảy ra sự cố gì.
- Nhấn nút Logcat ở cuối Android Studio hoặc chọn View > Tool Windows > Logcat (Chế độ xem > Cửa sổ công cụ > Logcat) trong trình đơn.
- Cửa sổ Logcat xuất hiện ở cuối Android Studio và chứa văn bản lạ.
Văn bản này là một dấu vết ngăn xếp (stack trace), tức là danh sách phương thức được gọi khi xảy ra sự cố.
- Di chuyển lên trên trong văn bản Logcat cho đến khi bạn thấy một dòng có chứa văn bản
FATAL EXCEPTION
.
2020-06-24 10:09:41.564 24423-24423/com.example.tiptime E/AndroidRuntime: FATAL EXCEPTION: main
Process: com.example.tiptime, PID: 24423
java.lang.NumberFormatException: empty String
at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1842)
at sun.misc.FloatingDecimal.parseDouble(FloatingDecimal.java:110)
at java.lang.Double.parseDouble(Double.java:538)
at com.example.tiptime.MainActivity.calculateTip(MainActivity.kt:22)
at com.example.tiptime.MainActivity$onCreate$1.onClick(MainActivity.kt:17)
- Đọc tiếp xuống dưới cho đến khi bạn tìm thấy dòng có
NumberFormatException
.
java.lang.NumberFormatException: empty String
Ở bên phải có ghi empty String
. Loại dữ liệu của ngoại lệ sẽ cho bạn biết sự cố có liên quan đến một định dạng số và nội dung còn lại cho bạn biết nguyên nhân của vấn đề: đã tìm thấy một String
trống khi lẽ ra phải là String
có giá trị.
- Tiếp tục đọc, bạn sẽ thấy một số lệnh gọi đến
parseDouble()
. - Bên dưới các lệnh gọi đó, hãy tìm dòng có
calculateTip
. Vui lòng lưu ý trong đó cũng có lớpMainActivity
.
at com.example.tiptime.MainActivity.calculateTip(MainActivity.kt:22)
- Hãy xem xét kỹ dòng đó, bạn sẽ thấy chính xác vị trí lệnh gọi trong mã, đó là dòng 22 trong
MainActivity.kt
. (Nếu bạn gõ mã lập trình khác đi, dòng này có thể là một số khác.) Dòng đó chuyển đổiString
thànhDouble
và chỉ định kết quả cho biếncost
.
val cost = stringInTextField.toDouble()
- Hãy xem tài liệu về Kotlin để biết phương thức
toDouble()
hoạt động trênString
. Phương thức này được gọi làString.toDouble()
. - Trang có nội dung là: "Exceptions:
NumberFormatException
– if the string is not a valid representation of a number" (Ngoại lệ: NumberFormatException – nếu chuỗi này không phải là một cách biểu thị hợp lệ của một số).
Ngoại lệ (exception) là cách hệ thống thông báo khi có sự cố. Trong trường hợp này, vấn đề xảy ra là toDouble()
không thể chuyển đổi String
trống thành Double
. Mặc dù EditText
có inputType=numberDecimal
, nhưng bạn vẫn có thể nhập một số giá trị mà toDouble()
không xử lý được, chẳng hạn như một chuỗi trống.
Tìm hiểu về giá trị rỗng
Lệnh gọi toDouble()
sẽ không hoạt động trên một chuỗi trống hoặc chuỗi không đại diện cho một số thập phân hợp lệ. May mắn là Kotlin cũng cung cấp một phương thức có tên là toDoubleOrNull()
để xử lý các vấn đề này. Phương thức này trả về một số thập phân khi có thể hoặc trả về null
nếu có sự cố.
Null (rỗng) là một giá trị đặc biệt, nghĩa là "không có giá trị". Giá trị này khác với một Double
có giá trị 0.0
hoặc String
trống không có ký tự, ""
. Null
có nghĩa là không có giá trị, không có Double
hoặc không có String
. Nhiều phương thức chờ đợi giá trị trả về nên không biết cách xử lý null
và sẽ ngừng hoạt động, nghĩa là ứng dụng sẽ gặp lỗi. Vì vậy, Kotlin cố gắng giới hạn phạm vi sử dụng null
. Bạn sẽ tìm hiểu thêm về chủ đề này trong các bài học sau.
Ứng dụng của bạn có thể kiểm tra null
được trả về từ toDoubleOrNull()
và hoạt động khác đi để ứng dụng không gặp lỗi.
- Trong
calculateTip()
, hãy thay đổi dòng khai báo biếncost
để gọitoDoubleOrNull()
thay vì gọitoDouble()
.
val cost = stringInTextField.toDoubleOrNull()
- Sau dòng đó, hãy thêm một câu lệnh để kiểm tra xem
cost
có phải lànull
không và nếu có thì trả về qua phương thức đó. Lệnhreturn
có nghĩa là thoát khỏi phương thức mà không thực thi các lệnh còn lại. Nếu phương thức cần trả về một giá trị, bạn sẽ chỉ định phương thức đó bằng một lệnhreturn
kèm theo biểu thức.
if (cost == null) {
return
}
- Chạy lại ứng dụng.
- Không có văn bản trong trường Cost of Service (Chi phí dịch vụ), hãy nhấn vào Calculate (Tính toán). Lần này, ứng dụng không gặp sự cố! Thật tuyệt! Bạn đã tìm thấy ra lỗi và khắc phục thành công!
Xử lý một trường hợp khác
Không phải tất cả lỗi đều khiến ứng dụng của bạn gặp sự cố. Đôi khi, kết quả có thể khiến người dùng nhầm lẫn.
Sau đây là một trường hợp khác mà bạn nên cân nhắc. Điều gì sẽ xảy ra nếu người dùng:
- nhập số tiền hợp lệ cho chi phí
- nhấn vào Calculate (Tính toán) để tính tiền boa
- xoá chi phí
- nhấn vào Calculate (Tính toán) lần nữa?
Lần đầu tiên, số tiền boa sẽ được tính toán và xuất hiện trên màn hình như dự kiến. Lần thứ hai, phương thức calculateTip()
sẽ trở về sớm do hoá đơn bạn vừa mới thêm, nhưng ứng dụng sẽ vẫn hiện số tiền trước đó. Việc này có thể gây nhầm lẫn cho người dùng, vì vậy hãy thêm mã để xoá số tiền boa nếu có vấn đề.
- Xác nhận sự cố này là do nhập chi phí hợp lệ và nhấn vào Calculate (Tính toán), sau đó xoá văn bản và nhấn lại vào Calculate (Tính toán). Giá trị tiền boa đầu tiên sẽ vẫn hiển thị.
- Bên trong
if
vừa thêm vào, trước câu lệnhreturn
, hãy thêm một dòng để thiết lập thuộc tínhtext
củatipResult
thành một chuỗi trống.
if (cost == null) {
binding.tipResult.text = ""
return
}
Mã này sẽ xoá số tiền trước khi trở lại từ calculateTip()
.
- Chạy lại ứng dụng và thử trường hợp trên. Giá trị tiền boa đầu tiên sẽ biến mất khi bạn nhấn vào Calculate (Tính toán) lần thứ hai.
Xin chúc mừng! Bạn đã tạo một ứng dụng tính toán tiền boa dành cho Android và xử lý một số trường hợp ngoại lệ!
6. Áp dụng các phương pháp lập trình hay
Công cụ tính tiền boa của bạn hiện đang hoạt động, nhưng bạn có thể cải thiện mã một chút để thao tác dễ dàng hơn sau này bằng cách áp dụng các phương pháp lập trình hay.
- Mở
MainActivity.kt
(app > java > com.example.tiptime > MainActivity). - Khi nhìn vào đầu phương thức
calculateTip()
, bạn sẽ thấy phương thức này được gạch dưới bằng một đường nhấp nháy màu xám.
- Di con trỏ qua
calculateTip()
, bạn sẽ thấy một thông báo như sau Function ‘calculateTip' could be private (Hàm "calculateTip" có thể ở chế độ riêng tư), bên dưới là đề xuất Make ‘calculateTip' ‘private' (Đặt "calculateTip" ở chế độ "riêng tư").
Theo kiến thức trong các lớp học lập trình trước, private
có nghĩa là phương thức hoặc biến chỉ hiển thị với mã trong lớp đó, trong trường hợp này là lớp MainActivity
. Không có lý do nào để mã bên ngoài MainActivity
gọi calculateTip()
, vì vậy bạn có thể yên tâm thiết lập chế độ private
.
- Chọn Make ‘calculateTip' ‘private' (Đặt "calculateTip" ở chế độ "riêng tư") hoặc thêm từ khoá
private
trướcfun calculateTip()
. Đường màu xám dướicalculateTip()
biến mất.
Kiểm tra mã
Đường màu xám rất mờ và dễ bị cho qua. Bạn có thể xem xét toàn bộ tệp để tìm các đường màu xám khác, nhưng có một cách đơn giản hơn để đảm bảo bạn tìm thấy tất cả đề xuất.
- Khi
MainActivity.kt
vẫn mở, hãy chọn Analyze > Inspect Code… (Phân tích > Kiểm tra mã…) trong trình đơn. Bạn sẽ thấy một hộp thoại có tên Specify Inspection Scope (Chỉ định phạm vi kiểm tra). - Chọn tuỳ chọn bắt đầu bằng File (Tệp) rồi nhấn OK. Thao tác sẽ giới hạn phạm vi kiểm tra trong
MainActivity.kt
. - Cửa sổ có Inspection Results (Kết quả kiểm tra) sẽ xuất hiện ở dưới cùng.
- Nhấp vào hình tam giác màu xám bên cạnh Kotlin, rồi nhấp vào Style issues (Vấn đề về định kiểu) cho đến khi bạn thấy hai thông báo. Thông báo đầu tiên là Class member can have ‘private' visibility (Thành viên trong lớp có thể có chế độ hiển thị "riêng tư").
- Nhấp vào hình tam giác màu xám cho đến khi bạn nhìn thấy thông báo Property ‘binding' could be private (Thuộc tính "liên kết" có thể ở chế độ riêng tư) và nhấp vào thông báo. Android Studio cho hiện một số mã trong
MainActivity
và làm nổi bật biếnbinding
. - Nhấn nút Make ‘binding' ‘private' (Thiết lập "liên kết" ở chế độ "riêng tư") Android Studio sẽ xoá vấn đề này khỏi trang Inspection Results (Kết quả kiểm tra).
- Nếu xem
binding
trong mã, bạn sẽ thấy Android Studio đã thêm từ khoáprivate
trước khi khai báo.
private lateinit var binding: ActivityMainBinding
- Nhấp vào hình tam giác màu xám trong kết quả cho đến khi bạn thấy thông báo Variable declaration could be inlined (Nội dung khai báo biến có thể nằm trên cùng dòng). Android Studio một lần nữa hiển thị một số mã, nhưng lần này sẽ làm nổi bật biến
selectedId
. - Nếu nhìn vào mã, bạn sẽ thấy
selectedId
chỉ được sử dụng 2 lần: lần thứ nhất là trong dòng được làm nổi bật với giá trị được gán làtipOptions.checkedRadioButtonId
và ở dòng tiếp theo trongwhen
. - Nhấn nút Inline variable (Biến cùng dòng). Android Studio thay thế
selectedId
trong biểu thứcwhen
bằng giá trị được chỉ định trong dòng trước. Sau đó, công cụ này sẽ xoá hoàn toàn dòng trước đó vì không cần thiết nữa!
val tipPercentage = when (binding.tipOptions.checkedRadioButtonId) {
R.id.option_twenty_percent -> 0.20
R.id.option_eighteen_percent -> 0.18
else -> 0.15
}
Thật thú vị! Mã của bạn ít đi một dòng và một biến.
Xoá các biến không cần thiết
Android Studio không còn kết quả nào từ quá trình kiểm tra. Tuy nhiên, nếu xem kỹ mã của mình, bạn sẽ thấy một mẫu tương tự như những gì bạn vừa thay đổi: biến roundUp
được gán trên một dòng, được sử dụng trên dòng tiếp theo và không được dùng cho nơi nào khác.
- Sao chép biểu thức ở bên phải
=
từ dòng có gánroundUp
.
val roundUp = binding.roundUpSwitch.isChecked
- Thay thế
roundUp
trong dòng tiếp theo bằng biểu thức mà bạn vừa sao chép,binding.roundUpSwitch.isChecked
.
if (binding.roundUpSwitch.isChecked) {
tip = kotlin.math.ceil(tip)
}
- Xoá dòng có
roundUp
vì không cần thiết nữa.
Bạn vừa làm thực hiện cùng thao tác mà Android Studio giúp bạn thực hiện thông qua biến selectedId
. Xin nhắc lại, mã của bạn phải ít đi một dòng và một biến. Đây là những thay đổi nhỏ nhưng giúp mã của bạn ngắn gọn và dễ đọc hơn.
(Không bắt buộc) Loại bỏ mã lặp lại
Khi ứng dụng của bạn đã chạy đúng cách, bạn có thể tìm cách dọn dẹp mã lập trình và làm cho mã ngắn gọn hơn. Ví dụ: khi bạn không nhập giá trị cho chi phí dịch vụ, ứng dụng sẽ cập nhật tipResult
thành một chuỗi trống ""
. Khi có giá trị, bạn sử dụng NumberFormat
để định dạng giá trị đó. Bạn có thể áp dụng chức năng này ở những nơi khác trong ứng dụng, chẳng hạn như để hiển thị tiền boa 0.0
thay vì hiển thị chuỗi trống.
Để giảm mã trùng lặp, bạn có thể trích xuất hai dòng mã này vào hàm riêng. Hàm trợ giúp này có thể nhập một số tiền boa dưới dạng Double
, định dạng giá trị này và cập nhật tipResult
TextView
trên màn hình.
- Xác định mã trùng lặp trong
MainActivity.kt
. Bạn có thể sử dụng các dòng mã này nhiều lần trong hàmcalculateTip()
, một lần cho trường hợp0.0
và một lần cho trường hợp chung.
val formattedTip = NumberFormat.getCurrencyInstance().format(0.0)
binding.tipResult.text = getString(R.string.tip_amount, formattedTip)
- Di chuyển mã trùng lặp vào hàm riêng. Một thay đổi đối với mã là nhận số tiền boa tham số để mã hoạt động ở nhiều vị trí.
private fun displayTip(tip : Double) {
val formattedTip = NumberFormat.getCurrencyInstance().format(tip)
binding.tipResult.text = getString(R.string.tip_amount, formattedTip)
}
- Cập nhật hàm
calculateTip()
để sử dụng hàm trợ giúpdisplayTip()
và kiểm tra cả0.0
.
MainActivity.kt
private fun calculateTip() {
...
// If the cost is null or 0, then display 0 tip and exit this function early.
if (cost == null || cost == 0.0) {
displayTip(0.0)
return
}
...
if (binding.roundUpSwitch.isChecked) {
tip = kotlin.math.ceil(tip)
}
// Display the formatted tip value on screen
displayTip(tip)
}
Lưu ý
Mặc dù ứng dụng hiện đang hoạt động nhưng chưa sẵn sàng để phát hành chính thức. Bạn cần kiểm thử thêm. Bạn cần cải thiện thêm về mặt hình ảnh và tuân thủ các nguyên tắc của Material Design. Bạn cũng sẽ tìm hiểu cách thay đổi giao diện và biểu tượng ứng dụng trong các lớp học lập trình sau.
7. Mã giải pháp
Mã giải pháp cho lớp học lập trình này có dạng như dưới đây.
MainActivity.kt
(lưu ý về dòng đầu tiên: hãy thay thế tên gói nếu tên gói của bạn không phải là com.example.tiptime
)
package com.example.tiptime
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import com.example.tiptime.databinding.ActivityMainBinding
import java.text.NumberFormat
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.calculateButton.setOnClickListener { calculateTip() }
}
private fun calculateTip() {
val stringInTextField = binding.costOfService.text.toString()
val cost = stringInTextField.toDoubleOrNull()
if (cost == null) {
binding.tipResult.text = ""
return
}
val tipPercentage = when (binding.tipOptions.checkedRadioButtonId) {
R.id.option_twenty_percent -> 0.20
R.id.option_eighteen_percent -> 0.18
else -> 0.15
}
var tip = tipPercentage * cost
if (binding.roundUpSwitch.isChecked) {
tip = kotlin.math.ceil(tip)
}
val formattedTip = NumberFormat.getCurrencyInstance().format(tip)
binding.tipResult.text = getString(R.string.tip_amount, formattedTip)
}
}
Chỉnh sửa strings.xml
<string name="tip_amount">Tip Amount: %s</string>
Chỉnh sửa activity_main.xml
...
<TextView
android:id="@+id/tip_result"
...
tools:text="Tip Amount: $10" />
...
Chỉnh sửa build.gradle
của mô-đun ứng dụng
android {
...
buildFeatures {
viewBinding = true
}
...
}
8. Tóm tắt
- Tính năng liên kết thành phần hiển thị cho phép bạn dễ dàng viết mã tương tác với các thành phần giao diện người dùng trong ứng dụng
- Loại dữ liệu
Double
trong Kotlin có thể lưu trữ số thập phân. - Sử dụng thuộc tính
checkedRadioButtonId
củaRadioGroup
để tìm xemRadioButton
nào được chọn. - Sử dụng
NumberFormat.getCurrencyInstance()
để lấy trình định dạng giúp chuyển số sang đơn vị tiền tệ. - Bạn có thể sử dụng các tham số chuỗi như
%s
để tạo các chuỗi động có thể dễ dàng dịch sang ngôn ngữ khác. - Việc kiểm thử là rất quan trọng!
- Bạn có thể sử dụng Logcat trong Android Studio để khắc phục những vấn đề như sự cố ứng dụng.
- Dấu vết ngăn xếp cung cấp danh sách phương thức đã gọi. Điều này có thể hữu ích nếu mã tạo ra ngoại lệ.
- Ngoại lệ cho biết sự cố mà mã không mong muốn.
Null
có nghĩa là "không có giá trị".- Không phải mã nào cũng xử lý được giá trị
null
, vì vậy, hãy cẩn thận khi sử dụng. - Sử dụng Analyze > Inspect Code (Phân tích > Kiểm tra mã) để xem nội dung đề xuất về cách cải thiện mã của bạn.
9. Các lớp học lập trình khác để cải thiện giao diện người dùng
Chúc mừng bạn đã tạo thành công cụ tính tiền boa! Bạn sẽ nhận thấy rằng vẫn còn nhiều cách cải thiện giao diện người dùng để làm cho ứng dụng trông đẹp hơn. Nếu bạn quan tâm, hãy tham khảo các lớp học lập trình sau để tìm hiểu thêm cách thay đổi giao diện và biểu tượng ứng dụng, cũng như cách làm theo các phương pháp hay nhất theo nguyên tắc Material Design cho ứng dụng Tip Time!
10. Tìm hiểu thêm
- Loại dữ liệu
Double
trong Kotlin - Loại dữ liệu số trong Kotlin
- Xử lý an toàn giá trị rỗng trong Kotlin
- Tệp kê khai ứng dụng
- Liên kết
View
NumberFormat.getCurrencyInstance()
- tham số chuỗi
- kiểm thử
- Logcat
- Phân tích dấu vết ngăn xếp
11. Tự thực hành
- Với ứng dụng chuyển đổi đơn vị để nấu ăn trong bài tập thực hành trước, hãy thêm mã cho logic và các phép tính để chuyển đổi đơn vị như mililit hoặc ounce chất lỏng.