Sitemap

Mastering the Android View Lifecycle

A Developer’s Guide

9 min readOct 13, 2025

--

Press enter or click to view image in full size

Understanding the lifecycle isn’t just about memorizing callbacks — it’s about building apps that feel alive, responsive, and bug-free.

“Everything you see on the screen in Android is a View.”
– Every Android developer, at some point.

If you’ve built Android UIs, you’ve already been working with ViewsTextView, ImageView, Button, and so on. But when you start creating custom views or debugging rendering issues, you realize:

“Wait… how exactly does a View come to life?”

Let’s take a deep dive into the View Lifecycle, the process through which your view is created, measured, drawn, interacted with, and destroyed.

🚀 The Birth of a View: From XML to Screen

When your activity or fragment sets a layout using:

setContentView(R.layout.activity_main)

Android doesn’t just display it magically. It actually:

  1. Parses the XML layout file
    → Creates Java/Kotlin View objects for each tag (TextView, Button, etc.)
  2. Builds a hierarchy (the View Tree)
    → Root layout (like ConstraintLayout) becomes the parent of other views.
  3. Measures and lays out the hierarchy.
  4. Draws everything onto the screen.

This is where the View lifecycle steps in.

What Exactly Is the View Lifecycle?

At its core, the Android View Lifecycle is the series of states and callbacks that a View goes through from creation to destruction. Think of it as the “life story” of every UI component in your app — each view is born, lives, and eventually dies, with several important milestones in between.

Unlike Activities or Fragments which have their own lifecycles, Views have a simpler yet equally important lifecycle that directly impacts how your UI behaves, performs, and manages resources.

Press enter or click to view image in full size
View Life cycle along with Activity

The View Lifecycle States: A Journey

Let’s walk through what happens to a View from the moment you declare it in XML or create it programmatically:

inflate() → onAttachedToWindow() → onMeasure() → onLayout() → onDraw()

(user interacts)

onDetachedFromWindow()

🧱 1. Constructor Phase: Birth

Every View starts here. When you instantiate a view, the constructor is called:

// Three possible constructors
class CustomView(context: Context) : View(context)
class CustomView(context: Context, attrs: AttributeSet?) : View(context, attrs)
class CustomView(context: Context, attrs: AttributeSet?, defStyleAttr: Int)
: View(context, attrs, defStyleAttr)

What’s happening: The View object is created in memory. At this point, it’s just a Java/Kotlin object — no measurements, no positioning, nothing visual yet.

🔍 Pro tip: This is where you should initialize paint objects, read custom attributes, and set up any non-UI related properties.

🔗 2. onAttachedToWindow(): Finding a Home

override fun onAttachedToWindow() {
super.onAttachedToWindow()
// View is now part of the window
// Safe to start animations or register listeners
}

What’s happening: Your View has been added to the view hierarchy and is now attached to a window. It has access to resources and can interact with the window system.

🔍 Tip: This is a great place to register global listeners (sensor listeners, broadcast receivers) or start animations.
Just remember to unregister them in onDetachedFromWindow().

📏 3. onMeasure(): Sizing Up

Next comes the sizing phase.
The parent asks: “Hey, how big do you want to be?”
The child replies: “Well, based on your constraints, here’s what works for me.”

This negotiation happens in onMeasure().

override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
val size = 200
setMeasuredDimension(size, size)
}

What’s happening: The parent View is asking, “How much space do you need?”

🔍 Key concept:
MeasureSpec defines constraints like:

  • EXACTLY → parent says, “Be this size.”
  • AT_MOST → parent says, “Be as big as you want, up to this limit.”
  • UNSPECIFIED → parent doesn’t care.

Important: Always call setMeasuredDimension() or you'll get an exception!

📐 4. onLayout() – Placing Children (Only in ViewGroups)

If your custom view extends ViewGroup (like a custom layout),
this is where you position your child views.

override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
super.onLayout(changed, left, top, right, bottom)

// Position child views if you're a ViewGroup
// Calculate dimensions that depend on final size

if (changed) {
// Size or position changed - recalculate complex layouts
initializeDrawingObjects()
}
}

What’s happening: Your View now knows its exact position within its parent. The coordinates (left, top, right, bottom) tell you where you are in the view hierarchy.

The changed parameter: This boolean tells you if the size or position has changed since the last layout. Use it to avoid unnecessary recalculations.

🔍 Tip: Simple views (like a View that draws itself) don’t implement this, only containers do.

🎨 5. onDraw() – The Artist at Work

The canvas stage.
Here, you paint your pixels. Everything the user sees is drawn here.

override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)

// Draw your custom content
canvas.drawCircle(
width / 2f,
height / 2f,
min(width, height) / 2f,
paint
)

canvas.drawText(
"Hello, View!",
width / 2f,
height / 2f,
textPaint
)
}

What’s happening: The system gives you a Canvas object and says, “Draw yourself!” This is called every time your View needs to render.

🔍 Performance tip: onDraw() is called frequently (potentially 60 times per second for animations). Never allocate objects here! Create Paint objects and other resources in the constructor or onSizeChanged().

👆 6. onTouchEvent() – When the User Talks to You

❌ 6. onDetachedFromWindow() – Cleanup Time

override fun onDetachedFromWindow() {
super.onDetachedFromWindow()

// Clean up resources
stopAnimations()
unregisterListeners()
releaseResources()
}

What’s happening: Your View is being removed from the window. This is your chance to clean up.

Critical for: Preventing memory leaks by removing listeners, stopping animations, and releasing heavy resources.

🧠 The Internal Drawing Pipeline

To tie it all together, here’s what happens under the hood when Android renders:

ViewRootImpl → performTraversals()
├─ measure()
│ └─ onMeasure()
├─ layout()
│ └─ onLayout()
└─ draw()
├─ drawBackground()
├─ onDraw()
└─ dispatchDraw() (for child views)

Real-World Example: A Custom Loading View

Let’s build a practical example that demonstrates the lifecycle in action:

class PulsingLoadingView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
private val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = Color.BLUE
style = Paint.Style.FILL
}

private var radius = 0f
private var animator: ValueAnimator? = null
private var centerX = 0f
private var centerY = 0f
init {
// Constructor: Initialize paint and properties
setupAnimator()
}
private fun setupAnimator() {
animator = ValueAnimator.ofFloat(0f, 1f).apply {
duration = 1000
repeatCount = ValueAnimator.INFINITE
repeatMode = ValueAnimator.REVERSE

addUpdateListener { animation ->
val fraction = animation.animatedValue as Float
// This will trigger onDraw()
invalidate()
}
}
}
override fun onAttachedToWindow() {
super.onAttachedToWindow()
// Start animating when attached
animator?.start()
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
val desiredSize = 100.dpToPx()

val width = resolveSize(desiredSize, widthMeasureSpec)
val height = resolveSize(desiredSize, heightMeasureSpec)

setMeasuredDimension(width, height)
}
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)

// Calculate center and max radius based on actual size
centerX = w / 2f
centerY = h / 2f
radius = min(w, h) / 4f
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)

// Get current animation value
val fraction = animator?.animatedValue as? Float ?: 0f
val currentRadius = radius * (0.5f + fraction * 0.5f)

// Draw pulsing circle
canvas.drawCircle(centerX, centerY, currentRadius, paint)
}
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
// Clean up: stop animation to prevent leaks
animator?.cancel()
}
private fun Int.dpToPx(): Int {
return (this * resources.displayMetrics.density).toInt()
}
}

Common Pitfalls and How to Avoid Them

1. Memory Leaks from Listeners

Wrong:

override fun onAttachedToWindow() {
super.onAttachedToWindow()
EventBus.register(this)
// Never unregistered!
}

Correct:

override fun onAttachedToWindow() {
super.onAttachedToWindow()
EventBus.register(this)
}

override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
EventBus.unregister(this)
}

2. Object Allocation in onDraw()

Wrong:

override fun onDraw(canvas: Canvas) {
val paint = Paint() // New object every frame!
canvas.drawCircle(x, y, radius, paint)
}

Correct:

private val paint = Paint()

override fun onDraw(canvas: Canvas) {
canvas.drawCircle(x, y, radius, paint)
}

3. Ignoring Configuration Changes

Wrong:

override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
setMeasuredDimension(200, 200) // Fixed size, ignores constraints
}

Correct:

override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
val size = resolveSize(200, widthMeasureSpec)
setMeasuredDimension(size, size)
}

Advanced: ViewGroup Lifecycle

When you create custom ViewGroups, the lifecycle becomes more complex because you’re managing children:

class CustomViewGroup(context: Context, attrs: AttributeSet?) 
: ViewGroup(context, attrs) {
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
// First, measure all children
children.forEach { child ->
measureChild(child, widthMeasureSpec, heightMeasureSpec)
}

// Then, determine our own size based on children
val desiredWidth = children.sumOf { it.measuredWidth }
val desiredHeight = children.maxOf { it.measuredHeight }

setMeasuredDimension(
resolveSize(desiredWidth, widthMeasureSpec),
resolveSize(desiredHeight, heightMeasureSpec)
)
}
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
var currentLeft = paddingLeft

// Position each child
children.forEach { child ->
if (child.visibility != GONE) {
child.layout(
currentLeft,
paddingTop,
currentLeft + child.measuredWidth,
paddingTop + child.measuredHeight
)
currentLeft += child.measuredWidth
}
}
}
}

Best Practices: Your Lifecycle Checklist

Always clean up in onDetachedFromWindow()

  • Cancel animations
  • Unregister listeners
  • Release heavy resources

Use onSizeChanged() for size-dependent calculations

  • It’s called after onMeasure() and onLayout()
  • Perfect for calculating drawing coordinates

Never allocate objects in onDraw()

  • Create reusable objects in the constructor or init block
  • Use object pools for complex scenarios

Respect MeasureSpec in onMeasure()

  • Use resolveSize() or properly handle all three modes
  • Always call setMeasuredDimension()

Call invalidate() to trigger redraw

  • Use invalidate() for UI-thread updates
  • Use postInvalidate() from background threads

Call requestLayout() when size changes

  • Forces a new measure/layout pass
  • Use when your View’s size requirements change

🧩 Case 1: Your custom view inflates a layout (XML)

If your view is composed of existing UI elements (e.g., TextView, ImageView, etc.) and you’re inflating a layout XMLinside your custom class, then you do not need to override onDraw().

class ProfileCardView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null
) : FrameLayout(context, attrs) {

init {
LayoutInflater.from(context).inflate(R.layout.view_profile_card, this, true)
}
}

Here’s what happens:

  • You’re extending a ViewGroup (FrameLayout), not a raw View.
  • The layout inflation handles all child views and their rendering.
  • The system calls each child’s onDraw() automatically you don’t need to draw anything yourself.

No need for onDraw()
Don’t override it : in fact, doing so may cause unnecessary overdraw or even block child rendering if you forget to call super.onDraw().

🧩 Case 2: Your view draws its own custom graphics

If your custom view doesn’t inflate a layout and instead renders shapes, text, or images manually using the Canvas API — then you must override onDraw().

Example:

class CircleView(context: Context) : View(context) {
private val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = Color.RED
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
canvas.drawCircle(width / 2f, height / 2f, width / 2f, paint)
}
}

onDraw() required because you’re handling rendering manually.

💡 In short:
If your custom view uses XML + existing views, you don’t need onDraw().
If your custom view creates its own visuals, you do.

Debugging Lifecycle Issues

Enable layout bounds in Developer Options to visualize your View hierarchy. Also, add logging to track lifecycle callbacks:

override fun onAttachedToWindow() {
super.onAttachedToWindow()
Log.d(TAG, "onAttachedToWindow")
}

override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
Log.d(TAG, "onMeasure - width: ${MeasureSpec.toString(widthMeasureSpec)}")
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
}
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
Log.d(TAG, "onLayout - changed: $changed, bounds: ($left,$top,$right,$bottom)")
super.onLayout(changed, left, top, right, bottom)
}

Conclusion: Respect the Lifecycle

🧭 Final Thoughts

The View Lifecycle isn’t just about when things happen, it’s about understanding why they happen.

Knowing these steps helps you:

  • Build smooth, custom animations.
  • Optimize rendering and performance.
  • Avoid UI glitches and memory leaks.

So the next time your custom view doesn’t look right, remember:

Measure → Layout → Draw
That’s the holy trinity of rendering.

Every callback exists for a reason, and respecting that design leads to better apps.

Wrapping Up:

  • Constructor: Initialize
  • onAttachedToWindow(): Start resources
  • onMeasure(): Calculate size
  • onLayout(): Position elements
  • onDraw(): Render content
  • onDetachedFromWindow(): Clean up

Master these fundamentals, and you’ll find that complex custom views become not just possible, but enjoyable to build. Your users won’t see the lifecycle but they’ll definitely feel the difference in how your app performs.

Happy coding! 🚀

Have you encountered interesting lifecycle challenges in your custom views? Share your experiences in the comments below!

--

--

Android Dev Nexus
Android Dev Nexus

Written by Android Dev Nexus

Your friendly neighbourhood developers working at PayPal, turning Android fundamentals into concepts you’ll actually understand. A new post every Tue and Fri.

No responses yet