Extension Functions trong Kotlin
🎯 Mục tiêu: Hiểu Extension Functions - cách “thêm” methods vào classes có sẵn mà không cần kế thừa hay modify source code.
💡 Extension Functions là gì?
Extension function cho phép “mở rộng” class bằng cách thêm function mới, hoạt động như thể nó là member của class đó.
// Thêm method cho String
fun String.addExclamation(): String = "$this!"
// Thêm method cho Int
fun Int.isEven(): Boolean = this % 2 == 0
fun main() {
println("Hello".addExclamation()) // Hello!
println(4.isEven()) // true
println(5.isEven()) // false
}📝 Cú pháp
fun ReceiverType.functionName(parameters): ReturnType {
// 'this' tham chiếu đến receiver object
return this.someOperation()
}| Component | Description |
|---|---|
ReceiverType | Class bạn muốn extend |
functionName | Tên function mới |
this | Instance của ReceiverType (implicit) |
🔧 Ví dụ thực tế
String Extensions
fun String.isEmail(): Boolean =
contains("@") && contains(".") && length > 5
fun String.toTitleCase(): String =
split(" ").joinToString(" ") { word ->
word.lowercase().replaceFirstChar { it.uppercase() }
}
fun String.truncate(maxLength: Int, suffix: String = "..."): String =
if (length <= maxLength) this else take(maxLength - suffix.length) + suffix
fun String.removeWhitespace(): String =
filterNot { it.isWhitespace() }
fun main() {
println("[email protected]".isEmail()) // true
println("hello world kotlin".toTitleCase()) // Hello World Kotlin
println("This is a long text".truncate(10)) // This is...
println("h e l l o".removeWhitespace()) // hello
}Collection Extensions
fun <T> List<T>.secondOrNull(): T? = getOrNull(1)
fun <T> List<T>.randomOrNull(): T? = if (isEmpty()) null else random()
fun <T> List<T>.chunkedBySize(size: Int): List<List<T>> = chunked(size)
fun <T> MutableList<T>.swap(index1: Int, index2: Int) {
val tmp = this[index1]
this[index1] = this[index2]
this[index2] = tmp
}
fun main() {
val numbers = listOf(1, 2, 3, 4, 5)
println(numbers.secondOrNull()) // 2
println(numbers.randomOrNull()) // Random number
println(numbers.chunkedBySize(2)) // [[1, 2], [3, 4], [5]]
val mutableList = mutableListOf("a", "b", "c")
mutableList.swap(0, 2)
println(mutableList) // [c, b, a]
}Number Extensions
fun Int.squared(): Int = this * this
fun Int.cubed(): Int = this * this * this
fun Double.roundTo(decimals: Int): Double =
"%.${decimals}f".format(this).toDouble()
fun Int.ordinal(): String = when {
this % 100 in 11..13 -> "${this}th"
this % 10 == 1 -> "${this}st"
this % 10 == 2 -> "${this}nd"
this % 10 == 3 -> "${this}rd"
else -> "${this}th"
}
fun main() {
println(5.squared()) // 25
println(3.cubed()) // 27
println(3.14159.roundTo(2)) // 3.14
println(1.ordinal()) // 1st
println(2.ordinal()) // 2nd
println(3.ordinal()) // 3rd
println(11.ordinal()) // 11th
println(21.ordinal()) // 21st
}📦 Extension Properties
Không chỉ functions, Kotlin cho phép extension properties:
val String.lastChar: Char
get() = this[length - 1]
val String.firstChar: Char
get() = this[0]
val <T> List<T>.lastIndex: Int
get() = size - 1
// Mutable extension property
var StringBuilder.lastChar: Char
get() = this[length - 1]
set(value) {
this.setCharAt(length - 1, value)
}
fun main() {
println("Hello".lastChar) // o
println("Hello".firstChar) // H
val sb = StringBuilder("Hello")
sb.lastChar = '!'
println(sb) // Hell!
}Extension properties không có backing field: Không thể khởi tạo state, chỉ có computed getters/setters.
// ❌ Lỗi - không có backing field
val String.cachedLength: Int = length
// ✅ OK - computed property
val String.cachedLength: Int
get() = length🎯 Generic Extensions
fun <T> T.toSingletonList(): List<T> = listOf(this)
fun <T> T.applyIf(condition: Boolean, block: T.() -> T): T =
if (condition) block() else this
fun <T : Comparable<T>> T.coerceInOrNull(range: ClosedRange<T>): T? =
if (this in range) this else null
fun main() {
val list = "Hello".toSingletonList()
println(list) // [Hello]
val number = 5.applyIf(true) { this * 2 }
println(number) // 10
println(5.coerceInOrNull(1..10)) // 5
println(15.coerceInOrNull(1..10)) // null
}⚠️ Static Resolution
Extensions are resolved STATICALLY, không phải dynamically (polymorphism):
open class Parent
class Child : Parent()
fun Parent.greet() = "Hello from Parent"
fun Child.greet() = "Hello from Child"
fun printGreeting(parent: Parent) {
println(parent.greet())
}
fun main() {
printGreeting(Child()) // "Hello from Parent" ❗ Không phải "Hello from Child"
}Extension được chọn dựa trên compile-time type (Parent), không phải runtime type (Child).
Member Functions Win
Nếu class đã có member function cùng tên, member sẽ được gọi:
class Example {
fun greet() = "Member function"
}
fun Example.greet() = "Extension function"
fun main() {
println(Example().greet()) // "Member function" ← member wins!
}📁 Organizing Extensions
// ✅ Best practice: Nhóm extensions theo receiver type
// File: StringExtensions.kt
package com.example.extensions
fun String.isEmail(): Boolean = /* ... */
fun String.toTitleCase(): String = /* ... */
// File: ListExtensions.kt
package com.example.extensions
fun <T> List<T>.secondOrNull(): T? = /* ... */
// Import và sử dụng
import com.example.extensions.isEmail
import com.example.extensions.toTitleCase
// hoặc
import com.example.extensions.*🛠️ Thực hành
Bài tập: Tạo utility extensions
// TODO: Tạo các extensions sau:
// - String.isValidPhoneNumber(): Boolean (10-11 digits)
// - String.mask(visibleChars: Int): String (mask all except last N chars)
// - Int.toOrdinal(): String (1st, 2nd, 3rd, 4th...)
// - List<Int>.median(): DoubleLời giải:
fun String.isValidPhoneNumber(): Boolean {
val digitsOnly = filter { it.isDigit() }
return digitsOnly.length in 10..11
}
fun String.mask(visibleChars: Int = 4, maskChar: Char = '*'): String {
if (length <= visibleChars) return this
val masked = maskChar.toString().repeat(length - visibleChars)
return masked + takeLast(visibleChars)
}
fun Int.toOrdinal(): String = when {
this % 100 in 11..13 -> "${this}th"
this % 10 == 1 -> "${this}st"
this % 10 == 2 -> "${this}nd"
this % 10 == 3 -> "${this}rd"
else -> "${this}th"
}
fun List<Int>.median(): Double {
if (isEmpty()) return 0.0
val sorted = sorted()
val middle = size / 2
return if (size % 2 == 0) {
(sorted[middle - 1] + sorted[middle]) / 2.0
} else {
sorted[middle].toDouble()
}
}
fun main() {
println("0901234567".isValidPhoneNumber()) // true
println("090123".isValidPhoneNumber()) // false
println("4532123456789012".mask()) // ************9012
println("1234567890".mask(3, 'X')) // XXXXXXX890
println(1.toOrdinal()) // 1st
println(22.toOrdinal()) // 22nd
println(111.toOrdinal()) // 111th
println(listOf(1, 3, 5, 7, 9).median()) // 5.0
println(listOf(1, 2, 3, 4, 5, 6).median()) // 3.5
}📱 Trong Android
// View Extensions
fun View.show() { visibility = View.VISIBLE }
fun View.hide() { visibility = View.GONE }
fun View.invisible() { visibility = View.INVISIBLE }
fun View.setVisibleIf(condition: Boolean) {
visibility = if (condition) View.VISIBLE else View.GONE
}
// Context Extensions
fun Context.toast(message: String, duration: Int = Toast.LENGTH_SHORT) {
Toast.makeText(this, message, duration).show()
}
fun Context.dpToPx(dp: Int): Int =
(dp * resources.displayMetrics.density).toInt()
fun Context.getColorCompat(colorRes: Int): Int =
ContextCompat.getColor(this, colorRes)
// Fragment Extensions
fun Fragment.hideKeyboard() {
view?.let { activity?.hideKeyboard(it) }
}
// EditText Extensions
fun EditText.onTextChanged(action: (String) -> Unit) {
addTextChangedListener(object : TextWatcher {
override fun afterTextChanged(s: Editable?) {}
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
action(s?.toString() ?: "")
}
})
}
// Usage
binding.button.show()
binding.progressBar.setVisibleIf(isLoading)
toast("Welcome!")
binding.editText.onTextChanged { text -> validateInput(text) }⚠️ Lưu ý quan trọng
Khi nào dùng Extension Functions:
- Utility functions cho existing classes
- Thêm domain-specific methods
- Improve readability
- Không có access đến private members
Khi KHÔNG nên dùng:
- Cần access private members → dùng member function
- Override behavior → dùng inheritance
- Cần polymorphism → dùng interface
Extensions không access private members:
class User(private val password: String)
// ❌ Không thể access private
fun User.showPassword() = password // Compile error!✅ Checklist - Tự kiểm tra
Sau bài học này, bạn có thể:
- Tạo extension functions cho existing classes
- Sử dụng
thisđể tham chiếu receiver - Tạo generic extension functions
- Tạo extension properties
- Hiểu static resolution (không có polymorphism)
- Biết member functions có priority cao hơn extensions
- Tổ chức extensions trong files riêng
Tiếp theo: Inline Functions