Question

Fixing NullPointerException When Accessing Views in a Kotlin Fragment

kotlinandroidkotlin-android-extensions

Question

I am using Kotlin Android Extensions inside an Android Fragment, and I get a NullPointerException when I try to access a view inside onCreateView().

The crash says:

Caused by: java.lang.NullPointerException: Attempt to invoke virtual
method 'android.view.View android.view.View.findViewById(int)' on a
null object reference

Here is the fragment code:

package com.obaied.testrun.Fragment

import android.os.Bundle
import android.support.v4.app.Fragment
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import com.obaied.acaan.R
import kotlinx.android.synthetic.main.fragment_card_selector.*

class CardSelectorFragment : Fragment() {
    val TAG = javaClass.canonicalName

    companion object {
        fun newInstance(): CardSelectorFragment {
            return CardSelectorFragment()
        }
    }

    override fun onCreateView(
        inflater: LayoutInflater?,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        val rootView = inflater?.inflate(R.layout.fragment_card_selector, container, false)
        btn_K.setOnClickListener {
            Log.d(TAG, "onViewCreated(): hello world")
        }

        return rootView
    }
}

How should Kotlin Android Extensions be used correctly with Fragments, and why does this crash happen?

Short Answer

By the end of this page, you will understand why Fragment views can be null during the wrong lifecycle stage, how Kotlin Android Extensions behaved in Fragments, and how to safely access views using the Fragment's root view or modern View Binding.

Concept

In Android, a Fragment has two related but different lifecycles:

  • the Fragment lifecycle itself
  • the Fragment's View lifecycle

This distinction is the reason many beginners see NullPointerException when working with UI elements in Fragments.

When onCreateView() runs, you are responsible for creating and returning the Fragment's view hierarchy. Until that view is properly created and associated with the Fragment, direct access to synthetic view properties like btn_K may fail.

In your example, this line is the problem:

btn_K.setOnClickListener { ... }

The synthetic property tries to find btn_K from the Fragment's current root view. But inside onCreateView(), that root view is not yet fully attached as the Fragment's active view, so the lookup can happen against null, causing a crash.

This matters because Fragments are widely used in:

  • multi-screen Android apps
  • tab layouts
  • navigation components
  • reusable UI sections

If you access views at the wrong time, your app becomes fragile and crashes during screen creation, recreation, or navigation.

Also, Kotlin Android Extensions are now deprecated. In modern Android development, View Binding is the recommended approach because it is type-safe and works clearly with the Fragment view lifecycle.

Mental Model

Think of a Fragment like a manager of a room, and the Fragment's View as the furniture inside that room.

  • The manager (Fragment) may exist before the furniture is fully set up.
  • onCreateView() is when you start assembling the furniture.
  • Only after the room is ready should you safely interact with a chair or table (Button, TextView, etc.).

If you try to sit on a chair before it has been placed in the room, you get an error.

That is what happens when you access a view too early in a Fragment: the UI element is being requested before the Fragment has a valid view hierarchy to search in.

Take Quiz

Syntax and Examples

Correct approach with the inflated root view

If you are using the older synthetic access pattern, access views from the inflated rootView inside onCreateView().

override fun onCreateView(
    inflater: LayoutInflater,
    container: ViewGroup?,
    savedInstanceState: Bundle?
): View {
    val rootView = inflater.inflate(R.layout.fragment_card_selector, container, false)

    rootView.btn_K.setOnClickListener {
        Log.d(TAG, "hello world")
    }

    return rootView
}

Here, btn_K is resolved from rootView, not from the Fragment before its view is ready.

Better lifecycle-based approach: use onViewCreated()

A safer pattern is to inflate the layout in onCreateView() and work with child views in onViewCreated().

override fun onCreateView(
    inflater: LayoutInflater,
    container: ViewGroup?,
    savedInstanceState: Bundle?
): View {
    return inflater.inflate(R.layout.fragment_card_selector, container, false)
}

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)

    btn_K.setOnClickListener {
        Log.d(TAG, "hello world")
    }
}

At this point, the Fragment's view exists, so the synthetic property can find the button.

Modern recommended approach: View Binding

class CardSelectorFragment : Fragment() {

    private var _binding: FragmentCardSelectorBinding? = null
    private val binding get() = _binding!!

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        _binding = FragmentCardSelectorBinding.inflate(inflater, container, false)
        return binding.root
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        binding.btnK.setOnClickListener {
            Log.d("CardSelectorFragment", "hello world")
        }
    }

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

This is clearer and avoids many lifecycle-related mistakes.

Why rootView.btn_K worked historically

With Kotlin Android Extensions, importing:

import kotlinx.android.synthetic.main.fragment_card_selector.view.*

allowed you to access views directly from a View object:

rootView.btn_K

That pattern uses the specific inflated view instead of depending on the Fragment's internal view reference.

Step by Step Execution

Consider this code:

override fun onCreateView(
    inflater: LayoutInflater,
    container: ViewGroup?,
    savedInstanceState: Bundle?
): View {
    val rootView = inflater.inflate(R.layout.fragment_card_selector, container, false)
    return rootView
}

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)

    btn_K.setOnClickListener {
        Log.d("CardSelectorFragment", "clicked")
    }
}

What happens step by step

  1. onCreateView() is called.
  2. The layout file fragment_card_selector.xml is inflated into a real View object.
  3. That View is returned to the Fragment system.
  4. The Fragment now officially has a created view hierarchy.
  5. onViewCreated() is called with that view.
  6. btn_K can now be found safely because the Fragment's view exists.
  7. The click listener is attached.
  8. When the button is tapped, the log message runs.

What went wrong in the original code

In the original version:

val rootView = inflater?.inflate(...)
btn_K.setOnClickListener { ... }

The button is accessed through the Fragment synthetic property before the Fragment has a valid attached view reference. The synthetic lookup internally depends on the Fragment's root view. Since that view is still effectively unavailable through the Fragment at that moment, the lookup fails and causes NullPointerException.

Real World Use Cases

Fragments and safe view access appear in many real Android scenarios:

  • Navigation screens: setting click listeners after a screen layout is created
  • Tabbed interfaces: each tab often uses a Fragment with its own view lifecycle
  • Form screens: accessing EditText, Button, and validation messages safely
  • RecyclerView host screens: wiring adapters after the layout exists
  • API result screens: updating labels, loading states, and retry buttons in onViewCreated()

Example: a login Fragment often inflates the layout in onCreateView() and sets listeners in onViewCreated():

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)

    binding.loginButton.setOnClickListener {
        // validate input and submit
    }
}

This keeps UI setup aligned with the view lifecycle.

Take Quiz

Real Codebase Usage

In real Android projects, developers usually follow a few consistent patterns.

1. Inflate in onCreateView(), bind in onViewCreated()

This is the most common pattern because it separates:

  • view creation from
  • view setup

2. Use View Binding instead of synthetic properties

Synthetic properties were convenient, but they hid lifecycle details. View Binding makes the view reference explicit.

3. Clear view references in onDestroyView()

Fragments can outlive their views. If you keep references too long, you risk memory leaks.

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

4. Use guard clauses when binding may be unavailable

In async code, developers often avoid touching views if the Fragment view has already been destroyed.

if (_binding == null) return
binding.statusText.text = "Loaded"

5. Keep UI logic near lifecycle-safe callbacks

Typical places:

  • onViewCreated() for listeners and adapters
  • onStart() for UI work tied to visibility
  • onDestroyView() for cleanup

This reduces crashes caused by invalid view access.

Common Mistakes

1. Accessing Fragment views too early

Broken example:

override fun onCreateView(
    inflater: LayoutInflater,
    container: ViewGroup?,
    savedInstanceState: Bundle?
): View? {
    val rootView = inflater?.inflate(R.layout.fragment_card_selector, container, false)
    btn_K.setOnClickListener { }
    return rootView
}

Why it fails:

  • btn_K is being resolved from the Fragment before its view is fully ready.

Fix:

  • use rootView.btn_K, or
  • move setup to onViewCreated()

2. Forgetting the .view.* synthetic import when using rootView

If you want to do this:

rootView.btn_K

you needed the correct import:

import kotlinx.android.synthetic.main.fragment_card_selector.view.*

Without it, the property is not available on View.

3. Keeping binding after the view is destroyed

Broken example:

private lateinit var binding: FragmentCardSelectorBinding

This can be risky in Fragments if the view is recreated.

Safer pattern:

private var _binding: FragmentCardSelectorBinding? = null
private val binding get() = _binding!!

Then clear it in onDestroyView().

4. Using nullable inflater unnecessarily

Older code often uses nullable parameters:

inflater: LayoutInflater?

In modern Android APIs, inflater is non-null in onCreateView().

Prefer:

inflater: LayoutInflater

5. Updating views after async work when the view no longer exists

Broken example:

someApiCall {
    binding.textView.text = "Done"
}

If the Fragment's view was destroyed before the callback finishes, this can crash.

Fix by checking lifecycle state or whether binding is still available.

Comparisons

ApproachHow it worksProsCons
Kotlin synthetic properties in FragmentAccess views like btn_K directlyShort syntaxDeprecated, hides lifecycle issues
rootView.findViewById()Find views manually from inflated rootExplicit and safe in onCreateView()More boilerplate
rootView.btn_K with synthetic .view.* importAccess synthetic properties from the actual ViewCleaner than findViewById()Still deprecated
View BindingGenerated binding class gives typed referencesRecommended, type-safe, lifecycle-friendlySlightly more setup

onCreateView() vs onViewCreated()

MethodPurposeBest used for
onCreateView()Create and return the Fragment's layoutInflating the layout
onViewCreated()Work with already-created viewsSetting listeners, adapters, and UI state

Synthetic properties vs View Binding

FeatureSynthetic PropertiesView Binding
StatusDeprecatedRecommended
Type safetyLimitedStrong
Lifecycle clarityLowerHigher
Fragment safetyEasy to misuseEasier to manage correctly
Take Quiz

Cheat Sheet

Fragment view access quick reference

  • A Fragment is not the same thing as its View
  • Inflate layout in onCreateView()
  • Access child views safely in onViewCreated()
  • Prefer View Binding in modern Android
  • Clear Fragment binding in onDestroyView()

Safe patterns

override fun onCreateView(
    inflater: LayoutInflater,
    container: ViewGroup?,
    savedInstanceState: Bundle?
): View {
    return inflater.inflate(R.layout.fragment_card_selector, container, false)
}

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)
    btn_K.setOnClickListener { }
}
val rootView = inflater.inflate(R.layout.fragment_card_selector, container, false)
rootView.findViewById<Button>(R.id.btn_K).setOnClickListener { }
return rootView
private var _binding: FragmentCardSelectorBinding? = null
private val binding get() = _binding!!

Rules to remember

  • Do not assume Fragment views exist before they are created
  • Do not keep view references after onDestroyView()
  • Do not use deprecated Kotlin Android Extensions in new code
  • Use onViewCreated() for UI setup whenever possible

FAQ

Why does a Fragment get NullPointerException when accessing a button?

Because the Fragment's view hierarchy may not be ready yet. Accessing a view before the Fragment has a valid root view causes the lookup to fail.

Should I use onCreateView() or onViewCreated() for setting click listeners?

Usually onViewCreated() is better because the view has already been created and is safe to use.

Are Kotlin Android synthetic properties still recommended?

No. They are deprecated. Use View Binding in modern Android projects.

Why do Fragments need onDestroyView() cleanup?

A Fragment can remain alive after its view is destroyed. Clearing binding prevents memory leaks and invalid view access.

Can I use findViewById() in a Fragment?

Yes. You can call it on the inflated root view or on the view passed to onViewCreated().

What is the safest modern way to access views in a Fragment?

View Binding is the recommended approach because it is explicit, type-safe, and fits the Fragment view lifecycle.

Why did rootView.btn_K work but btn_K crash?

rootView.btn_K accesses the button from the actual inflated layout object. btn_K on the Fragment depends on the Fragment's own current view reference, which may not be ready yet.

Take Quiz

Mini Project

Description

Build a simple Fragment that shows a button and a status message. When the user taps the button, the status text changes from Ready to Clicked. This project demonstrates the correct way to inflate a Fragment layout, access views safely, and clean up binding properly.

Goal

Create a Fragment that uses View Binding to safely interact with its views without causing lifecycle-related crashes.

Requirements

  • Create a Fragment layout with a Button and a TextView.
  • Inflate the layout using View Binding in onCreateView().
  • Set the button click listener in onViewCreated().
  • Update the TextView when the button is tapped.
  • Clear the binding reference in onDestroyView().
Take Quiz

Keep learning

Related questions

View more