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:
- Chuyển đến phần Kotlin Playground.
- 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ênFillInTheBlankQuestion
, bao gồm thuộc tínhString
choquestionText
,String
choanswer
vàString
chodifficulty
.
class FillInTheBlankQuestion(
val questionText: String,
val answer: String,
val difficulty: String
)
- Bên dưới lớp
FillInTheBlankQuestion
, hãy xác định một lớp khác có tênTrueOrFalseQuestion
cho các câu hỏi chọn đúng sai, bao gồm thuộc tínhString
choquestionText
,Boolean
choanswer
vàString
chodifficulty
.
class TrueOrFalseQuestion(
val questionText: String,
val answer: Boolean,
val difficulty: String
)
- 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ínhString
choquestionText
,Int
choanswer
vàString
chodifficulty
.
class NumericQuestion(
val questionText: String,
val answer: Int,
val difficulty: String
)
- 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
, answer
và difficulty
. Đ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 questionText
và difficulty
, đồ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:
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.
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.
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.
- Loại bỏ các định nghĩa lớp đối với
FillInTheBlankQuestion
,TrueOrFalseQuestion
vàNumericQuestion
. - Tạo một lớp mới có tên là
Question
.
class Question()
- 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>()
- Vui lòng thêm thuộc tính
questionText
answer
vàdifficulty
questionText
phải thuộc loạiString
.answer
phải thuộc loạiT
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ớpQuestion
. Thuộc tínhdifficulty
phải thuộc loạiString
.
class Question<T>(
val questionText: String,
val answer: T,
val difficulty: String
)
- Để 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
trongmain()
, 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")
}
- 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ớpQuestion
.
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.
- 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.
- 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. - 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.
- 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.
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.
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ó.
- Bên dưới lớp
Question
, hãy xác định một lớpenum
có tên làDifficulty
.
enum class Difficulty {
EASY, MEDIUM, HARD
}
- Trong lớp
Question
, hãy thay đổi loại dữ liệu của thuộc tínhdifficulty
từString
thànhDifficulty
.
class Question<T>(
val questionText: String,
val answer: T,
val difficulty: Difficulty
)
- 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
.
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.
- Trong
main()
, hãy in kết quả gọitoString()
trênquestion1
.
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())
}
- 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
- Đặt
Question
vào một lớp dữ liệu bằng từ khóadata
.
data class Question<T>(
val questionText: String,
val answer: T,
val difficulty: Difficulty
)
- 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ụ:
- 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.
- 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.
- Đố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).
- 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
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.
- Tạo một đối tượng có tên
StudentProgress
.
object StudentProgress {
}
- Ở 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ị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ì 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.
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.
- Trong
main()
, hãy thêm một lệnh gọi đếnprintln()
để xuất câu hỏianswered
vàtotal
từ đối tượngStudentProgress
.
fun main() {
...
println("${StudentProgress.answered} of ${StudentProgress.total} answered.")
}
- 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
.
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
.
- 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 {
}
- Di chuyển
question1
,question2
vàquestion3
từmain()
vào lớpQuiz
. 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)
}
- Di chuyển đối tượng
StudentProgress
vào lớpQuiz
.
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
}
}
- Đánh dấu đối tượng
StudentProgress
bằng từ khoácompanion
.
companion object StudentProgress {
var total: Int = 10
var answered: Int = 3
}
- Cập nhật lệnh gọi thành
println()
để tham chiếu đến các thuộc tính cóQuiz.answered
vàQuiz.total
. Mặc dù những thuộc tính này được khai báo trong đối tượngStudentProgress
, 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ớpQuiz
.
fun main() {
println("${Quiz.answered} of ${Quiz.total} answered.")
}
- 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 dp
và sp
chỉ định kích thước.
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.
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.
- Bên dưới lớp
Quiz
, hãy xác định một thuộc tính tiện íchQuiz.StudentProgress
có tên làprogressText
thuộc loạiString
.
val Quiz.StudentProgress.progressText: String
- 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"
- Thay thế mã trong hàm
main()
bằng mã có thể inprogressText
. 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ớpQuiz
.
fun main() {
println(Quiz.progressText)
}
- 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.
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!
- 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() {
}
- Sử dụng hàm
repeat()
để in ký tự▓
và số lầnanswered
. 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ụngprint()
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("▓") }
}
- Sử dụng hàm
repeat()
để in ký tự▒
, số lần bằng số chênh lệch giữatotal
vàanswered
. 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("▒") }
}
- In một dòng mới bằng cách sử dụng
println()
không có đối số, sau đó inprogressText
.
fun Quiz.StudentProgress.printProgressBar() {
repeat(Quiz.answered) { print("▓") }
repeat(Quiz.total - Quiz.answered) { print("▒") }
println()
println(Quiz.progressText)
}
- Hãy cập nhật mã trong
main()
để gọi hàmprintProgressBar()
.
fun main() {
Quiz.printProgressBar()
}
- 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.
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.
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.
Đổ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 đó.
- 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ênProgressPrintable
, 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 {
}
- 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
}
- Sửa đổi nội dung khai báo của lớp
Quiz
để mở rộng giao diệnProgressPrintable
.
class Quiz : ProgressPrintable {
...
}
- Trong lớp
Quiz
, hãy thêm thuộc tính có tênprogressText
thuộc loạiString
, như chỉ định trong giao diệnProgressPrintable
. Vì thuộc tính đó đến từProgressPrintable
, nên đứng trướcval
và có từ khoá override (ghi đè).
override val progressText: String
- 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"
- 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"
- 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()
}
- Trong lớp
Quiz
, hãy thêm phương thứcprintProgressBar()
bằng từ khoáoverride
.
override fun printProgressBar() {
}
- Di chuyển mã từ hàm mở rộng
printProgressBar()
cũ vàoprintProgressBar()
mới từ giao diện. Sửa đổi dòng cuối cùng để tham chiếu đến biếnprogressText
mới từ giao diện bằng cách loại bỏ tham chiếu đếnQuiz
.
override fun printProgressBar() {
repeat(Quiz.answered) { print("▓") }
repeat(Quiz.total - Quiz.answered) { print("▒") }
println()
println(progressText)
}
- Xoá hàm tiện ích
printProgressBar()
. Hiện tại, chức năng này nay thuộc về lớpQuiz
có thể mở rộngProgressPrintable
.
Đoạn mã cần xoá:
fun Quiz.StudentProgress.printProgressBar() {
repeat(Quiz.answered) { print("▓") }
repeat(Quiz.total - Quiz.answered) { print("▒") }
println()
println(Quiz.progressText)
}
- Cập nhật mã trong
main()
. Do hàmprintProgressBar()
hiện là một phương thức của lớpQuiz
, nên trước tiên bạn cần tạo thực thể cho đối tượngQuiz
rồi gọiprintProgressBar()
.
fun main() {
Quiz().printProgressBar()
}
- 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
, question2
và question3
bằng let()
:
- Thêm một hàm vào lớp
Quiz
có tên làprintQuiz()
.
fun printQuiz() {
}
- Thêm mã dưới đây để in
questionText
,answer
vàdifficulty
của câu hỏi. Mặc dù nhiều thuộc tính được truy cập choquestion1
,question2
vàquestion3
, 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()
}
- Bao quanh mã truy cập vào các thuộc tính
questionText
,answer
vàdifficulty
bằng một lệnh gọi hàmlet()
trênquestion1
,question2
vàquestion3
. 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()
}
- Cập nhật mã trong
main()
để tạo một phiên bản của lớpQuiz
có tên làquiz
.
fun main() {
val quiz = Quiz()
}
- Gọi
printQuiz()
.
fun main() {
val quiz = Quiz()
quiz.printQuiz()
}
- 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()
.
- Gọi
apply()
sau dấu ngoặc đơn đóng khi tạo một thực thể của lớpQuiz
. Bạn có thể bỏ qua dấu ngoặc đơn khi gọiapply()
và sử dụng cú pháp trailing lambda.
val quiz = Quiz().apply {
}
- 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ếnquiz
hay sử dụng ký hiệu dấu chấm nữa.
val quiz = Quiz().apply {
printQuiz()
}
- Hàm
apply()
trả về thực thể của lớpQuiz
. Tuy nhiên, vì bạn không sử dụng hàm này nữa nên hãy xoá biếnquiz
. Với hàmapply()
, 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ủaQuiz
.
Quiz().apply {
printQuiz()
}
- 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àmapply()
trả về các đối tượng đã được lưu trữ trongquiz
.
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.