Skip to Content
Kotlin📘 Ngôn ngữ Kotlin🔐 Property Accessors

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 cho val
  • 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 lateinit cho delayed initialization

Tiếp theo: Visibility Modifiers

Last updated on