Constructors are how objects come to life. Every time you write User("Alice", 25) or Intent(context, MainActivity::class.java), you're calling a constructor. Kotlin's constructor system is more expressive and concise than Java's — but it has some nuances worth understanding deeply. This guide covers everything from primary constructors to init blocks, secondary constructors, and constructor chaining.


What Is a Constructor?

A constructor is a special function that runs when an object is created. Its job is to initialize the object's properties and put it in a valid state.

class User(val name: String, val age: Int)

val user = User("Alice", 25)
// Constructor runs → name = "Alice", age = 25
// Object is now ready to use

Kotlin has two types of constructors:

  1. Primary constructor — defined in the class header
  2. Secondary constructor — defined inside the class body with the constructor keyword

Primary Constructor — Defined in the Class Header

The primary constructor is the most concise way to define a class and its properties simultaneously.

class User(val name: String, val age: Int, val email: String)

This single line:

  • Declares the class User
  • Defines the primary constructor with three parameters
  • Creates three properties: name, age, email
val user = User("Alice", 25, "alice@email.com")
println(user.name)    // Alice
println(user.age)     // 25
println(user.email)   // alice@email.com
val vs var in Primary Constructor
class Person(
    val name: String,      // val = immutable property
    var age: Int,          // var = mutable property
    val id: String = ""    // with default value
)

val person = Person("Alice", 25)
person.age = 26        // ✅ allowed — age is var
person.name = "Bob"    // ❌ error — name is val

Constructor Parameters Without val/var

Parameters without val or var are constructor parameters only — they are NOT properties. They exist only during the constructor call.

class User(val name: String, password: String) {
    // 'password' is a parameter, not a property
    val hashedPassword: String = hashPassword(password)
    // password is used here in init/property initialization, then gone
}

val user = User("Alice", "secret123")
println(user.name)           // ✅ Alice — it's a property
println(user.password)       // ❌ error — not a property
println(user.hashedPassword) // ✅ works — this is a property

This is useful for values you need during construction but don't want to store.

Default Parameter Values

class Article(
    val title: String,
    val content: String,
    val category: String = "General",
    val isPublished: Boolean = false,
    val viewCount: Int = 0
)

// All arguments
val a1 = Article("Kotlin Guide", "Content here", "Kotlin", true, 100)

// Only required — defaults used for rest
val a2 = Article("Quick Post", "Short content")

// Named arguments — skip some defaults
val a3 = Article(
    title = "Android Tips",
    content = "Tip content",
    isPublished = true
)

Visibility of Primary Constructor

By default, the primary constructor is public. You can change its visibility:

// Private constructor — only the class itself can create instances
class Singleton private constructor() {
    companion object {
        val instance: Singleton by lazy { Singleton() }
    }
}

// Internal constructor — only visible within the module
class InternalClass internal constructor(val data: String)

init Block — Running Code During Construction

The primary constructor can't contain code directly. Use init blocks to run initialization logic when the object is created.

class User(val name: String, val age: Int) {
    val displayName: String

    init {
        // Runs right after primary constructor parameters are set
        displayName = name.trim().replaceFirstChar { it.uppercase() }
        println("User created: $displayName")
    }
}

val user = User("  alice  ", 25)
// Prints: User created: Alice
println(user.displayName)  // Alice

Multiple init Blocks

You can have multiple init blocks — they run in the order they appear, interleaved with property initializers.

class Demo(val x: Int) {
    val doubled = x * 2           // property initializer runs first

    init {
        println("First init: x=$x, doubled=$doubled")
    }

    val tripled = x * 3           // second property initializer

    init {
        println("Second init: tripled=$tripled")
    }
}

val d = Demo(5)
// First init: x=5, doubled=10
// Second init: tripled=15

Validation in init

init is the perfect place to validate constructor arguments:

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

    init {
        require(name.isNotBlank()) { "Name cannot be blank" }
        require(age >= 0) { "Age cannot be negative: $age" }
        require(age <= 150) { "Age is unrealistic: $age" }
        require(email.contains("@")) { "Invalid email: $email" }
    }
}

val valid = User("Alice", 25, "alice@email.com")   // ✅ works

val invalid = User("", 25, "alice@email.com")
// ❌ throws IllegalArgumentException: Name cannot be blank

require() throws IllegalArgumentException with the given message if the condition is false.


Exact Order of Initialization

This is a subtle but important point. When an object is created, Kotlin initializes things in this exact order:

  1. Primary constructor parameters are set
  2. Property initializers and init blocks run in the order they appear in the class body
class OrderDemo(val value: Int) {
    val step1 = "Property 1: $value".also { println(it) }

    init {
        println("Init block 1: value=$value")
    }

    val step2 = "Property 2: ${value * 2}".also { println(it) }

    init {
        println("Init block 2: step1=$step1")
    }
}

val obj = OrderDemo(10)
// Property 1: 10
// Init block 1: value=10
// Property 2: 20
// Init block 2: step1=Property 1: 10

Secondary Constructors

Secondary constructors are defined inside the class body with the constructor keyword. Every secondary constructor must delegate to the primary constructor using this(...).

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

    // Secondary constructor — must call primary with 'this'
    constructor(name: String) : this(name, 0, "")

    constructor(name: String, age: Int) : this(name, age, "")
}

val u1 = User("Alice", 25, "alice@email.com")   // primary
val u2 = User("Bob")                             // secondary → name="Bob", age=0, email=""
val u3 = User("Charlie", 30)                     // secondary → email=""

Secondary Constructor with Logic

Secondary constructors can contain their own logic after calling the primary:

class Article(val title: String, val content: String, val category: String) {

    var wordCount: Int = 0

    constructor(title: String, content: String) : this(title, content, "General") {
        // This runs AFTER the primary constructor and init blocks
        wordCount = content.split(" ").size
        println("Article created with $wordCount words")
    }
}

val article = Article("Kotlin Guide", "Learn Kotlin with examples")
// Article created with 4 words
println(article.wordCount)   // 4
println(article.category)    // General

Order When Secondary Constructor Is Used

  1. Primary constructor parameters set
  2. Property initializers and init blocks run (top to bottom)
  3. Secondary constructor body runs
class Demo(val x: Int) {
    init {
        println("init block: x=$x")
    }

    constructor(x: Int, y: Int) : this(x) {
        println("secondary constructor: y=$y")
    }
}

val d = Demo(5, 10)
// init block: x=5           ← init runs first
// secondary constructor: y=10  ← then secondary body

Default Parameters vs Secondary Constructors

In most cases, default parameters in the primary constructor are cleaner than secondary constructors.

// ❌ Secondary constructors — verbose
class User(val name: String, val age: Int, val email: String) {
    constructor(name: String) : this(name, 0, "")
    constructor(name: String, age: Int) : this(name, age, "")
}

// ✅ Default parameters — much cleaner, same result
class User(
    val name: String,
    val age: Int = 0,
    val email: String = ""
)

Use secondary constructors when:

  • You need to do something different (not just provide defaults)
  • You're extending a Java class that requires specific constructors
  • You need to convert from a completely different type
// Secondary constructor from a different type — makes sense here
class User(val name: String, val email: String) {

    // Create User from a Map (e.g., from JSON parsing)
    constructor(map: Map<String, String>) : this(
        name = map["name"] ?: "",
        email = map["email"] ?: ""
    )

    // Create User from another data type
    constructor(googleAccount: GoogleSignInAccount) : this(
        name = googleAccount.displayName ?: "",
        email = googleAccount.email ?: ""
    )
}

val user1 = User("Alice", "alice@email.com")
val user2 = User(mapOf("name" to "Bob", "email" to "bob@email.com"))
val user3 = User(googleAccount)

Constructor Chaining

Secondary constructors can chain to other secondary constructors, which then delegate to the primary:

class Connection(
    val host: String,
    val port: Int,
    val timeout: Int,
    val useSSL: Boolean
) {
    // Secondary 1 — chains to secondary 2
    constructor(host: String) : this(host, 443)

    // Secondary 2 — chains to secondary 3
    constructor(host: String, port: Int) : this(host, port, 30)

    // Secondary 3 — chains to primary
    constructor(host: String, port: Int, timeout: Int) : this(host, port, timeout, true)
}

val c1 = Connection("api.example.com")           // port=443, timeout=30, ssl=true
val c2 = Connection("api.example.com", 8080)     // timeout=30, ssl=true
val c3 = Connection("api.example.com", 8080, 60) // ssl=true

Constructors in Inheritance

When a class extends another class, the child class must initialize the parent's constructor.

Primary constructor calling parent:

open class Animal(val name: String, val sound: String)

class Dog(name: String) : Animal(name, "Woof")
class Cat(name: String, indoor: Boolean = true) : Animal(name, "Meow")

val dog = Dog("Rex")
println(dog.name)    // Rex
println(dog.sound)   // Woof

Secondary constructor calling parent:

open class View(context: Context)

class CustomButton : View {
    constructor(context: Context) : super(context)
    constructor(context: Context, attrs: AttributeSet?) : super(context)
}

This is a common pattern when extending Android Views.


Real Android Examples

Fragment with safe argument creation

class ArticleFragment : Fragment() {

    // Properties initialized from arguments
    private val articleId: String by lazy {
        requireArguments().getString(ARG_ARTICLE_ID)
            ?: error("ArticleFragment requires article_id argument")
    }

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

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

        // Factory function using companion — cleaner than secondary constructor
        fun newInstance(articleId: String, category: String = "general"): ArticleFragment {
            return ArticleFragment().apply {
                arguments = Bundle().apply {
                    putString(ARG_ARTICLE_ID, articleId)
                    putString(ARG_CATEGORY, category)
                }
            }
        }
    }
}

ViewModel with validated constructor

class PaginatedViewModel(
    private val repository: ArticleRepository,
    private val category: String,
    private val pageSize: Int = 20
) : ViewModel() {

    init {
        require(category.isNotBlank()) { "Category cannot be blank" }
        require(pageSize in 5..100) { "Page size must be between 5 and 100, got $pageSize" }

        // Start loading immediately on creation
        loadFirstPage()
    }

    private fun loadFirstPage() {
        viewModelScope.launch {
            repository.getArticles(category, page = 1, size = pageSize)
        }
    }
}

Data model with multiple creation paths

data class UserProfile(
    val id: String,
    val name: String,
    val email: String,
    val avatarUrl: String?,
    val joinDate: Long,
    val isPremium: Boolean
) {
    companion object {
        // Factory functions — cleaner than secondary constructors for data classes
        fun fromApiResponse(response: UserApiResponse): UserProfile {
            return UserProfile(
                id = response.userId,
                name = response.displayName,
                email = response.emailAddress,
                avatarUrl = response.profilePictureUrl,
                joinDate = response.createdAt,
                isPremium = response.subscriptionStatus == "active"
            )
        }

        fun guest(): UserProfile {
            return UserProfile(
                id = "guest",
                name = "Guest",
                email = "",
                avatarUrl = null,
                joinDate = System.currentTimeMillis(),
                isPremium = false
            )
        }
    }
}

Common Mistakes to Avoid

Mistake 1: Forgetting val/var in primary constructor

// ❌ name and age are parameters, NOT properties
class User(name: String, age: Int)

val user = User("Alice", 25)
println(user.name)   // ❌ error — unresolved reference

// ✅ Add val or var
class User(val name: String, val age: Int)
println(user.name)   // ✅ Alice

Mistake 2: Trying to use secondary constructor body for validation

// ❌ Validation in secondary constructor body — runs AFTER init blocks
class User(val name: String) {
    constructor(name: String, age: Int) : this(name) {
        require(age >= 0)   // too late — object already created
    }
}

// ✅ Validate in init block — runs as part of primary construction
class User(val name: String, val age: Int = 0) {
    init {
        require(age >= 0) { "Age cannot be negative" }
    }
}

Mistake 3: Accessing uninitialized properties

class Broken(val size: Int) {
    val doubled = size * 2
    val message = "Size is $size and double is $doubled"  // ✅ fine — doubled initialized above

    // ❌ Don't reference properties that are declared below in the class body
    // The order of declaration matters
}

Mistake 4: Using secondary constructors when default params suffice

// ❌ Verbose and unnecessary
class Config(val host: String, val port: Int, val timeout: Int) {
    constructor(host: String) : this(host, 80, 30)
    constructor(host: String, port: Int) : this(host, port, 30)
}

// ✅ Clean and equivalent
class Config(
    val host: String,
    val port: Int = 80,
    val timeout: Int = 30
)

Summary

  • The primary constructor is defined in the class header — the most concise and common way
  • val/var in the primary constructor creates properties; parameters without them are just constructor-scoped variables
  • Use default parameter values instead of secondary constructors whenever possible
  • The init block runs initialization code in order, interleaved with property initializers
  • Use require() in init blocks to validate constructor arguments
  • Secondary constructors must delegate to the primary using this(...)
  • Initialization order: primary constructor parameters → property initializers and init blocks (top to bottom) → secondary constructor body
  • In inheritance, child class must call parent constructor with super(...) or this(...)
  • For data classes, use companion object factory functions instead of secondary constructors

Constructors are the foundation of every object in your app. Understanding them deeply — especially the initialization order — prevents subtle bugs and helps you write cleaner, more intentional code.

Happy coding!