Property Accessors trong Kotlin
🎯 Mục tiêu: Hiểu custom getters/setters, backing fields, và các patterns phổ biến với property accessors.
💡 Properties trong Kotlin
Trong Java, bạn phải viết fields + getters + setters. Kotlin tự động tạo chúng:
// Kotlin
class User(val name: String, var age: Int)
// Tương đương Java:
// public class User {
// private final String name;
// private int age;
//
// public User(String name, int age) { ... }
// public String getName() { return name; }
// public int getAge() { return age; }
// public void setAge(int age) { this.age = age; }
// }Kotlin cho phép customize getters và setters khi cần.
📝 Custom Getter
Computed Property
Property không lưu giá trị, tính toán mỗi lần truy cập:
class Rectangle(val width: Int, val height: Int) {
// Computed property - không có backing field
val area: Int
get() = width * height
val perimeter: Int
get() = 2 * (width + height)
val isSquare: Boolean
get() = width == height
// Single-expression getter (ngắn gọn hơn)
val diagonal: Double get() = kotlin.math.sqrt((width * width + height * height).toDouble())
}
fun main() {
val rect = Rectangle(10, 5)
println("Area: ${rect.area}") // 50
println("Perimeter: ${rect.perimeter}") // 30
println("Is Square: ${rect.isSquare}") // false
println("Diagonal: ${rect.diagonal}") // 11.18...
}Getter với logic
class User(val firstName: String, val lastName: String) {
val fullName: String
get() = "$firstName $lastName"
val initials: String
get() = "${firstName.first()}${lastName.first()}"
val isValid: Boolean
get() = firstName.isNotBlank() && lastName.isNotBlank()
}
fun main() {
val user = User("John", "Doe")
println(user.fullName) // John Doe
println(user.initials) // JD
println(user.isValid) // true
}🔧 Custom Setter
Setter với validation/transformation
class User(val id: Int) {
var name: String = ""
set(value) {
// Normalize name: trim and capitalize
field = value.trim().replaceFirstChar { it.uppercase() }
}
var email: String = ""
set(value) {
require(value.isBlank() || value.contains("@")) {
"Invalid email format"
}
field = value.lowercase().trim()
}
var age: Int = 0
set(value) {
field = value.coerceIn(0, 150) // Clamp to valid range
}
}
fun main() {
val user = User(1)
user.name = " alice "
println(user.name) // Alice
user.email = " [email protected] "
println(user.email) // [email protected]
user.age = 200
println(user.age) // 150 (clamped)
}Backing Field (field)
field là keyword đặc biệt để access backing field trong getter/setter:
class Counter {
var count: Int = 0
set(value) {
if (value >= 0) {
field = value // 'field' refers to backing field
}
// Không dùng 'this.count = value' - sẽ gây infinite loop!
}
// Property này KHÔNG có backing field (chỉ computed)
val isZero: Boolean
get() = count == 0
}Tránh infinite recursion:
var prop: Int = 0
set(value) {
prop = value // ❌ Gọi chính setter → infinite loop!
field = value // ✅ Access backing field trực tiếp
}🔒 Private Setter
Property có thể đọc từ bên ngoài, nhưng chỉ set từ bên trong:
class BankAccount(val accountNumber: String) {
var balance: Double = 0.0
private set // Chỉ class này có thể set
fun deposit(amount: Double) {
require(amount > 0) { "Amount must be positive" }
balance += amount
}
fun withdraw(amount: Double): Boolean {
return if (amount <= balance) {
balance -= amount
true
} else {
false
}
}
}
fun main() {
val account = BankAccount("123456")
// ✅ Có thể đọc
println(account.balance) // 0.0
// ❌ Không thể set trực tiếp
// account.balance = 1000.0 // Compile error!
// ✅ Phải dùng methods
account.deposit(1000.0)
println(account.balance) // 1000.0
}Kết hợp với custom setter
class Temperature {
var celsius: Double = 0.0
private set(value) {
field = value.coerceIn(-273.15, 1000.0) // Physical limits
}
fun setCelsius(value: Double) {
celsius = value
}
// Computed property for Fahrenheit
val fahrenheit: Double
get() = celsius * 9 / 5 + 32
}🎯 Late-Initialized Properties
Khi không thể khởi tạo property trong constructor:
class UserProfile {
// lateinit - sẽ khởi tạo sau
lateinit var username: String
lateinit var avatar: Bitmap
val isInitialized: Boolean
get() = ::username.isInitialized
fun loadFromApi(data: ApiResponse) {
username = data.username
avatar = data.avatar
}
fun display() {
if (::username.isInitialized) {
println("Username: $username")
} else {
println("Profile not loaded")
}
}
}lateinit restrictions:
- Chỉ cho
var, không choval - Chỉ cho non-primitive types (không cho Int, Double, Boolean…)
- Truy cập trước khi init →
UninitializedPropertyAccessException
📋 Backing Properties Pattern
Khi cần expose read-only version của mutable property:
class ViewModel {
// Private mutable
private val _users = mutableListOf<User>()
// Public read-only view
val users: List<User>
get() = _users
// Or using delegation
private val _items = mutableListOf<Item>()
val items: List<Item> by ::_items
fun addUser(user: User) {
_users.add(user)
}
}
fun main() {
val vm = ViewModel()
vm.addUser(User("Alice"))
// ✅ Đọc được
println(vm.users)
// ❌ Không thể modify từ bên ngoài
// vm.users.add(User("Bob")) // Compile error - List không có add()
}🔄 Observable Properties (Manual)
Trigger action khi property thay đổi:
class User(name: String) {
var name: String = name
set(value) {
val oldValue = field
field = value
onNameChanged(oldValue, value)
}
private fun onNameChanged(old: String, new: String) {
println("Name changed: $old -> $new")
// Update UI, notify observers, etc.
}
}
fun main() {
val user = User("Alice")
user.name = "Bob" // Name changed: Alice -> Bob
user.name = "Charlie" // Name changed: Bob -> Charlie
}Xem thêm Delegates.observable() trong bài Delegation để có cách tiện lợi hơn.
🛠️ Thực hành
Bài tập: Tạo Product class với validation
// TODO: Tạo class Product với:
// - name (trim và capitalize khi set)
// - price (không cho phép âm)
// - quantity (private set, chỉ có thể thay đổi qua addStock/sell)
// - totalValue (computed)Lời giải:
class Product(name: String, price: Double) {
var name: String = ""
set(value) {
field = value.trim().replaceFirstChar { it.uppercase() }
}
var price: Double = 0.0
set(value) {
field = maxOf(0.0, value) // Không cho phép âm
}
var quantity: Int = 0
private set
val totalValue: Double
get() = price * quantity
val formattedPrice: String
get() = "$${"%.2f".format(price)}"
init {
this.name = name
this.price = price
}
fun addStock(amount: Int) {
require(amount > 0) { "Amount must be positive" }
quantity += amount
}
fun sell(amount: Int): Boolean {
return if (amount <= quantity) {
quantity -= amount
true
} else {
false
}
}
override fun toString(): String {
return "$name - ${formattedPrice} x $quantity = $${"%.2f".format(totalValue)}"
}
}
fun main() {
val product = Product(" laptop ", 999.99)
println(product.name) // Laptop
product.addStock(10)
println(product) // Laptop - $999.99 x 10 = $9999.90
product.sell(3)
println(product.quantity) // 7
product.price = -100.0
println(product.price) // 0.0 (không cho âm)
}📱 Trong Android
// ViewModel với LiveData pattern
class UserViewModel : ViewModel() {
private val _user = MutableLiveData<User?>()
val user: LiveData<User?> = _user
private val _loading = MutableLiveData<Boolean>()
val loading: LiveData<Boolean> = _loading
fun loadUser(id: Long) {
_loading.value = true
viewModelScope.launch {
_user.value = repository.getUser(id)
_loading.value = false
}
}
}
// Custom View với observable properties
class CustomProgressBar @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null
) : View(context, attrs) {
var progress: Float = 0f
set(value) {
field = value.coerceIn(0f, 1f)
invalidate() // Redraw when progress changes
}
var progressColor: Int = Color.BLUE
set(value) {
field = value
paint.color = value
invalidate()
}
private val paint = Paint().apply {
color = progressColor
style = Paint.Style.FILL
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
canvas.drawRect(0f, 0f, width * progress, height.toFloat(), paint)
}
}⚠️ Lưu ý quan trọng
Khi nào dùng custom accessor:
- Custom getter: Computed values, caching, lazy loading
- Custom setter: Validation, normalization, side effects
- Private setter: Expose read-only view, controlled updates
Performance consideration:
Computed properties tính toán lại mỗi lần access. Nếu expensive, hãy cache:
class Expensive {
private var _cachedValue: String? = null
val expensiveValue: String
get() {
if (_cachedValue == null) {
_cachedValue = computeExpensiveValue()
}
return _cachedValue!!
}
}
// Hoặc dùng lazy
val lazyValue: String by lazy { computeExpensiveValue() }✅ Checklist - Tự kiểm tra
Sau bài học này, bạn có thể:
- Tạo computed properties với custom getter
- Tạo custom setter với validation/transformation
- Sử dụng
fieldđể access backing field - Sử dụng private setter để protect properties
- Hiểu khi nào property có backing field
- Áp dụng backing properties pattern
- Sử dụng
lateinitcho delayed initialization
Tiếp theo: Visibility Modifiers