Kotlin lateinit Keyword
last modified April 19, 2025
Kotlin's null safety system requires properties to be initialized. The
lateinit keyword allows delaying initialization while keeping
non-null types. This tutorial explores lateinit in depth with
practical examples.
Basic Definitions
The lateinit modifier marks a property for delayed initialization.
It must be declared as var (not val) and cannot be
nullable or primitive. The property must be initialized before first access.
Basic lateinit Usage
The simplest use case for lateinit is when initialization happens
after object creation but before usage. This is common in dependency injection
or test setups.
package com.zetcode
class UserService {
lateinit var username: String
fun initialize(name: String) {
username = name
}
fun greet() {
println("Hello, $username")
}
}
fun main() {
val service = UserService()
service.initialize("JohnDoe")
service.greet() // Output: Hello, JohnDoe
}
Here username is marked with lateinit and initialized
later via initialize. The property remains non-null while allowing
flexible initialization timing. Access before initialization would throw an
exception.
lateinit in Dependency Injection
lateinit is commonly used with dependency injection frameworks
where properties are set after construction. This avoids nullable types while
maintaining DI benefits.
package com.zetcode
class OrderProcessor {
lateinit var paymentGateway: PaymentGateway
fun processOrder(amount: Double) {
paymentGateway.charge(amount)
}
}
interface PaymentGateway {
fun charge(amount: Double)
}
class MockPaymentGateway : PaymentGateway {
override fun charge(amount: Double) {
println("Charged $$amount (mock)")
}
}
fun main() {
val processor = OrderProcessor()
processor.paymentGateway = MockPaymentGateway()
processor.processOrder(99.99) // Output: Charged $99.99 (mock)
}
The paymentGateway is initialized after construction but before use.
This pattern is common in Spring or other DI frameworks where wiring happens
after object creation.
lateinit in Android Activities
Android development frequently uses lateinit for views bound in
onCreate. This avoids null checks while ensuring views exist when
used.
package com.zetcode
// Simulating Android Activity
class MainActivity {
lateinit var submitButton: Button
fun onCreate() {
submitButton = Button("Submit") // Typically from findViewById()
}
fun setupButton() {
submitButton.setOnClickListener {
println("Button clicked!")
}
}
}
class Button(val text: String) {
fun setOnClickListener(action: () -> Unit) {
// Implementation would attach click handler
}
}
fun main() {
val activity = MainActivity()
activity.onCreate()
activity.setupButton()
}
This simulates Android's view binding pattern. The button is initialized in
onCreate and safely used later. Without lateinit,
we'd need nullable types or extra null checks.
lateinit vs Lazy Initialization
lateinit differs from lazy initialization. While both
delay initialization, lateinit is mutable and manual, whereas
lazy is immutable and automatic.
package com.zetcode
class Configuration {
lateinit var apiKey: String
val dbUrl by lazy { "jdbc:mysql://localhost/mydb" }
fun initialize(key: String) {
apiKey = key
}
}
fun main() {
val config = Configuration()
config.initialize("secret123")
println(config.apiKey) // Output: secret123
println(config.dbUrl) // Output: jdbc:mysql://localhost/mydb
}
apiKey must be manually initialized before use, while dbUrl
initializes automatically on first access. Choose lateinit for
mutable properties you'll initialize yourself.
Checking lateinit Initialization
Kotlin provides ::property.isInitialized to check if a
lateinit property was initialized. This is useful for validation.
package com.zetcode
class DataProcessor {
lateinit var dataSource: String
fun process() {
if (::dataSource.isInitialized) {
println("Processing data from $dataSource")
} else {
println("DataSource not initialized")
}
}
}
fun main() {
val processor = DataProcessor()
processor.process() // Output: DataSource not initialized
processor.dataSource = "database"
processor.process() // Output: Processing data from database
}
The isInitialized check prevents UninitializedPropertyAccessException.
This is safer than assuming initialization, especially in complex flows where
initialization might be conditional.
lateinit Constraints and Limitations
lateinit has several constraints. It only works with var,
can't be nullable, and can't be used with primitive types. Understanding these
limits prevents misuse.
package com.zetcode
class Example {
lateinit var name: String // Valid
// lateinit val constant: String // Error: must be var
// lateinit var age: Int // Error: primitive type
// lateinit var maybe: String? // Error: nullable type
}
fun main() {
val example = Example()
example.name = "Valid"
println(example.name)
}
The commented lines show invalid lateinit declarations. These
constraints exist because lateinit relies on runtime checks rather
than compile-time null safety. Primitive types can't be null so can't use
lateinit.
lateinit in Unit Testing
Unit tests often use lateinit for test subjects and mocks that are
initialized in setup methods. This keeps test code clean and focused.
package com.zetcode
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.mockito.Mockito.mock
import org.mockito.Mockito.verify
interface Logger {
fun log(message: String)
}
class Service(private val logger: Logger) {
fun doWork() {
logger.log("Working...")
}
}
class ServiceTest {
lateinit var service: Service
lateinit var mockLogger: Logger
@BeforeEach
fun setUp() {
mockLogger = mock(Logger::class.java)
service = Service(mockLogger)
}
@Test
fun `doWork logs message`() {
service.doWork()
verify(mockLogger).log("Working...")
}
}
This test class uses lateinit for the service and mock logger,
initializing them in setUp. This pattern is common in JUnit tests,
keeping test methods clean while avoiding nullable types.
Best Practices for lateinit
- Document initialization: Clearly document where and when
lateinitproperties should be initialized. - Prefer constructor injection: When possible, use constructor
parameters instead of
lateinitfor required dependencies. - Check initialization: Use
isInitializedwhen uncertain about initialization state. - Limit scope: Avoid
lateinitin public APIs where initialization control is unclear. - Consider alternatives: Evaluate if
lazyor nullable types might be better solutions for your use case.
Source
This tutorial covered Kotlin's lateinit keyword in depth, showing
its usage patterns and constraints. We explored practical examples in dependency
injection, Android development, testing, and more. Proper use of
lateinit can improve code readability while maintaining Kotlin's
null safety guarantees.
Author
List all Kotlin tutorials.