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!