Các thành phần chung, đối tượng và tiện ích

1. Giới thiệu

Trong nhiều thập kỷ, các lập trình viên đã nghĩ ra một số tính năng về ngôn ngữ lập trình để giúp bạn viết mã tốt hơn – chẳng hạn như dùng ít mã hơn để diễn đạt cùng một ý tưởng, trừu tượng hóa giúp trình bày các ý tưởng phức tạp và viết mã ngăn các nhà phát triển khác vô tình mắc một sai lầm nào đó. Ngôn ngữ Kotlin cũng không ngoại lệ, nó có nhiều tính năng giúp nhà phát triển viết mã sinh động hơn.

Thật không may là những tính năng này có thể khiến mọi thứ trở nên khó khăn nếu bạn mới lập trình lần đầu. Dù nghe có vẻ hữu ích, nhưng mức độ hữu ích và vấn đề mà các tính năng này hỗ trợ không phải lúc nào cũng rõ ràng. Bạn có thể đã thấy một số tính năng được sử dụng trong Compose và các thư viện khác.

Dù không thể so sánh với các trải nghiệm thực tế, nhưng lớp học lập trình này cũng sẽ cho bạn thấy một vài khái niệm trong Kotlin giúp bạn sắp xếp các ứng dụng có cấu trúc lớn hơn:

  • Các thành phần chung
  • Các loại lớp khác nhau (lớp enum và lớp dữ liệu)
  • Đối tượng singleton và các đối tượng đồng hành
  • Các thuộc tính và hàm mở rộng
  • Hàm phạm vi

Sau khi kết thúc khóa học, bạn sẽ có hiểu biết sâu hơn về mã đã được học, đồng thời tìm hiểu một số ví dụ về thời điểm bạn sẽ gặp, hoặc sử dụng những khái niệm này trong ứng dụng của mình.

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

  • Làm quen với các khái niệm lập trình hướng đối tượng, bao gồm cả tính kế thừa.
  • Cách xác định và triển khai giao diện.

Kiến thức bạn sẽ học được

  • Cách xác định tham số loại chung cho một lớp.
  • Cách tạo bản sao cho một lớp chung.
  • Trường hợp sử dụng của lớp enum và dữ liệu.
  • Cách xác định tham số loại chung để có thể triển khai giao diện.
  • Cách sử dụng các hàm phạm vi để truy cập vào các phương thức và thuộc tính của lớp.
  • Cách xác định các đối tượng singleton và đối tượng đồng hành cho một lớp.
  • Cách mở rộng các lớp hiện có bằng thuộc tính và phương thức mới.

Bạn cần có

  • Một trình duyệt web có quyền truy cập vào Kotlin Playground.

2. Tạo một lớp có thể tái sử dụng với các thành phần chung

Giả sử bạn đang viết một ứng dụng cho bài kiểm tra trực tuyến, tương tự như những bài kiểm tra bạn đã thấy trong khóa học này. Thường có nhiều loại câu hỏi kiểm tra, chẳng hạn như câu hỏi điền vào chỗ trống, hoặc câu trả lời đúng hay sai. Mỗi câu hỏi kiểm tra có thể được biểu thị bằng một lớp, với một vài thuộc tính.

Nội dung câu hỏi trong bài kiểm tra có thể được biểu thị bằng một chuỗi. Câu hỏi của bài kiểm tra cũng cần phải biểu thị câu trả lời. Tuy nhiên, các loại câu hỏi khác - chẳng hạn như đúng hoặc sai - có thể cần phải biểu thị câu trả lời bằng một loại dữ liệu khác. Chúng ta sẽ cùng xác định ba loại câu hỏi.

  • Câu hỏi điền vào chỗ trống: Câu trả lời là một từ do String đại diện.
  • Câu hỏi đúng hoặc sai: Câu trả lời được thể hiện bằng Boolean.
  • Bài toán: Câu trả lời là một giá trị số. Câu trả lời cho một bài toán số học đơn giản được biểu thị bằng Int.

Ngoài ra, các câu hỏi của bài kiểm tra trong ví dụ này sẽ luôn có điểm xếp hạng độ khó, bất kể loại câu hỏi nào. Điểm xếp hạng độ khó được biểu thị bằng một chuỗi có thể có ba giá trị là "easy", "medium" hoặc "hard".

Xác định các lớp đại diện cho từng loại câu hỏi kiểm tra:

  1. Chuyển đến phần Kotlin Playground.
  2. Phía trên hàm main(), hãy xác định một lớp cho các câu hỏi điền vào chỗ trống có tên FillInTheBlankQuestion, bao gồm thuộc tính String cho questionText, String cho answer, và String cho difficulty.
class FillInTheBlankQuestion(
    val questionText: String,
    val answer: String,
    val difficulty: String
)
  1. Bên dưới lớp FillInTheBlankQuestion, hãy xác định một lớp khác có tên TrueOrFalseQuestion cho các câu hỏi đúng hoặc sai, bao gồm thuộc tính String cho questionText, Boolean cho answer, và String cho difficulty.
class TrueOrFalseQuestion(
    val questionText: String,
    val answer: Boolean,
    val difficulty: String
)
  1. Cuối cùng, dưới hai lớp còn lại, hãy xác định lớp NumericQuestion, bao gồm thuộc tính String cho questionText, Int cho answer, và String của difficulty.
class NumericQuestion(
    val questionText: String,
    val answer: Int,
    val difficulty: String
)
  1. Hãy xem mã bạn đã viết. Bạn có nhận thấy tần suất lặp lại không?
class FillInTheBlankQuestion(
    val questionText: String,
    val answer: String,
    val difficulty: String
)

class TrueOrFalseQuestion(
    val questionText: String,
    val answer: Boolean,
    val difficulty: String
)
class NumericQuestion(
    val questionText: String,
    val answer: Int,
    val difficulty: String
)

Cả ba lớp đều có cùng thuộc tính là questionText, answerdifficulty. Điểm khác biệt duy nhất là loại dữ liệu của thuộc tính answer. Bạn có thể nghĩ rằng dĩ nhiên giải pháp là tạo một lớp mẹ với questionTextdifficulty, đồng thời mỗi lớp con sẽ xác định thuộc tính answer.

Tuy nhiên, việc sử dụng tính năng kế thừa cũng gặp phải vấn đề như trên. Mỗi lần thêm một loại câu hỏi mới, bạn phải thêm thuộc tính answer. Sự khác biệt duy nhất là loại dữ liệu. Ngoài ra, việc một lớp mẹ Question không có thuộc tính câu trả lời cũng có vẻ bất thường.

Khi bạn muốn một thuộc tính có các loại dữ liệu khác nhau, thì phân lớp con không phải là đáp án. Thay vào đó, Kotlin cung cấp một giá trị được gọi là loại chung, cho phép bạn lấy một thuộc tính duy nhất có thể chứa các loại dữ liệu khác nhau, tùy thuộc vào trường hợp sử dụng cụ thể.

Loại dữ liệu chung là gì?

Loại chung, hoặc viết tắt là chung, cho phép một loại dữ liệu chẳng hạn như lớp, chỉ định một kiểu dữ liệu giữ chỗ không xác định có thể dùng cho các thuộc tính và phương thức của nó. Chính xác thì điều này có nghĩa là gì?

Ở ví dụ trên, thay vì xác định thuộc tính câu trả lời cho mỗi loại dữ liệu, bạn có thể tạo một lớp duy nhất để đại diện cho bất kỳ câu hỏi nào, đồng thời sử dụng tên trình giữ chỗ cho loại dữ liệu có thuộc tính answer. Bạn có thể chỉ định loại dữ liệu thực tế:String, Int, Boolean, v.v. khi tạo lớp đó. Bất cứ nơi nào tên trình giữ chỗ được dùng, loại dữ liệu được truyền vào lớp cũng sẽ được sử dụng thay thế. Dưới đây là cú pháp để xác định loại chung cho một lớp:

10a38dbaa8f10ec6.png

Loại dữ liệu chung được cung cấp khi tạo bản sao cho một lớp, do đó nó cần được định nghĩa như một phần của chữ ký lớp. Sau tên lớp là dấu ngoặc nhọn hướng sang trái (<), theo sau là tên trình giữ chỗ cho loại dữ liệu, kế đến là dấu ngoặc nhọn hướng sang phải (>).

Sau đó, bạn có thể dùng tên trình giữ chỗ ở bất cứ nơi nào có loại dữ liệu thực trong lớp đó, chẳng hạn như cho một thuộc tính.

ec3dcacd1a216bd4.png

Việc này cũng giống với mọi khai báo thuộc tính khác, ngoại trừ tên trình giữ chỗ được sử dụng thay cho loại dữ liệu.

Làm sao để lớp nhận biết được loại dữ liệu nào sẽ sử dụng? Loại dữ liệu chung được sử dụng sẽ truyền ở dạng tham số trong dấu ngoặc nhọn khi bạn tạo bản sao cho lớp.

4a21173cb6d2451b.png

Sau tên lớp là dấu ngoặc nhọn hướng sang trái (<), theo sau là loại dữ liệu thực tế, String, Boolean, Int, v.v., tiếp đến là dấu ngoặc phải (>). Loại dữ liệu của giá trị mà bạn truyền vào cho thuộc tính chung phải khớp với loại dữ liệu trong dấu ngoặc nhọn. Bạn sẽ tạo thuộc tính chung cho câu trả lời để có thể sử dụng một lớp đại diện cho bất kỳ loại câu hỏi kiểm tra nào, dù câu trả lời là String, Boolean, Int hay bất kỳ loại dữ liệu nào.

Tái cấu trúc mã của bạn để sử dụng các thành phần chung

Tái cấu trúc mã của bạn để sử dụng một lớp có tên là Question có thuộc tính câu trả lời chung.

  1. Loại bỏ các định nghĩa lớp đối với FillInTheBlankQuestion, TrueOrFalseQuestionNumericQuestion.
  2. Tạo một lớp mới có tên là Question.
class Question()
  1. Sau tên lớp, nhưng trước dấu ngoặc đơn, hãy thêm tham số loại chung bằng dấu ngoặc nhọn trái và phải. Gọi loại chung T.
class Question<T>()
  1. Vui lòng thêm thuộc tính questionTextanswerdifficulty questionText phải thuộc loại String. answer phải thuộc loại T vì loại dữ liệu của nó được chỉ định khi tạo bản sao cho lớp Question. Thuộc tính difficulty phải thuộc loại String.
class Question<T>(
    val questionText: String,
    val answer: T,
    val difficulty: String
)
  1. Để xem cách hoạt động của thuộc tính này đối với nhiều loại câu hỏi, chẳng hạn như điền vào chỗ trống hoặc chọn đúng sai, hãy tạo 3 bản sao của lớp Question trong main() như bên dưới.
fun main() {
    val question1 = Question<String>("Quoth the raven ___", "nevermore", "medium")
    val question2 = Question<Boolean>("The sky is green. True or false", false, "easy")
    val question3 = Question<Int>("How many days are there between full moons?", 28, "hard")
}
  1. Chạy mã của bạn để đảm bảo mọi thứ hoạt động. Bạn hiện đã có 3 bản sao của lớp Question – mỗi phiên bản có các loại dữ liệu khác nhau cho câu trả lời – thay vì 3 lớp khác nhau hoặc thay vì sử dụng tính kế thừa. Nếu muốn xử lý các câu hỏi có loại câu trả lời khác, bạn có thể sử dụng lại cùng một lớp Question.

3. Sử dụng một lớp enum

Ở phần trước, bạn đã xác định một thuộc tính về độ khó với ba giá trị có thể là: "easy", "medium" và "hard" ("dễ", "trung bình" và "khó"). Ở phần này sẽ có một vài bất cập.

  1. Nếu vô tình nhập sai một trong ba chuỗi này, bạn có thể gây ra lỗi.
  2. Nếu giá trị thay đổi, chẳng hạn như "medium" được đổi tên thành "average", thì bạn cần phải cập nhật tất cả các cách sử dụng chuỗi.
  3. Việc vô tình sử dụng một chuỗi khác không thuộc một trong ba giá trị hợp lệ là điều mà bạn hoặc các nhà phát triển khác có thể gặp phải.
  4. Mã sẽ khó duy trì hơn nếu bạn thêm nhiều cấp độ khó khác.

Kotlin sẽ giúp bạn giải quyết các vấn đề này bằng một loại lớp đặc biệt có tên là lớp enum. Lớp enum được dùng để tạo các loại có một tập hợp các giá trị có thể bị giới hạn. Chẳng hạn như trong thực tế, bốn hướng chính là bắc, nam, đông và tây có thể được biểu thị bằng một lớp enum. Mã không cần thiết và cũng không được cho phép việc sử dụng bất kỳ chỉ dẫn bổ sung nào. Dưới đây là cú pháp cho một lớp enum.

2046d73e89bd8167.png

Mỗi giá trị có thể của một enum được gọi là hằng số enum. Hằng số enum được đặt bên trong hàm dựng và phân tách bằng dấu phẩy. Tên hằng số phải được viết hoa mọi chữ cái.

Bạn phải tham chiếu đến các hằng số enum bằng toán tử dấu chấm.

d6f72b0c1f3218df.png

Sử dụng hằng số enum

Sửa đổi mã của bạn để sử dụng hằng số enum thay vì String để biểu thị độ khó.

  1. Bên dưới lớp Question, hãy xác định một lớp enum có tên là Difficulty.
enum class Difficulty {
    EASY, MEDIUM, HARD
}
  1. Trong lớp Question, hãy thay đổi loại dữ liệu của thuộc tính difficulty từ String thành Difficulty.
class Question<T>(
    val questionText: String,
    val answer: T,
    val difficulty: Difficulty
)
  1. Khi tạo ba câu hỏi, hãy truyền cấp độ khó vào hằng số enum.
val question1 = Question<String>("Quoth the raven ___", "nevermore", Difficulty.MEDIUM)
val question2 = Question<Boolean>("The sky is green. True or false", false, Difficulty.EASY)
val question3 = Question<Int>("How many days are there between full moons?", 28, Difficulty.HARD)

4. Sử dụng một lớp dữ liệu

Nhiều lớp mà bạn từng sử dụng, chẳng hạn như các lớp con của Activity, chứa nhiều phương thức để thực hiện các hành động khác nhau. Các lớp này không chỉ thể hiện dữ liệu mà còn chứa nhiều chức năng.

Trong khi đó, các lớp như lớp Question chỉ chứa dữ liệu. Các lớp này không chứa phương thức để thực hiện một hành động. Chúng có thể được xác định là một lớp dữ liệu. Việc xác định một lớp làm lớp dữ liệu cho phép trình biên dịch Kotlin thực hiện các giả định nhất định và tự động triển khai một số phương thức. Chẳng hạn như hàm toString() được gọi phía sau bởi println(). Khi bạn sử dụng một lớp dữ liệu, toString() và các phương thức khác sẽ tự động được triển khai dựa trên các thuộc tính của lớp đó.

Để xác định một lớp dữ liệu, bạn chỉ cần thêm từ khóa data trước từ khóa class.

4f6effa88d56a850.png

Chuyển đổi Question thành lớp dữ liệu

Đầu tiên, bạn sẽ thấy điều gì xảy ra khi bạn cố gọi một phương thức như toString() trên một lớp không phải là lớp dữ liệu. Sau đó, bạn sẽ chuyển đổi Question thành một lớp dữ liệu để triển khai phương thức này và các phương thức khác theo mặc định.

  1. Trong main(), hãy in kết quả gọi toString() trên question1.
fun main() {
    val question1 = Question<String>("Quoth the raven ___", "nevermore", Difficulty.MEDIUM)
    val question2 = Question<Boolean>("The sky is green. True or false", false, Difficulty.EASY)
    val question3 = Question<Int>("How many days are there between full moons?", 28, Difficulty.HARD)
    println(question1.toString())
}
  1. Chạy mã. Kết quả chỉ hiển thị tên lớp và giá trị nhận dạng duy nhất của đối tượng.
Question@37f8bb67
  1. Đặt Question vào một lớp dữ liệu bằng từ khóa data.
data class Question<T>(
    val questionText: String,
    val answer: T,
    val difficulty: Difficulty
)
  1. Chạy lại mã. Bằng cách đánh dấu lớp này dưới dạng một lớp dữ liệu, Kotlin có thể xác định cách hiển thị các thuộc tính của lớp khi gọi toString().
Question(questionText=Quoth the raven ___, answer=nevermore, difficulty=MEDIUM)

Khi lớp được xác định là lớp dữ liệu, các phương thức dưới đây sẽ được triển khai.

  • equals()
  • hashCode(): bạn sẽ thấy phương thức này khi làm việc với một số loại bộ sưu tập nhất định.
  • toString()
  • componentN(): component1(), component2(), v.v.
  • copy()

5. Sử dụng đối tượng singleton

Trong nhiều trường hợp mà bạn muốn một lớp chỉ có một bản sao. Ví dụ như:

  1. Số liệu thống kê người chơi trong một trò chơi trên thiết bị di động của người dùng hiện tại.
  2. Tương tác với một thiết bị phần cứng, chẳng hạn như gửi âm thanh qua loa.
  3. Đối tượng để truy cập vào một nguồn dữ liệu từ xa (chẳng hạn như cơ sở dữ liệu Firebase).
  4. Xác thực việc chỉ một người dùng có thể đăng nhập tại cùng một thời điểm.

Trong các trường hợp nêu trên, có thể bạn chỉ cần sử dụng một lớp. Tuy nhiên, bạn cũng có thể tạo chỉ một bản sao của lớp đó. Nếu chỉ có một thiết bị phần cứng hoặc chỉ có một người dùng đăng nhập cùng lúc, thì sẽ không có lý do gì để tạo nhiều hơn một phiên bản. Việc có hai đối tượng truy cập cùng một thiết bị phần cứng có thể dẫn đến một số hành vi lạ và lỗi.

Bạn có thể truyền đạt rõ trong mã của mình về việc một đối tượng chỉ được có một bản sao bằng cách xác định đối tượng đó là một singleton. Singleton là lớp chỉ có thể chứa một bản sao. Kotlin cung cấp một cấu trúc đặc biệt được gọi là đối tượng, dùng để tạo lớp singleton.

Xác định đối tượng singleton

81e0355283d36761.png

Cú pháp của một đối tượng cũng tương tự như cú pháp của một lớp. Bạn chỉ cần dùng từ khóa object thay vì từ khóa class. Đối tượng singleton không thể chứa một hàm dựng vì bạn không thể tạo các bản sao trực tiếp. Thay vào đó, tất cả các thuộc tính đều được xác định trong dấu ngoặc nhọn và được cấp một giá trị ban đầu.

Một số ví dụ đã nêu trước đó có thể không rõ ràng, đặc biệt khi bạn chưa từng làm việc với các thiết bị phần cứng cụ thể hoặc chưa xử lý qua việc xác thực trong ứng dụng của mình. Tuy nhiên, bạn sẽ thấy các đối tượng singleton xuất hiện khi tiếp tục tìm hiểu quá trình phát triển Android. Hãy xem một ví dụ đơn giản thực tế bằng cách sử dụng đối tượng cho trạng thái của người dùng, trong đó chỉ cần một bản sao.

Đối với bài kiểm tra, sẽ thật tuyệt nếu bạn có thể theo dõi tổng số câu hỏi và số câu hỏi mà học viên đã trả lời từ trước đến nay. Bạn chỉ cần một bản sao của lớp này, vì vậy, thay vì khai báo dưới dạng lớp, hãy khai báo lớp này dưới dạng một đối tượng singleton.

  1. Tạo một đối tượng có tên StudentProgress.
object StudentProgress {
}
  1. Ở ví dụ này, chúng ta sẽ giả định có tổng cộng 10 câu hỏi, và 3 trong số đó đã được trả lời cho đến nay. Thêm hai thuộc tính Int: total với giá trị 10, và answered với giá trị 3.
object StudentProgress {
    var total: Int = 10
    var answered: Int = 3
}

Truy cập vào một đối tượng singleton

Bạn có nhớ là mình không thể trực tiếp tạo một bản sao của đối tượng singleton không? Rồi làm thế nào bạn có thể truy cập vào các thuộc tính sau đó?

Vì cùng một thời điểm, chỉ một phiên bản của Progress tồn tại, nên bạn có thể truy cập vào thuộc tính bằng cách tham chiếu đến tên của chính đối tượng đó, theo sau là toán tử dấu chấm (.), kế đến là tên thuộc tính.

2ed33b669a8d055c.png

Hãy cập nhật hàm main() để truy cập vào các thuộc tính của đối tượng singleton.

  1. Trong main(), hãy thêm một lệnh gọi đến println() để xuất câu hỏi answeredtotal từ đối tượng StudentProgress.
fun main() {
    ...
    println("${StudentProgress.answered} of ${StudentProgress.total} answered.")
}
  1. Chạy mã để xác minh mọi thứ đều hoạt động.
...
3 of 10 answered

Khai báo đối tượng dưới dạng đối tượng đồng hành

Các lớp và đối tượng trong Kotlin có thể được xác định trong các loại khác. Đây cũng là một cách hay để sắp xếp mã của bạn. Bạn có thể xác định đối tượng singleton bên trong một lớp khác bằng đối tượng đồng hành. Đối tượng đồng hành cho phép bạn truy cập vào các thuộc tính và phương thức từ bên trong lớp, nếu các thuộc tính và phương thức của đối tượng thuộc về lớp đó, giúp việc sử dụng cú pháp ngắn gọn hơn.

Để khai báo đối tượng đồng hành, bạn chỉ cần thêm từ khóa companion trước object.

e65d858bb7b607c4.png

Bạn sẽ tạo một lớp mới có tên là Quiz để lưu trữ câu hỏi của bài kiểm tra và đặt StudentProgress làm đối tượng đồng hành của lớp Quiz.

  1. Bên dưới lớp enum Difficulty, hãy xác định một lớp mới có tên là Quiz.
class Quiz {
}
  1. Di chuyển question1, question2question3 từ main() vào lớp Quiz. Bạn cũng cần phải xóa println(question1.toString()) nếu chưa xóa.
class Quiz {
    val question1 = Question<String>("Quoth the raven ___", "nevermore", Difficulty.MEDIUM)
    val question2 = Question<Boolean>("The sky is green. True or false", false, Difficulty.EASY)
    val question3 = Question<Int>("How many days are there between full moons?", 28, Difficulty.HARD)

}
  1. Di chuyển đối tượng StudentProgress vào lớp Quiz.
class Quiz {
    val question1 = Question<String>("Quoth the raven ___", "nevermore", Difficulty.MEDIUM)
    val question2 = Question<Boolean>("The sky is green. True or false", false, Difficulty.EASY)
    val question3 = Question<Int>("How many days are there between full moons?", 28, Difficulty.HARD)

    object StudentProgress {
        var total: Int = 10
        var answered: Int = 3
    }
}
  1. Đánh dấu đối tượng StudentProgress bằng từ khóa companion.
companion object StudentProgress {
    var total: Int = 10
    var answered: Int = 3
}
  1. Cập nhật lệnh gọi thành println() để tham chiếu đến các thuộc tính có Quiz.answeredQuiz.total. Mặc dù những thuộc tính này được khai báo trong đối tượng StudentProgress, nhưng chúng vẫn có thể truy cập được bằng ký hiệu dấu chấm chỉ cần tên của lớp Quiz.
fun main() {
    println("${Quiz.answered} of ${Quiz.total} answered.")
}
  1. Chạy mã của bạn để xác minh kết quả.
3 of 10

6. Mở rộng lớp bằng các thuộc tính và phương thức mới

Khi làm việc với tính năng Compose, bạn có thể nhận thấy một số cú pháp thú vị khi chỉ định kích thước của các thành phần trên giao diện người dùng. Có những loại số chẳng hạn như Double có các thuộc tính dpsp chỉ định kích thước.

eb15d8b633c2b813.png

Tại sao các nhà thiết kế của ngôn ngữ Kotlin lại sử dụng thuộc tính và hàm trên các loại dữ liệu tích hợp sẵn, đặc biệt là khi xây dựng giao diện cho người dùng Android? Họ có thể dự đoán tương lai chăng? Kotlin đã được thiết kế để dùng với Compose ngay cả trước khi tính năng này tồn tại sao?

Tất nhiên là không! Khi viết một lớp, bạn thường không biết chính xác cách nhà phát triển khác sẽ sử dụng hoặc dự định sử dụng lớp đó trong ứng dụng của họ. Bạn không thể dự đoán tất cả các trường hợp sử dụng sau này, cũng như không nên thêm phần không cần thiết vào mã đối với một số trường hợp sử dụng ngoài dự kiến.

Ngôn ngữ Kotlin có chức năng cung cấp cho các nhà phát triển khác khả năng mở rộng những loại dữ liệu hiện có, thêm các thuộc tính và phương thức có thể truy cập được bằng cú pháp dấu chấm, dưới dạng một phần của loại dữ liệu đó. Một nhà phát triển không làm việc với các loại dấu phẩy động trong Kotlin, chẳng hạn như ai đó đang tạo thư viện Compose, có thể chọn thêm các thuộc tính và phương thức dành riêng cho kích thước của Giao diện người dùng.

Bạn đã thấy cú pháp này trong 2 học phần đầu tiên với nội dung Compose, nên bây giờ bạn sẽ tìm hiểu cách hoạt động của tính năng này. Bạn hãy thêm một số thuộc tính và phương thức để mở rộng các loại hiện có.

Thêm một thuộc tính tiện ích

Để xác định một thuộc tính mở rộng, vui lòng thêm tên loại và một toán tử dấu chấm (.) vào trước tên biến.

e4ad1e0f91ac8583.png

Bạn sẽ tái cấu trúc mã chính để in tiến trình làm bài kiểm tra thành thuộc tính tiện ích.

  1. Bên dưới lớp Quiz, hãy xác định một thuộc tính của phần mở rộng Quiz.StudentProgress có tên là progressText thuộc loại String.
val Quiz.StudentProgress.progressText: String
  1. Xác định phương thức getter cho thuộc tính mở rộng có thể trả về cùng một chuỗi được dùng trước đó trong main().
val Quiz.StudentProgress.progressText: String
    get() = "${answered} of ${total} answered"
  1. Thay thế mã trong main() bằng mã in progressText. Vì đây là thuộc tính mở rộng của đối tượng đồng hành nên bạn có thể truy cập vào phần tử đó bằng ký hiệu dấu chấm sử dụng tên lớp Quiz.
fun main() {
    println(Quiz.progressText)
}
  1. Hãy chạy mã của bạn để xác minh mã đó hoạt động.
3 of 10 answered

Thêm một hàm mở rộng

Để xác định một hàm mở rộng, vui lòng thêm tên loại và một toán tử dấu chấm (.) vào trước tên hàm.

495b8e34d337ec73.png

Bạn phải thêm một hàm mở rộng để hiển thị tiến độ bài kiểm tra dưới dạng thanh tiến trình. Vì bạn không thể tạo thanh tiến trình trong Kotlin playground, nên bạn sẽ in thanh tiến trình kiểu cổ điển bằng văn bản!

  1. Thêm một hàm mở rộng vào đối tượng StudentProgress có tên là printProgressBar(). Hàm không được nhận tham số và không có giá trị trả về.
fun Quiz.StudentProgress.printProgressBar() {
}
  1. Sử dụng hàm repeat() để in ký tự và số lần answered. Phần tô đậm này của thanh tiến trình thể hiện số câu hỏi được trả lời. Vui lòng sử dụng print() vì bạn không muốn thêm một dòng mới sau mỗi ký tự.
fun Quiz.StudentProgress.printProgressBar() {
    repeat(Quiz.answered) { print("▓") }
}
  1. Sử dụng hàm repeat() để in ký tự , số lần bằng số chênh lệch giữa totalanswered. Phần được tô bóng này đại diện cho các câu hỏi còn lại trong thanh quy trình.
fun Quiz.StudentProgress.printProgressBar() {
    repeat(Quiz.answered) { print("▓") }
    repeat(Quiz.total - Quiz.answered) { print("▒") }
}
  1. In một dòng mới bằng cách sử dụng println() không có đối số, sau đó in progressText.
fun Quiz.StudentProgress.printProgressBar() {
    repeat(Quiz.answered) { print("▓") }
    repeat(Quiz.total - Quiz.answered) { print("▒") }
    println()
    println(Quiz.progressText)
}
  1. Hãy cập nhật mã trong main() để gọi hàm printProgressBar().
fun main() {
    Quiz.printProgressBar()
}
  1. Chạy mã của bạn để xác minh kết quả.
▓▓▓▒▒▒▒▒▒▒
3 of 10 answered

Tôi có nhất thiết phải thực hiện các thao tác này không? Chắc chắn là không. Tuy nhiên, việc sở hữu thuộc tính và phương thức mở rộng cung cấp cho bạn nhiều tùy chọn hơn để hiển thị mã của bạn cho các nhà phát triển khác. Việc sử dụng cú pháp dấu chấm trên các loại khác có thể giúp mã của bạn dễ đọc hơn, cho cả bạn và các nhà phát triển khác.

7. Dùng hàm phạm vi để truy cập vào các phương thức và thuộc tính của lớp

Như bạn đã thấy, Kotlin có nhiều tính năng giúp mã trở nên ngắn gọn hơn.

Một trong những tính năng đó mà bạn sẽ gặp khi tiếp tục tìm hiểu quá trình phát triển Android là hàm phạm vi. Hàm phạm vi cho phép bạn truy cập ngắn gọn vào các thuộc tính và phương thức từ một lớp mà không phải liên tục truy cập vào tên biến. Chính xác thì điều này có nghĩa là gì? Hãy cùng tham khảo ví dụ dưới đây:

Loại bỏ các tham chiếu đối tượng lặp lại bằng hàm phạm vi

Hàm phạm vi là các hàm bậc cao hơn cho phép bạn truy cập vào thuộc tính và phương thức của một đối tượng mà không cần tham chiếu đến tên của đối tượng. Hàm có tên là hàm phạm vi vì phần nội dung hàm được truyền vào sẽ nhận phạm vi của đối tượng được gọi. Chẳng hạn như một số hàm phạm vi cho phép bạn truy cập vào các thuộc tính và phương thức trong một lớp, như thể các hàm được xác định là một phương thức của lớp đó. Nhờ vậy, mã của bạn sẽ dễ đọc hơn vì nó cho phép bạn bỏ qua tên đối tượng khi có thêm mã thừa.

Để minh họa rõ hơn về điều này, hãy xem xét một vài hàm phạm vi khác nhau mà bạn sẽ gặp sau này trong khóa học.

Thay thế tên đối tượng dài bằng let()

Hàm let() cho phép bạn tham chiếu đến một đối tượng trong biểu thức lambda bằng giá trị nhận dạng it, thay vì tên thực tế của đối tượng. Điều này giúp bạn tránh việc sử dụng tên đối tượng dài, mang tính mô tả nhiều lần khi truy cập vào nhiều thuộc tính. Hàm let() là một hàm mở rộng có thể được gọi trên bất kỳ đối tượng Kotlin nào bằng ký hiệu dấu chấm.

Hãy thử truy cập vào các thuộc tính của question1, question2question3 bằng let():

  1. Thêm một hàm vào lớp Quiz có tên là printQuiz().
fun printQuiz() {

}
  1. Thêm mã dưới đây để in questionText, answerdifficulty của câu hỏi. Mặc dù nhiều thuộc tính được truy cập cho question1, question2question3, nhưng toàn bộ tên biến đều được sử dụng mỗi lần. Nếu tên biến thay đổi, bạn sẽ cần cập nhật tất cả các cách sử dụng.
fun printQuiz() {
    println(question1.questionText)
    println(question1.answer)
    println(question1.difficulty)
    println()
    println(question2.questionText)
    println(question2.answer)
    println(question2.difficulty)
    println()
    println(question3.questionText)
    println(question3.answer)
    println(question3.difficulty)
    println()
}
  1. Bao quanh mã truy cập vào các thuộc tính questionText, answerdifficulty bằng một lệnh gọi hàm let() trên question1, question2question3. Thay thế tên biến trong mỗi biểu thức lambda bằng hàm đó.
fun printQuiz() {
    question1.let {
        println(it.questionText)
        println(it.answer)
        println(it.difficulty)
    }
    println()
    question2.let {
        println(it.questionText)
        println(it.answer)
        println(it.difficulty)
    }
    println()
    question3.let {
        println(it.questionText)
        println(it.answer)
        println(it.difficulty)
    }
    println()
}
  1. Tạo một bản sao của lớp Quiz có tên là quiz.
fun main() {
    Quiz.printProgressBar()
    val quiz = Quiz()
}
  1. Gọi printQuiz().
fun main() {
    Quiz.printProgressBar()
    val quiz = Quiz()
    quiz.printQuiz()
}
  1. Chạy mã để xác minh mọi thứ đều hoạt động.
Quoth the raven ___
nevermore
MEDIUM

Quoth the raven ___
nevermore
MEDIUM

Quoth the raven ___
nevermore
MEDIUM

Gọi các phương thức của một đối tượng mà không cần biến sử dụng hàm apply()

Một trong các tính năng thú vị của hàm phạm vi là bạn có thể gọi các hàm này trên một đối tượng trước khi đối tượng đó thậm chí được chỉ định cho một biến. Ví dụ như hàm apply() là hàm mở rộng có thể được gọi trên một đối tượng bằng ký hiệu dấu chấm. Hàm apply() cũng trả về một tham chiếu đến đối tượng đó để có thể lưu trữ trong một biến.

Hãy cập nhật mã trong main() để gọi hàm apply().

  1. Gọi apply() sau dấu ngoặc đơn đóng khi tạo một bản sao của lớp Quiz. Bạn có thể bỏ qua dấu ngoặc đơn khi gọi apply() và sử dụng cú pháp trailing lambda.
val quiz = Quiz().apply {
}
  1. Di chuyển lệnh gọi đến printQuiz() bên trong biểu thức lambda. Bạn không cần tham chiếu đến biến quiz hay sử dụng ký hiệu dấu chấm nữa.
val quiz = Quiz().apply {
    printQuiz()
}
  1. Hàm apply() trả về bản sao của lớp Quiz. Tuy nhiên, vì bạn không sử dụng hàm này nữa nên hãy xóa biến quiz. Với hàm apply(), bạn thậm chí không cần biến để gọi các phương thức trên bản sao của Quiz.
Quiz().apply {
    printQuiz()
}
  1. Chạy mã. Vui lòng lưu ý là bạn đã có thể gọi phương thức này mà không cần tham chiếu đến bản sao của Quiz. Hàm apply() trả về các đối tượng đã được lưu trữ trong quiz.
▓▓▓▒▒▒▒▒▒▒
3 of 10 answered.
kotlin.Unit
Quoth the raven ___
nevermore
MEDIUM

Quoth the raven ___
nevermore
MEDIUM

Quoth the raven ___
nevermore
MEDIUM

Mặc dù bạn không nhất thiết phải dùng các hàm phạm vi để đạt được kết quả như mong muốn, nhưng những ví dụ trên cho thấy cách nó giúp mã của bạn ngắn gọn hơn và tránh lặp lại cùng một tên biến.

Đoạn mã trên chỉ đưa ra hai ví dụ, nhưng bạn nên đánh dấu trang và tham khảo tài liệu về Hàm phạm vi khi gặp lại cách sử dụng chúng ở khóa học sau này.

8. Tóm tắt

Bạn vừa có cơ hội xem một số tính năng mới đang hoạt động trong Kotlin. Thành phần chung cho phép các loại dữ liệu được truyền dưới dạng tham số vào các lớp, lớp enum xác định một tập hợp giới hạn các giá trị có thể, còn các lớp dữ liệu giúp tự động tạo một số phương thức hữu ích cho các lớp.

Bạn cũng đã tìm hiểu cách tạo đối tượng singleton vốn bị giới hạn trong một bản sao, cách đặt đối tượng này làm đối tượng đồng hành của một lớp khác, và cách mở rộng các lớp hiện có bằng những thuộc tính mới chỉ nhận và phương thức mới. Sau cùng, bạn cũng đã thấy một số ví dụ về cách hàm phạm vi cung cấp cú pháp đơn giản hơn khi truy cập vào các thuộc tính và phương thức.

Bạn sẽ gặp lại các khái niệm này ở những học phần sau khi tìm hiểu thêm về Kotlin, quá trình phát triển Android và tính năng Compose. Giờ thì bạn đã hiểu rõ hơn về cách hoạt động của các hàm này cũng như cách nó giúp cải thiện khả năng tái sử dụng và khả năng đọc mã của bạn.

9. Tìm hiểu thêm