DevBits
DevBitsAndroid Handbook
Kotlin

What is the difference between lateinit and lazy?

Understand the difference between assigning a value later and creating it on first access.

KOT-002Intermediate~45 sec

What the interviewer is testing

This isn't really a question about delayed initialization.

It's about ownership.

Can you recognise when something else is responsible for providing a value, and when your own code should create it?

A strong answer explains why lateinit and lazy solve different problems instead of treating them as interchangeable ways to avoid initializing a property immediately.

❌ Common mistake

If they ask you to elaborate

lateinit is assignment-driven

Sometimes an object cannot receive a useful value when the property is declared.

Android gives us plenty of examples: dependency injection may happen after construction, a view may only exist after a lifecycle callback, or test setup may provide a dependency before the test runs.

@Inject
lateinit var repository: UserRepository

The class is saying: this value is required, but it will be supplied later.

That is why lateinit works with var. The property starts without a value and is assigned later. It also cannot be used with nullable types or primitive types; the feature exists specifically for non-null reference properties that Kotlin would otherwise require you to initialize immediately.

lazy is access-driven

With lazy, the owner already knows how to create the value:

private val adapter by lazy {
    UserAdapter()
}

Nothing else needs to assign adapter. The first access runs the initializer, and subsequent accesses return the same result.

That makes lazy useful when creating something immediately would be unnecessary work, or when the value should not exist until the code path that needs it is actually reached.

The important part is not "lazy is faster." The important part is that initialization is tied to first access instead of to a separate setup step.

Production considerations

The Android-specific trap is lifetime.

A lazy value is normally retained for as long as the object that owns the property is retained. That is fine for something whose lifetime matches the Activity, Fragment, ViewModel, or other owner. It is a poor fit for a value that needs to be cleared and recreated repeatedly.

Fragment ViewBinding is a good example. A Fragment instance can outlive its View, so keeping a binding for the whole Fragment lifetime can retain references to a destroyed View hierarchy. That is why the common Fragment pattern uses a nullable backing property that is cleared in onDestroyView() rather than a normal lazy binding that lives for the Fragment's entire lifetime.

private var _binding: FragmentProfileBinding? = null
private val binding get() = checkNotNull(_binding)

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    _binding = FragmentProfileBinding.bind(view)
}

override fun onDestroyView() {
    _binding = null
    super.onDestroyView()
}

lateinit also deserves the same lifecycle thinking. If a property can legitimately become unavailable again, simply making it lateinit does not model that state. The declaration should match the lifetime of the thing it refers to, not just avoid a nullable type.

So the production question is not merely "which keyword is shorter?" It is who supplies this value, when does it become valid, and how long should it remain valid?

Code example

Here are the two ideas side by side:

class ProfileActivity : AppCompatActivity() {

    @Inject
    lateinit var repository: ProfileRepository

    private val adapter by lazy {
        ProfileAdapter()
    }
}

repository is provided later by the injection process, so initialization happens through assignment. adapter can be created by the Activity itself, so initialization can wait until the property is first used.

The syntax is less important than the ownership difference: one value is supplied; the other is created by its owner.

Follow-up questions

What separates a senior answer

Key takeaways

Learn more

On this page