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

👥 Companion Object

🎯 Mục tiêu: Hiểu Companion Object - cách Kotlin thay thế static members của Java, factory patterns, và tương tác với Java code.


Java sử dụng static cho class-level members:

// Java
public class User {
public static final int MAX_NAME_LENGTH = 50;
public static User fromJson(String json) {
return new User(json);
}
}
// Sử dụng
User.MAX_NAME_LENGTH;
User.fromJson("{...}");

Kotlin không có static keyword. Thay vào đó, sử dụng companion object:

class User(val name: String) {
companion object {
const val MAX_NAME_LENGTH = 50
fun fromJson(json: String): User {
return User(json) // Simplified
}
}
}
// Sử dụng giống như static
User.MAX_NAME_LENGTH
User.fromJson("{...}")

class MyClass {
companion object {
val property = "value"
fun method() = println("Hello from companion")
}
}
fun main() {
println(MyClass.property) // value
MyClass.method() // Hello from companion
}
class User(val name: String) {
companion object Factory { // Đặt tên là "Factory"
fun create(name: String) = User(name)
fun createGuest() = User("Guest")
}
}
fun main() {
// Cả hai cách đều hoạt động
val user1 = User.create("Alice")
val user2 = User.Factory.create("Bob")
}

Companion object rất phù hợp cho Factory pattern:

class DatabaseConnection private constructor(
val host: String,
val port: Int,
val database: String
) {
companion object {
private var instance: DatabaseConnection? = null
// Factory methods
fun production(): DatabaseConnection {
return instance ?: DatabaseConnection(
host = "prod.db.example.com",
port = 5432,
database = "production"
).also { instance = it }
}
fun development(): DatabaseConnection {
return DatabaseConnection(
host = "localhost",
port = 5432,
database = "dev"
)
}
fun fromConfig(config: Map<String, String>): DatabaseConnection {
return DatabaseConnection(
host = config["host"] ?: "localhost",
port = config["port"]?.toIntOrNull() ?: 5432,
database = config["database"] ?: "default"
)
}
}
override fun toString() = "Connection($host:$port/$database)"
}
fun main() {
val prod = DatabaseConnection.production()
val dev = DatabaseConnection.development()
println(prod) // Connection(prod.db.example.com:5432/production)
println(dev) // Connection(localhost:5432/dev)
}

class AppConfig {
companion object {
// Compile-time constants
const val VERSION = "1.0.0"
const val API_VERSION = 2
const val BASE_URL = "https://api.example.com"
// Runtime values (không phải const)
val startTime = System.currentTimeMillis()
// ❌ Compile error - const chỉ cho primitives và String
// const val list = listOf(1, 2, 3)
}
}
fun main() {
println(AppConfig.VERSION) // 1.0.0
println(AppConfig.API_VERSION) // 2
println(AppConfig.startTime) // Timestamp
}

Companion object có thể implement interfaces:

interface JsonFactory<T> {
fun fromJson(json: String): T
fun schema(): String
}
class User(val name: String, val email: String) {
companion object : JsonFactory<User> {
override fun fromJson(json: String): User {
// Parse JSON (simplified)
return User("parsed", "parsed@example.com")
}
override fun schema(): String = """
{
"name": "string",
"email": "string"
}
""".trimIndent()
}
}
// Generic function working with any JsonFactory
fun <T> loadFromApi(factory: JsonFactory<T>, json: String): T {
return factory.fromJson(json)
}
fun main() {
val user = loadFromApi(User, """{"name": "Alice"}""")
println(User.schema())
}

class User(val name: String) {
companion object // Empty companion object
}
// Extension function cho companion object
fun User.Companion.createAdmin(name: String): User {
return User("Admin: $name")
}
fun User.Companion.createFromEnvironment(): User {
val name = System.getenv("USER") ?: "Unknown"
return User(name)
}
fun main() {
val admin = User.createAdmin("Alice")
val envUser = User.createFromEnvironment()
println(admin.name) // Admin: Alice
println(envUser.name) // Current system user
}

// Kotlin
class User(val name: String) {
companion object {
const val MAX_LENGTH = 50
@JvmStatic // Tạo static method thực sự
fun create(name: String) = User(name)
@JvmField // Expose field thay vì getter
val DEFAULT = User("Default")
}
}
// Java - sử dụng
public class Main {
public static void main(String[] args) {
// const val - tự động static
int max = User.MAX_LENGTH;
// @JvmStatic
User user = User.create("Alice");
// @JvmField
User defaultUser = User.DEFAULT;
// Không có annotation - phải dùng Companion
// User.Companion.someMethod();
}
}

Bài tập: Tạo Logger singleton với companion object

// TODO: Tạo Logger class với:
// - Private constructor
// - Companion object với instance getter
// - log(level: LogLevel, message: String)
// - info(), warn(), error() convenience methods

Lời giải:

enum class LogLevel { INFO, WARN, ERROR }
class Logger private constructor(private val tag: String) {
companion object {
private val instances = mutableMapOf<String, Logger>()
fun getInstance(tag: String = "APP"): Logger {
return instances.getOrPut(tag) { Logger(tag) }
}
// Convenience methods
fun info(message: String) = getInstance().info(message)
fun warn(message: String) = getInstance().warn(message)
fun error(message: String) = getInstance().error(message)
}
fun log(level: LogLevel, message: String) {
val timestamp = java.time.LocalDateTime.now()
println("[$timestamp][$tag][$level] $message")
}
fun info(message: String) = log(LogLevel.INFO, message)
fun warn(message: String) = log(LogLevel.WARN, message)
fun error(message: String) = log(LogLevel.ERROR, message)
}
fun main() {
// Using default logger
Logger.info("Application started")
Logger.warn("Low memory")
// Using tagged logger
val dbLogger = Logger.getInstance("DATABASE")
dbLogger.info("Connected to database")
dbLogger.error("Query failed")
}

// Fragment/Activity arguments
class UserFragment : Fragment() {
companion object {
private const val ARG_USER_ID = "user_id"
private const val ARG_USER_NAME = "user_name"
fun newInstance(userId: Long, userName: String): UserFragment {
return UserFragment().apply {
arguments = Bundle().apply {
putLong(ARG_USER_ID, userId)
putString(ARG_USER_NAME, userName)
}
}
}
}
private val userId by lazy { arguments?.getLong(ARG_USER_ID) ?: -1 }
private val userName by lazy { arguments?.getString(ARG_USER_NAME) ?: "" }
}
// Intent extras
class DetailActivity : AppCompatActivity() {
companion object {
const val EXTRA_ITEM_ID = "extra_item_id"
fun createIntent(context: Context, itemId: Long): Intent {
return Intent(context, DetailActivity::class.java).apply {
putExtra(EXTRA_ITEM_ID, itemId)
}
}
}
}
// ViewModelFactory
class UserViewModel(private val repository: UserRepository) : ViewModel() {
companion object {
fun factory(repository: UserRepository) = object : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
@Suppress("UNCHECKED_CAST")
return UserViewModel(repository) as T
}
}
}
}


Sau bài học này, bạn có thể:

  • Hiểu tại sao Kotlin dùng companion object thay vì static
  • Tạo companion object với properties và methods
  • Sử dụng named companion object
  • Tạo factory methods trong companion object
  • Sử dụng const val cho compile-time constants
  • Implement interface trong companion object
  • Sử dụng @JvmStatic@JvmField cho Java interop
  • Tạo extension functions cho companion object

Tiếp theo: Object Declaration