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 hoá 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 sai lầm. 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:

  • Loại chung
  • Các loại lớp khác (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

Kết thúc lớp học lập trình này, bạn sẽ hiểu rõ hơn về mã đã thấy ở đây, đồ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 thực thể 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 lớp học lập trình 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ư câu hỏi chọn đúng 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. Điểm xếp hạng độ khó được biểu thị bằng một chuỗi có thể có 3 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 answerString 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 chọn đúng sai, bao gồm thuộc tính String cho questionText, Boolean cho answerString cho difficulty.
class TrueOrFalseQuestion(
    val questionText: String,
    val answer: Boolean,
    val difficulty: String
)
  1. Cuối cùng, dưới 2 lớp còn lại, hãy xác định một lớp NumericQuestion, bao gồm thuộc tính String cho questionText, Int cho answerString cho 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ả 3 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. Có thể bạn cho rằng giải pháp tất yếu là tạo một lớp cha với questionTextdifficulty, đồng thời mỗi lớp con đều có thuộc tính answer.

Tuy nhiên, việc sử dụng cơ chế 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à kiểu chung, cho phép một thuộc tính có thể chứa các loại dữ liệu khác nhau, tuỳ thuộc vào trường hợp sử dụng cụ thể.

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

Kiểu 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 loại 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 loại dữ liệu đó. Chính xác thì điều này có nghĩa là gì?

Ở ví dụ trên, thay vì định nghĩa thuộc tính answer (đáp án) cụ thể cho mỗi loại dữ liệu, bạn có thể tạo một lớp duy nhất để biểu thị bất kỳ câu hỏi nào, đồng thời sử dụng tên phần giữ chỗ cho loại dữ liệu có thuộc tính answer. Bạn có thể chỉ định kiểu dữ liệu String, Int, Boolean, v.v. khi tạo thực thể cho lớp đó. Bất cứ nơi nào tên phần 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:

67367d9308c171da.png

Loại dữ liệu chung được cung cấp khi bạn tạo thực thể cho một lớp, do đó, loại này 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 góc hướng sang trái (<), theo sau là tên phần giữ chỗ cho kiểu dữ liệu, kế đến là dấu ngoặc góc hướng sang phải (>).

Sau này, bạn có thể dùng tên phần 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.

81170899b2ca0dc9.png

Việc này cũng giống với mọi thao tác khai báo thuộc tính khác, ngoại trừ điểm bạn dùng tên phần giữ chỗ 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 mà loại chung dùng sẽ được truyền ở dạng tham số trong dấu ngoặc góc khi bạn tạo thực thể cho lớp.

9b8fce54cac8d1ea.png

Sau tên lớp là dấu ngoặc góc hướng sang trái (<), theo sau là loại dữ liệu thực, String, Boolean, Int, v.v., tiếp đến là dấu ngoặc góc hướng sang 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 góc. 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 thuộc tính này được chỉ định khi tạo thực thể 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 thực thể của lớp Question trong main(), như minh hoạ dưới đây.
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 thực thể của lớp Question (mỗi thực thể có các loại dữ liệu riêng cho câu trả lời) thay vì 3 lớp riêng biệt hoặc thay vì sử dụng cơ chế 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 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 3 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ế, 4 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.

f4bddb215eb52392.png

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

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

f3cfa84c3f34392b.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 những 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ừ khoá data trước từ khoá class.

e7cd946b4ad216f4.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 đoạn mã. Kết quả chỉ cho thấy 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 là 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ụ:

  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 mỗi lúc, thì sẽ không có lý do gì để tạo nhiều hơn một thực thể. 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 (object), dùng để tạo lớp singleton.

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

645e8e8bbffbb5f9.png

Cú pháp của một đối tượng tương tự như cú pháp của một lớp. Bạn chỉ cần dùng từ khoá object thay vì từ khoá 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 thực thể trực tiếp. Thay vào đó, mọi thuộc tính đều được định nghĩa 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 trê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 thực thể.

Đố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ừ đầu đến giờ. Bạn chỉ cần một thực thể 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 2 thuộc tính Int: total với giá trị 10answered 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ì vào mỗi thời điểm, chỉ một phiên bản của StudentProgress tồn tại, nên bạn có thể truy cập vào các thuộc tính của phiên bản đó 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.

1b610fd87e99fe25.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ừ khoá companion trước từ khoá object.

68b263904ec55f29.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 xoá println(question1.toString()) nếu chưa xoá.
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ừ khoá 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 answered.

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. Những loại số (ví dụ: Double) dường như có các thuộc tính dpsp chỉ định kích thước.

a25c5a0d7bb92b60.png

Tại sao các nhà thiết kế của ngôn ngữ Kotlin lại 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ả 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 tiện ích, hãy thêm tên loại và toán tử dấu chấm (.) trước tên biến.

1e8a52e327fe3f45.png

Bạn sẽ tái cấu trúc mã trong hàm main() để in tiến trình bài kiểm tra bằng một 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 tiện ích 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 tiện ích 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 hàm main() bằng mã có thể in progressText. Vì đây là thuộc tính tiện ích của đối tượng đồng hành nên bạn có thể truy cập vào thuộc tính đó bằng ký hiệu dấu chấm cù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 tiện ích, hãy thêm tên loại và toán tử dấu chấm (.) trước tên hàm.

879ff2761e04edd9.png

Bạn phải thêm một hàm tiện ích để hiện tiến trình 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ẽ hiển thị thanh tiến trình trên màn hì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. Hãy 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 tuỳ 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 giao diện để viết lại hàm tiện ích

Ở trang trước, bạn đã biết cách dùng các thuộc tính tiện ích và hàm tiện ích để thêm thuộc tính và phương thức vào đối tượng StudentProgress mà không cần trực tiếp thêm mã vào đối tượng đó. Tuy đây là cách hay để thêm chức năng vào một lớp đã xác định, nhưng không phải lúc nào cũng cần mở rộng lớp nếu bạn có quyền truy cập vào mã nguồn. Cũng có trường hợp bạn không biết nên triển khai gì, chỉ cần có một phương thức hoặc thuộc tính nhất định. Nếu cần nhiều lớp có cùng các thuộc tính và phương thức bổ sung, mà có thể với hành vi khác nhau, bạn có thể xác định các thuộc tính và phương thức này bằng một giao diện.

Ví dụ: ngoài các bài kiểm tra, giả sử bạn cần khảo sát các lớp học, các bước trong một công thức hoặc một dữ liệu có thứ tự nào khác có thể sử dụng thanh tiến trình. Bạn có thể định nghĩa một đối tượng được gọi là giao diện, đối tượng đó sẽ xác định các phương thức và/hoặc thuộc tính mà từng lớp trong số này phải cung cấp.

eeed58ed687897be.png

Một giao diện được xác định bằng từ khoá interface, theo sau là tên dạng UpperCamelCase (các chữ viết liền với chữ cái đầu viết hoa), kế đó là dấu mở và đóng ngoặc nhọn. Trong dấu ngoặc nhọn, bạn có thể xác định mọi chữ ký phương thức hoặc thuộc tính chỉ nhận mà bất cứ lớp nào tuân theo giao diện này cũng phải triển khai.

6b04a8f50b11f2eb.png

Mỗi giao diện là một hợp đồng. Một lớp tuân theo một giao diện thì được xem là mở rộng cho giao diện đó. Một lớp có thể khai báo rằng lớp này muốn mở rộng giao diện bằng cách dùng dấu hai chấm (:), theo sau là dấu cách, tiếp đó là tên giao diện.

78af59840c74fa08.png

Đổi lại, lớp này phải triển khai tất cả các thuộc tính và phương thức được chỉ định trong giao diện. Điều này cho phép bạn dễ dàng đảm bảo rằng mọi lớp mà giao diện cần để mở rộng đều triển khai các phương thức giống hệt nhau, có cùng chữ ký phương thức. Nếu sửa đổi giao diện theo bất cứ cách nào (ví dụ: thêm hoặc xoá thuộc tính hoặc phương thức hoặc thay đổi chữ ký phương thức), thì trình biên dịch sẽ yêu cầu bạn cập nhật mọi lớp có mở rộng giao diện, nhằm đảm bảo các đoạn mã của bạn được nhất quán và dễ duy trì hơn.

Giao diện cho phép thay đổi hành vi của những lớp có mở rộng giao diện đó. Tuỳ theo từng lớp mà bạn có thể cung cấp phương thức triển khai.

Hãy xem cách bạn có thể viết lại thanh tiến trình để sử dụng giao diện, đồng thời làm cho lớp Quiz (Bài kiểm tra) mở rộng giao diện đó.

  1. Phía trên lớp Quiz, hãy xác định một giao diện tên là ProgressPrintable. Chúng ta chọn tên ProgressPrintable, vì tên này giúp bất cứ lớp nào mở rộng giao diện đó cũng có thể in thanh tiến trình.
interface ProgressPrintable {
}
  1. Trong giao diện ProgressPrintable, hãy xác định một thuộc tính tên là progressText.
interface ProgressPrintable {
    val progressText: String
}
  1. Sửa đổi nội dung khai báo của lớp Quiz để mở rộng giao diện ProgressPrintable.
class Quiz : ProgressPrintable {
    ...
}
  1. Trong lớp Quiz, hãy thêm thuộc tính có tên progressText thuộc loại String, như chỉ định trong giao diện ProgressPrintable. Vì thuộc tính đó đến từ ProgressPrintable, nên đứng trước val và có từ khoá override (ghi đè).
override val progressText: String
  1. Sao chép phương thức getter của thuộc tính đó từ thuộc tính mở rộng progressText cũ.
override val progressText: String
        get() = "${answered} of ${total} answered"
  1. Xoá thuộc tính mở rộng progressText cũ.

Đoạn mã cần xoá:

val Quiz.StudentProgress.progressText: String
    get() = "${answered} of ${total} answered"
  1. Trong giao diện ProgressPrintable, hãy thêm một phương thức tên là printProgressBar. Phương thức này không nhận tham số và không có giá trị trả về.
interface ProgressPrintable {
    val progressText: String
    fun printProgressBar()
}
  1. Trong lớp Quiz, hãy thêm phương thức printProgressBar() bằng từ khoá override.
override fun printProgressBar() {
}
  1. Di chuyển mã từ hàm mở rộng printProgressBar() cũ vào printProgressBar() mới từ giao diện. Sửa đổi dòng cuối cùng để tham chiếu đến biến progressText mới từ giao diện bằng cách loại bỏ tham chiếu đến Quiz.
override fun printProgressBar() {
    repeat(Quiz.answered) { print("▓") }
    repeat(Quiz.total - Quiz.answered) { print("▒") }
    println()
    println(progressText)
}
  1. Xoá hàm tiện ích printProgressBar(). Hiện tại, chức năng này nay thuộc về lớp Quiz có thể mở rộng ProgressPrintable.

Đoạn mã cần xoá:

fun Quiz.StudentProgress.printProgressBar() {
    repeat(Quiz.answered) { print("▓") }
    repeat(Quiz.total - Quiz.answered) { print("▒") }
    println()
    println(Quiz.progressText)
}
  1. Cập nhật mã trong main(). Do hàm printProgressBar() hiện là một phương thức của lớp Quiz, nên trước tiên bạn cần tạo thực thể cho đối tượng Quiz rồi gọi printProgressBar().
fun main() {
    Quiz().printProgressBar()
}
  1. Chạy đoạn mã. Kết quả xuất ra không thay đổi, nhưng đoạn mã của bạn nay có tính mô-đun cao hơn. Khi cơ sở mã của bạn tăng lên, bạn có thể dễ dàng thêm các lớp tuân theo cùng một giao diện để sử dụng lại đoạn mã mà không cần kế thừa từ lớp cấp cao.
▓▓▓▒▒▒▒▒▒▒
3 of 10 answered.

Có nhiều trường hợp sử dụng giao diện để giúp lập cấu trúc cho đoạn mã, ngoài ra bạn cũng bắt đầu thường xuyên bắt gặp các trường hợp sử dụng này trong các đơn vị thông dụng. Sau đây là ví dụ về một số giao diện mà bạn có thể bắt gặp trong quá trình xử lý Kotlin.

  • Chèn phần phụ thuộc theo cách thủ công. Tạo một giao diện xác định tất cả thuộc tính và phương thức của phần phụ thuộc. Hãy yêu cầu giao diện đóng vai trò kiểu dữ liệu của phần phụ thuộc (hoạt động, trường hợp kiểm thử, v.v.) để có thể sử dụng một thực thể của lớp triển khai giao diện bất kỳ. Nhờ đó, bạn có thể hoán đổi các phương thức triển khai cơ bản.
  • Mô phỏng các bài thử nghiệm tự động. Cả lớp mô phỏng và lớp thực đều tuân theo cùng một giao diện.
  • Truy cập các phần phụ thuộc tương tự trong ứng dụng Compose Multiplatform. Ví dụ: tạo một giao diện cung cấp một tập hợp thuộc tính và phương thức phổ biến cho Android và máy tính, ngay cả khi có sự khác biệt về phương thức triển khai cơ bản tuỳ theo nền tảng.
  • Một số kiểu dữ liệu trong Compose (ví dụ: Modifier) là các giao diện. Điều này cho phép bạn thêm các đối tượng sửa đổi mới mà không cần truy cập hoặc sửa đổi mã nguồn cơ bản.

8. 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 quy trình phát triển Android là hàm phạm vi (scope function). Hàm phạm vi cho phép bạn truy cập nhanh 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 lượt 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 trong trường hợp nếu có thì bị thừa.

Để minh hoạ 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 khoá 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. Cập nhật mã trong main() để tạo một phiên bản của lớp Quiz có tên là quiz.
fun main() {
    val quiz = Quiz()
}
  1. Gọi printQuiz().
fun main() {
    val quiz = Quiz()
    quiz.printQuiz()
}
  1. Chạy mã để xác minh mọi thứ đều hoạt động.
Quoth the raven ___
nevermore
MEDIUM

The sky is green. True or false
false
EASY

How many days are there between full moons?
28
HARD

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 đã được gán 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 thực thể 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ề thực thể 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 xoá 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 đoạn 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.
Quoth the raven ___
nevermore
MEDIUM

The sky is green. True or false
false
EASY

How many days are there between full moons?
28
HARD

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ỉ minh hoạ 2 ví dụ, nhưng bạn nên đánh dấu lại và tham khảo thêm tài liệu về Hàm phạm vi khi gặp lại cách sử dụng chúng ở phần sau này trong khoá học.

9. 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. Kiểu 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 thực thể, 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 chúng giúp cải thiện khả năng tái sử dụng và khả năng đọc đoạn mã của bạn.

10. Tìm hiểu thêm