Mastering the Android View Lifecycle
A Developer’s Guide
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 Views — TextView, 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:
- Parses the XML layout file
→ Creates Java/KotlinViewobjects for each tag (TextView,Button, etc.) - Builds a hierarchy (the View Tree)
→ Root layout (likeConstraintLayout) becomes the parent of other views. - Measures and lays out the hierarchy.
- 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.
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()andonLayout() - 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 rawView. - 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!
