Back to articles

Kotlin

Object and Companion Object in Kotlin

Object and Companion Object in Kotlin

Two of the most commonly misunderstood features in Kotlin are object and companion object. They look similar but serve different purposes. This guide explains both clearly — what they are, when to use them, and how they appear in real Android code every day.


The Problem They Solve

In Java, you use the static keyword for class-level members — constants, factory methods, utility functions, and singletons. Kotlin has no static keyword. Instead, it gives you two cleaner mechanisms:

  • object — for singletons and anonymous objects
  • companion object — for class-level members (Kotlin's replacement for static)

object Declaration — Singleton

The object keyword creates a singleton — a class that has exactly one instance, created automatically the first time it's accessed.

object AppConfig {
    const val BASE_URL = "https://api.androidnewworld.com"
    const val TIMEOUT_SECONDS = 30
    const val MAX_RETRY_COUNT = 3
    var isDebugMode = false

    fun getFullUrl(endpoint: String): String {
        return "$BASE_URL/$endpoint"
    }
}

You access it directly without creating an instance:

println(AppConfig.BASE_URL)                    // https://api.androidnewworld.com
println(AppConfig.getFullUrl("articles"))      // https://api.androidnewworld.com/articles
AppConfig.isDebugMode = true

Real-world analogy: Think of object like a government — there is exactly one Government of India. You don't create a new one each time you need it. You just reference the one that exists. That's a singleton.


How Singleton Works Under the Hood

The Kotlin compiler turns an object declaration into a class with a private constructor and a public static INSTANCE field — the classic Java singleton pattern, but done automatically.

// What Kotlin generates (roughly)
public final class AppConfig {
    public static final AppConfig INSTANCE = new AppConfig();
    private AppConfig() { }
    // ... members
}

You get thread-safe, lazy initialization for free — no double-checked locking needed.


object — Common Use Cases

1. Application-Wide Configuration

object NetworkConfig {
    const val BASE_URL = "https://api.androidnewworld.com/v1/"
    const val CONNECT_TIMEOUT = 30L
    const val READ_TIMEOUT = 30L
    const val WRITE_TIMEOUT = 30L

    val defaultHeaders = mapOf(
        "Content-Type" to "application/json",
        "Accept" to "application/json"
    )
}

2. Utility / Helper Object

object DateUtils {
    private val formatter = SimpleDateFormat("MMM dd, yyyy", Locale.getDefault())

    fun formatDate(timestamp: Long): String {
        return formatter.format(Date(timestamp))
    }

    fun formatRelativeTime(timestamp: Long): String {
        val diff = System.currentTimeMillis() - timestamp
        val minutes = diff / 60_000
        val hours = minutes / 60
        val days = hours / 24

        return when {
            minutes < 1  -> "just now"
            minutes < 60 -> "${minutes}m ago"
            hours < 24   -> "${hours}h ago"
            days < 7     -> "${days}d ago"
            else         -> formatDate(timestamp)
        }
    }

    fun isToday(timestamp: Long): Boolean {
        val today = Calendar.getInstance()
        val date = Calendar.getInstance().apply { timeInMillis = timestamp }
        return today.get(Calendar.DAY_OF_YEAR) == date.get(Calendar.DAY_OF_YEAR) &&
               today.get(Calendar.YEAR) == date.get(Calendar.YEAR)
    }
}

// Usage
println(DateUtils.formatRelativeTime(someTimestamp))
println(DateUtils.isToday(System.currentTimeMillis()))  // true

3. Event Bus / App-Level State

object UserSession {
    var currentUser: User? = null
    var authToken: String? = null
    var isLoggedIn: Boolean = false

    fun login(user: User, token: String) {
        currentUser = user
        authToken = token
        isLoggedIn = true
    }

    fun logout() {
        currentUser = null
        authToken = null
        isLoggedIn = false
    }
}

// Anywhere in the app
if (UserSession.isLoggedIn) {
    showDashboard()
}
UserSession.login(user, "token_abc123")

4. Constants File

object Constants {
    // API
    const val BASE_URL = "https://api.androidnewworld.com/"
    const val API_KEY = "your_api_key_here"

    // SharedPreferences keys
    const val PREF_USER_ID = "pref_user_id"
    const val PREF_AUTH_TOKEN = "pref_auth_token"
    const val PREF_THEME = "pref_theme"

    // Bundle keys
    const val KEY_ARTICLE_ID = "article_id"
    const val KEY_USER_ID = "user_id"

    // Request codes
    const val REQUEST_CAMERA = 1001
    const val REQUEST_STORAGE = 1002
    const val REQUEST_LOCATION = 1003

    // Timeouts
    const val SPLASH_DELAY = 2000L
    const val DEBOUNCE_DELAY = 300L
}

object Extending Classes and Interfaces

An object can extend a class or implement interfaces:

interface Greeter {
    fun greet(name: String): String
}

object EnglishGreeter : Greeter {
    override fun greet(name: String) = "Hello, $name!"
}

object HindiGreeter : Greeter {
    override fun greet(name: String) = "Namaste, $name!"
}

fun greetUser(greeter: Greeter, name: String) {
    println(greeter.greet(name))
}

greetUser(EnglishGreeter, "Alice")   // Hello, Alice!
greetUser(HindiGreeter, "Bob")       // Namaste, Bob!

Anonymous Object — One-Time Object Without a Name

You can create an object on the fly without declaring a named class. This is commonly used for implementing listeners and callbacks:

// Implementing an interface inline
val clickListener = object : View.OnClickListener {
    override fun onClick(v: View?) {
        println("View clicked!")
    }
}
button.setOnClickListener(clickListener)

// Anonymous object with multiple interface implementations
val handler = object : TextWatcher, View.OnFocusChangeListener {
    override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
    override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
        validateInput(s.toString())
    }
    override fun afterTextChanged(s: Editable?) {}
    override fun onFocusChange(v: View?, hasFocus: Boolean) {
        if (!hasFocus) validateInput(editText.text.toString())
    }
}

In practice, single-method interfaces are more commonly replaced with lambdas:

// Lambda is cleaner when interface has only one method
button.setOnClickListener { println("View clicked!") }

companion object — Class-Level Members

A companion object lives inside a class and provides members that belong to the class itself, not to instances. It's Kotlin's replacement for Java's static.

class User(val name: String, val email: String) {

    companion object {
        // Class-level constant
        const val MAX_NAME_LENGTH = 50

        // Factory function — creates User from different sources
        fun fromJson(json: String): User {
            // parse JSON
            return User("Parsed Name", "parsed@email.com")
        }

        fun guest(): User = User("Guest", "guest@anonymous.com")
    }
}

// Access companion members on the class name, not instance
println(User.MAX_NAME_LENGTH)          // 50
val user = User.fromJson("{...}")
val guest = User.guest()

companion object — Common Use Cases

1. Factory Functions

Factory functions create objects in ways the constructor alone can't express clearly:

class Article private constructor(
    val id: String,
    val title: String,
    val content: String,
    val type: String
) {
    companion object {
        fun createBlogPost(title: String, content: String): Article {
            return Article(
                id = UUID.randomUUID().toString(),
                title = title,
                content = content,
                type = "blog"
            )
        }

        fun createNewsItem(title: String, content: String): Article {
            return Article(
                id = UUID.randomUUID().toString(),
                title = title,
                content = content,
                type = "news"
            )
        }

        fun fromMap(map: Map<String, String>): Article {
            return Article(
                id = map["id"] ?: "",
                title = map["title"] ?: "",
                content = map["content"] ?: "",
                type = map["type"] ?: "blog"
            )
        }
    }
}

val post = Article.createBlogPost("Kotlin Guide", "Everything about Kotlin...")
val news = Article.createNewsItem("Android 16 Released", "Google announces...")

2. TAG Constant for Logging

One of the most common uses of companion object in Android:

class HomeFragment : Fragment() {

    companion object {
        private const val TAG = "HomeFragment"
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        Log.d(TAG, "onViewCreated called")
    }
}

3. Fragment/Activity Factory — newInstance Pattern

The recommended way to pass arguments to a Fragment:

class ArticleDetailFragment : Fragment() {

    companion object {
        private const val ARG_ARTICLE_ID = "article_id"
        private const val ARG_CATEGORY = "category"

        // Factory function — ensures required arguments are always provided
        fun newInstance(articleId: String, category: String): ArticleDetailFragment {
            return ArticleDetailFragment().apply {
                arguments = Bundle().apply {
                    putString(ARG_ARTICLE_ID, articleId)
                    putString(ARG_CATEGORY, category)
                }
            }
        }
    }

    private val articleId: String by lazy {
        requireArguments().getString(ARG_ARTICLE_ID) ?: error("Article ID required")
    }

    private val category: String by lazy {
        requireArguments().getString(ARG_CATEGORY) ?: "general"
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        viewModel.loadArticle(articleId)
    }
}

// Usage — from another Fragment or Activity
val fragment = ArticleDetailFragment.newInstance(
    articleId = "article_001",
    category = "kotlin"
)

4. Providing Dependencies / Injection

class UserRepository private constructor(
    private val apiService: ApiService,
    private val userDao: UserDao
) {
    companion object {
        @Volatile
        private var instance: UserRepository? = null

        fun getInstance(apiService: ApiService, userDao: UserDao): UserRepository {
            return instance ?: synchronized(this) {
                instance ?: UserRepository(apiService, userDao).also {
                    instance = it
                }
            }
        }
    }
}

Naming a companion object

By default, companion objects are accessed as Companion. You can give them a custom name:

class MyClass {
    companion object Factory {
        fun create(): MyClass = MyClass()
    }
}

// Access via class name (most common)
val obj = MyClass.create()

// Access via companion name (less common)
val obj = MyClass.Factory.create()

companion object Implementing an Interface

interface JsonParser<T> {
    fun fromJson(json: String): T
}

class User(val name: String, val email: String) {

    companion object : JsonParser<User> {
        override fun fromJson(json: String): User {
            // parse JSON
            return User("Parsed", "parsed@email.com")
        }
    }
}

// Can be passed where JsonParser<User> is expected
fun <T> parseData(json: String, parser: JsonParser<T>): T {
    return parser.fromJson(json)
}

val user = parseData("{...}", User)  // User's companion object is passed

object vs companion object — Key Differences

  object companion object
Standalone ✅ Declared on its own ❌ Must be inside a class
Access Via its own name Via the enclosing class name
One per file Can have many One per class
Use for Singletons, utils, constants Class-level members, factories, TAG
Inherits from Can extend class/interface Can extend class/interface
Java equivalent Singleton class static members

Common Mistakes to Avoid

Mistake 1: Using object when you need multiple instances

// ❌ Wrong — object is singleton, only one user can exist
object User {
    var name = ""
    var email = ""
}

// ✅ Use class for things you need multiple of
class User(val name: String, val email: String)

Mistake 2: Putting mutable shared state in object carelessly

// ❌ Dangerous in multi-threaded code
object Counter {
    var count = 0  // not thread-safe
}

// ✅ Use atomic operations or synchronization
object Counter {
    private val _count = AtomicInteger(0)
    val count: Int get() = _count.get()
    fun increment() = _count.incrementAndGet()
}

Mistake 3: Accessing companion members via instance

class User(val name: String) {
    companion object {
        fun guest() = User("Guest")
    }
}

val user = User("Alice")
val guest = user.guest()   // ❌ Works but misleading — looks like instance method
val guest = User.guest()   // ✅ Clear — this is a class-level operation

Mistake 4: Not using const for compile-time constants

companion object {
    val MAX_SIZE = 100       // ❌ val — runtime constant, slightly less efficient
    const val MAX_SIZE = 100 // ✅ const val — compile-time constant, inlined by compiler
}

Summary

  • object creates a singleton — one instance for the entire app, accessed by name
  • Use object for app-wide config, utilities, constants, and event buses
  • companion object provides class-level members — Kotlin's replacement for Java static
  • Use companion object for factory functions, TAG constants, and newInstance patterns
  • Anonymous object lets you implement interfaces inline without naming a class
  • Both object and companion object can extend classes and implement interfaces
  • Always access companion object members via the class name, not an instance
  • Use const val in companion objects for compile-time constants
  • Avoid mutable shared state in object without thread safety consideration

These two features replace the static keyword entirely in Kotlin and do it in a much cleaner, more object-oriented way. You'll see them in practically every Android codebase.

Happy coding!

76 views · 0 comments

Comments (0)

No comments yet. Be the first to share your thoughts.