Kahibaro
Discord Login Register

4.2 Constructors

Why Constructors Matter in Kotlin Classes

When you create a class in Kotlin, you often want to control how objects of that class are created. Constructors are the special parts of a class that define how you can create (or "construct") an instance. In this chapter you focus on the different ways Kotlin lets you define constructors, how they interact with properties, and how they work with inheritance. You will use these ideas in almost every Android app, especially when modeling data or creating custom components.

Primary Constructor Basics

Kotlin has a concept called the primary constructor. It is a concise way to define how a class is created, usually right next to the class name.

A simple class with a primary constructor can look like this:

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

Here, name and age are parameters of the primary constructor. The keywords val and var make them properties of the class at the same time. That means you can write:

val user = User("Alice", 30)
println(user.name)  // prints Alice
println(user.age)   // prints 30

name is read only because it uses val. age is mutable because it uses var.

You can also have parameters that are not properties. They are available inside the class, but not as fields you can access from outside.

class Person(private val id: Int, val name: String) {
    fun printId() {
        println("ID: $id")
    }
}
val p = Person(123, "Bob")
// p.id  // Error, id is private and not a property you can access directly
p.printId()  // OK

The primary constructor itself does not contain any code. It only lists parameters. Initialization code belongs inside an init block or directly in property declarations.

`init` Blocks and Initialization Order

Sometimes you want to execute code right after an object is created, for example to validate arguments or compute derived values. In Kotlin you use init blocks for that.

class User(val name: String, val age: Int) {
    init {
        println("User created: $name, age $age")
    }
}

The init block runs whenever you create an instance.

If a class has properties with initializers and also one or more init blocks, Kotlin guarantees a specific order of execution.

Initialization happens in this order:

  1. Primary constructor parameters are passed.
  2. Property initializers are executed in the order they are declared.
  3. init blocks are executed in the order they appear in the class.

Consider:

class Example(name: String) {
    val upperName = name.uppercase()
    init {
        println("First init: $upperName")
    }
    val length = upperName.length
    init {
        println("Second init: length = $length")
    }
}

When you write Example("kotlin"), the sequence is:

  1. Parameter name is received.
  2. upperName is initialized with "KOTLIN".
  3. First init block runs, prints First init: KOTLIN.
  4. length is initialized with 6.
  5. Second init block runs, prints Second init: length = 6.

Understanding initialization order is important when your initialization logic depends on other properties.

Default Parameter Values in Constructors

Kotlin supports default values for constructor parameters. This is very useful when many values often stay the same and only a few change.

class User(
    val name: String,
    val age: Int = 18,
    val isActive: Boolean = true
)
val u1 = User("Alice")                  // age = 18, isActive = true
val u2 = User("Bob", age = 25)          // isActive = true
val u3 = User("Carol", isActive = false) // uses default age = 18

This reduces the need to write many overloaded constructors. You simply provide default values and then create objects flexibly. You can also use named arguments to make it clear which parameter you are setting.

In Android, you will often define data classes for API responses or entities with many optional fields. Defaults in the primary constructor are a clean way to handle this.

Secondary Constructors

Sometimes you need more than one way to create an object. For example, you might want to create an object from a different kind of input, or you want a special "shortcut" constructor. Kotlin lets you define secondary constructors for this.

A secondary constructor is defined inside the class body, using the constructor keyword.

class User(val name: String, val age: Int) {
    constructor(name: String) : this(name, 18) {
        println("Secondary constructor used, default age 18")
    }
}

The call User("Alice") uses the secondary constructor, which internally calls the primary one with age = 18.

Every secondary constructor must eventually delegate to the primary constructor using this(...), or to another secondary constructor that itself delegates to the primary constructor.

This rule ensures that the primary constructor and its initialization logic always run, so the class is always fully initialized. If your class has no primary constructor, a secondary constructor can directly initialize properties, but once you define a primary constructor, all others must link to it.

You can have multiple secondary constructors, for example:

class Rectangle(val width: Int, val height: Int) {
    constructor(size: Int) : this(size, size)
    constructor(width: Int, ratio: Double) : this(width, (width * ratio).toInt())
}

Here you have three ways to create a Rectangle but they all end up using the same primary constructor.

Constructors and Property Declarations

One of Kotlin's strengths is reducing boilerplate when you define properties that come from constructor parameters. You already saw this with val and var in the primary constructor. You can also combine this with custom property initialization.

For simple cases, you let the primary constructor create the properties directly:

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

For more complex cases, you might store a transformed version in a custom property:

class Person(name: String) {
    val displayName = name.trim().uppercase()
}

Here name is a constructor parameter, not a property. It is used to initialize displayName. Outside the class, you cannot access name but you can access displayName.

Sometimes you also need custom getters or setters:

class Product(price: Double) {
    var price: Double = price
        set(value) {
            require(value >= 0) { "Price must be non-negative" }
            field = value
        }
}

The constructor parameter is used to set the backing property price. The custom setter enforces a rule whenever the property changes, including the first assignment.

Visibility and Access Modifiers in Constructors

The visibility of a constructor controls where you can create instances of a class. By default, constructors are public, which means you can create the class from anywhere.

You can restrict constructor visibility, for example to enforce factory patterns or to control instance creation.

To change the visibility of the primary constructor, you must write the constructor keyword explicitly.

class Secret private constructor(val data: String) {
    companion object {
        fun create(data: String): Secret {
            return Secret(data)
        }
    }
}
val s = Secret.create("hidden")
// val s2 = Secret("hidden")  // Error, constructor is private

Here the primary constructor is private, so instances can only be created from inside the class itself. The companion object (covered elsewhere) exposes a controlled create method.

You can also use other visibility modifiers like internal or protected, but protected is only relevant in inheritance scenarios and applies to members, not to top level constructors directly.

Secondary constructors can also have their own visibility modifiers:

class User private constructor(val name: String) {
    constructor(name: String, isAdmin: Boolean) : this(name) {
        // some extra logic
    }
}

In this example, the primary constructor is private. The public secondary constructor becomes the only way to create User from outside, but it still must call the private primary constructor.

Constructors and Inheritance

When you have a class that extends another class, the subclass constructor has to call a constructor of the superclass. This connects directly to how constructors work in Kotlin.

You declare that a class inherits from another by using a colon after the class name. At that point, you must supply the arguments for the superclass constructor if it has one.

open class Animal(val name: String)
class Dog(name: String, val breed: String) : Animal(name)

Animal has a primary constructor that takes name. The Dog class primary constructor takes name and breed, and then calls the Animal constructor with name. This ensures that the superclass is properly initialized.

If the base class has multiple constructors, the primary one is the one in the class header. Secondary constructors can also be used with inheritance.

open class Shape(val color: String)
class Circle : Shape {
    val radius: Double
    constructor(radius: Double) : super("black") {
        this.radius = radius
    }
    constructor(radius: Double, color: String) : super(color) {
        this.radius = radius
    }
}

Here Circle has two secondary constructors. Each one calls a constructor of Shape using super(...). The super call must be the first statement in the secondary constructor header. You cannot call this(...) and super(...) at the same time, so each secondary constructor must choose one direct target.

Constructor Overloading vs Default Parameters

In languages like Java it is common to write many overloaded constructors to handle different parameter sets. In Kotlin you usually prefer default parameter values in the primary constructor instead of many secondary constructors.

Compare these two designs.

Many secondary constructors:

class User {
    val name: String
    val age: Int
    val isActive: Boolean
    constructor(name: String) {
        this.name = name
        this.age = 18
        this.isActive = true
    }
    constructor(name: String, age: Int) {
        this.name = name
        this.age = age
        this.isActive = true
    }
    constructor(name: String, age: Int, isActive: Boolean) {
        this.name = name
        this.age = age
        this.isActive = isActive
    }
}

Primary constructor with defaults:

class User(
    val name: String,
    val age: Int = 18,
    val isActive: Boolean = true
)

The second version is shorter, easier to read, and more flexible with named arguments. In most cases, you should start with default parameters in the primary constructor. Use secondary constructors only when you have a genuinely different construction path, for example when you need to create an object from a JSON string, a database cursor, or another specific object type.

Best Practices and Common Pitfalls

When using constructors in Kotlin, a few patterns and rules help you avoid problems.

Try to keep the primary constructor small and focused. If you find yourself passing ten or more parameters, consider grouping related data into another class. For example, replace many address fields with a single Address object. This keeps your classes easier to understand and construct.

Prefer immutable properties in constructors when possible. Use val for fields that do not need to change after creation. This makes your classes safer, especially in multithreaded environments or when they represent stable data from a server or a database.

Avoid heavy work in constructors and init blocks. Constructing an object should usually be quick. Expensive operations, such as network calls or database queries, belong in separate methods or in other parts of your architecture. This is very important in Android, because heavy work in constructors can indirectly block the main thread.

Be careful with initialization order. If an init block reads a property, make sure that property is declared before the init block, or that you understand the default initialization. If you access something before it is initialized, you might see unexpected values or runtime errors.

Use constructor visibility to control how objects are created when needed. This is useful when you want to enforce that objects are always created with some validation logic or through a factory. A private or internal constructor, combined with a helper function, gives you control over instance creation.

Finally, remember that in inheritance hierarchies, constructors must cooperate. The subclass constructor is responsible for passing the correct arguments to the superclass. Design the base class constructors carefully, because they affect how all subclasses must be written.

With these ideas, you can design classes that are clear to use, safe to construct, and flexible enough for real Android projects.

Views: 2

Comments

Please login to add a comment.

Don't have an account? Register now!