🔐 Property Accessors
🎯 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
Phần tiêu đề “💡 Properties trong Kotlin”Trong Java, bạn phải viết fields + getters + setters. Kotlin tự động tạo chúng:
// Kotlinclass 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
Phần tiêu đề “📝 Custom Getter”Computed Property
Phần tiêu đề “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
Phần tiêu đề “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
Phần tiêu đề “🔧 Custom Setter”Setter với validation/transformation
Phần tiêu đề “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 = " ALICE@EXAMPLE.COM " println(user.email) // alice@example.com
user.age = 200 println(user.age) // 150 (clamped)}Backing Field (field)
Phần tiêu đề “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}🔒 Private Setter
Phần tiêu đề “🔒 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
Phần tiêu đề “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
Phần tiêu đề “🎯 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") } }}📋 Backing Properties Pattern
Phần tiêu đề “📋 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)
Phần tiêu đề “🔄 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}🛠️ Thực hành
Phần tiêu đề “🛠️ 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
Phần tiêu đề “📱 Trong Android”// ViewModel với LiveData patternclass 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 propertiesclass 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
Phần tiêu đề “⚠️ Lưu ý quan trọng”✅ Checklist - Tự kiểm tra
Phần tiêu đề “✅ 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