Bỏ qua để đến nội dung

🔌 Extension Functions

🎯 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 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
}

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)

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
}
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]
}
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
}

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!
}

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
}

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!
}

StringExtensions.kt
// ✅ Best practice: Nhóm extensions theo receiver type
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.*

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(): Double

Lờ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
}

// 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) }


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