Tính tiền boa

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 ButtonCalculate (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.

ebf5c40d4e12d4c7.png

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.

  1. Mở dự án Tip Time (Tính tiền boa) trong Android Studio.
  2. 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.
  3. Nếu chưa chọn, hãy chọn chế độ xem Android trên trình đơn thả xuống.

2a83e2b0aee106dd.png.

  • 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 Android
  • strings.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ị

  1. Mở tệp build.gradle của ứng dụng ( Gradle Scripts > build.gradle (Module: Tip_Time.app) )
  2. Trong phần android, hãy thêm các dòng sau:
buildFeatures {
    viewBinding = true
}
  1. 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).
  2. Nhấn vào Sync Now (Đồng bộ hoá ngay).

349d99c67c2f40f1.png

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.

674d243aa6f85b8b.png

  1. Mở MainActivity.kt (app > java > com.example.tiptime > MainActivity).
  2. Thay thế tất cả mã hiện tại của lớp MainActivity bằng mã này để thiết lập MainActivity 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)
    }
}
  1. 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ố.

  1. Dòng này sẽ khởi chạy đối tượng binding mà bạn sẽ dùng để truy cập vào Views trong bố cục activity_main.xml.
binding = ActivityMainBinding.inflate(layoutInflater)
  1. 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ụng binding.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 đó.

  1. Tại MainActivity.kt trong onCreate(), sau lệnh gọi đến setContentView(), 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ọi calculateTip().
binding.calculateButton.setOnClickListener{ calculateTip() }
  1. Vẫn bên trong lớp MainActivity nhưng bên ngoài onCreate(), hãy thêm một phương thức trợ giúp có tên calculateTip().
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().

  1. 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ủa EditText 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ượng binding 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.

  1. Tiếp theo, hãy chuyển đổi văn bản thành số thập phân. Gọi toDouble() trên stringInTextField rồi lưu trữ nó trong biến cost.
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 EditTextEditable, 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().

  1. Gọi toString() trên binding.costOfService.text để chuyển đổi thành String:
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.

  1. Trong calculateTip(), hãy lấy thuộc tính checkedRadioButtonId của tipOptions 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.

  1. 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.

  1. Trong calculateTip() sau những mã khác mà bạn đã thêm, hãy nhân tipPercentage với cost 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.

  1. Chỉ định thuộc tính isChecked của nút làm tròn cho biến roundUp.
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().

32c29f73a3f20f93.png

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.

  1. Thêm câu lệnh if chỉ định giá trị trần của tiền boa cho biến tip nếu roundUp 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.

  1. Trong calculateTip(), sau các đoạn mã khác, gọi NumberFormat.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ệ.

  1. Sử dụng trình định dạng số, liên kết lệnh gọi phương thức format() với tip 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)
  1. 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ản NumberFormat mà bạn muốn.
  2. Di chuột qua NumberFormat rồi chọn Import (Nhập) trong cửa sổ bật lên. d9d2f92d5ef01df6.png
  3. 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ệp MainActivityNumberFormat 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.

  1. Mở strings.xml (app > res > values > strings.xml)
  2. Thay đổi chuỗi tip_amount từ Tip Amount thành Tip 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.

  1. Bây giờ, hãy thiết lập văn bản của tipResult. Quay lại phương thức calculateTip() trong MainActivity.kt, hãy gọi getString(R.string.tip_amount, formattedTip) và gán cho thuộc tính text của kết quả tiền boa TextView.
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 đó.

  1. Mở activity_main.xml (app > res > layout > activity_main.xml).
  2. Tìm tip_result TextView.
  3. Xoá dòng có thuộc tính android:text.
android:text="@string/tip_amount"
  1. Thêm một dòng cho thuộc tính tools:text, thiết lập thành Tip 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.

  1. Lưu ý rằng văn bản về công cụ xuất hiện trong Layout Editor.
  2. 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).

42fd6cd5e24ca433.png

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?

  1. 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").
  2. 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).
  3. 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ì.

  1. 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.

1b68ee5190018c8a.png

  1. Cửa sổ Logcat xuất hiện ở cuối Android Studio và chứa văn bản lạ. 22139575476ae9d.png

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ố.

  1. 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)
  1. Đọ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ị.

  1. Tiếp tục đọc, bạn sẽ thấy một số lệnh gọi đến parseDouble().
  2. 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ớp MainActivity.
at com.example.tiptime.MainActivity.calculateTip(MainActivity.kt:22)
  1. 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 đổi String thành Double và chỉ định kết quả cho biến cost.
val cost = stringInTextField.toDouble()
  1. Hãy xem tài liệu về Kotlin để biết phương thức toDouble() hoạt động trên String. Phương thức này được gọi là String.toDouble().
  2. 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ù EditTextinputType=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.

  1. Trong calculateTip(), hãy thay đổi dòng khai báo biến cost để gọi toDoubleOrNull() thay vì gọi toDouble().
val cost = stringInTextField.toDoubleOrNull()
  1. 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ệnh return 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ệnh return kèm theo biểu thức.
if (cost == null) {
    return
}
  1. Chạy lại ứng dụng.
  2. 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:

  1. nhập số tiền hợp lệ cho chi phí
  2. nhấn vào Calculate (Tính toán) để tính tiền boa
  3. xoá chi phí
  4. 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 đề.

  1. 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ị.
  2. Bên trong if vừa thêm vào, trước câu lệnh return, hãy thêm một dòng để thiết lập thuộc tính text của tipResult 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().

  1. 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.

  1. Mở MainActivity.kt (app > java > com.example.tiptime > MainActivity).
  2. 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.

3737ebab72be9a5b.png

  1. 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ư"). 6205e927b4c14cf3.png

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.

  1. Chọn Make ‘calculateTip' ‘private' (Đặt "calculateTip" ở chế độ "riêng tư") hoặc thêm từ khoá private trước fun calculateTip(). Đường màu xám dưới calculateTip() 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.

  1. 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). 1d2c6f8415e96231.png
  2. 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.
  3. Cửa sổ có Inspection Results (Kết quả kiểm tra) sẽ xuất hiện ở dưới cùng.
  4. 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ư"). e40a6876f939c0d9.png
  5. 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ến binding. 8d9d7b5fc7ac5332.png
  6. 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).
  7. 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
  1. 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. 781017cbcada1194.png
  2. 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 trong when.
  3. Nhấn nút Inline variable (Biến cùng dòng). Android Studio thay thế selectedId trong biểu thức when 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.

  1. Sao chép biểu thức ở bên phải = từ dòng có gán roundUp.
val roundUp = binding.roundUpSwitch.isChecked
  1. 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)
}
  1. 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.

  1. 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àm calculateTip(), một lần cho trường hợp 0.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)
  1. 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)
}
  1. Cập nhật hàm calculateTip() để sử dụng hàm trợ giúp displayTip() 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.

966018df4a149822.png

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ủa RadioGroup để tìm xem RadioButton 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

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.