Question

What to Use Instead of Deprecated Handler() in Android Kotlin

kotlinjavaandroidandroid-handler

Question

In Android Kotlin, how can I fix the deprecation warning in the following code? Also, what are the recommended alternatives for delaying code execution?

Handler().postDelayed({
    context?.let {
        // code
    }
}, 3000)

Short Answer

By the end of this page, you will understand why Handler() was deprecated, how to replace it correctly using Handler(Looper.getMainLooper()), and when better alternatives such as lifecycleScope, coroutines, or view.postDelayed() are more appropriate in modern Android code.

Concept

In Android, a Handler is used to schedule work to run on a specific thread's message queue. For many years, developers could write Handler() with no arguments, and Android would attach it to the current thread's Looper.

That behavior was deprecated because it is easy to misuse:

  • Not every thread has a Looper
  • It may not be obvious which thread the Handler is attached to
  • It can lead to bugs, crashes, or delayed code running on the wrong thread

The modern approach is to be explicit about which thread should run the code.

For example, if you want to run code on the main UI thread after a delay, use:

Handler(Looper.getMainLooper()).postDelayed({
    // code
}, 3000)

This makes your intent clear: the delayed block should run on the main thread.

In modern Android development, however, Handler is often no longer the best first choice. Depending on the situation, you may prefer:

  • Coroutines with delay() for lifecycle-aware async work
  • view.postDelayed() when the work is tied to a specific View
  • lifecycleScope.launch in activities and fragments

This matters because delayed work often outlives the screen that started it. If you use the wrong tool, you can create memory leaks, update destroyed views, or run code after a fragment/activity is gone.

Mental Model

Think of a Looper as a worker's inbox and a Handler as the assistant who places tasks into that inbox.

  • The thread is the worker
  • The Looper is the worker's task queue
  • The Handler submits tasks to that queue
  • postDelayed() says: "Put this task in the queue, but wait 3 seconds first"

The old Handler() constructor was like saying, "Give this task to whoever happens to be nearby." That is risky.

Handler(Looper.getMainLooper()) is like saying, "Give this task specifically to the main UI worker." That is clearer and safer.

Take Quiz

Syntax and Examples

Basic replacement for deprecated Handler()

If you want to run delayed code on the main thread:

Handler(Looper.getMainLooper()).postDelayed({
    context?.let {
        // code
    }
}, 3000)

You can also write it with a trailing lambda:

Handler(Looper.getMainLooper()).postDelayed(3000) {
    context?.let {
        // code
    }
}

Using view.postDelayed()

If the delayed work belongs to a specific view:

myButton.postDelayed({
    myButton.text = "Done"
}, 3000)

This is convenient when the action is clearly related to that view.

Using coroutines in a Fragment or Activity

Modern Android often uses coroutines instead:

lifecycleScope.launch {
    delay(3000)
    context?.let {
        // code
    }
}

This is often easier to read, especially when you already use coroutines.

Why these are better

  • Handler(Looper.getMainLooper()) is explicit about the thread
  • view.postDelayed() is concise for UI-related work
  • Coroutines are easier to cancel and manage with lifecycle-aware scopes

Step by Step Execution

Consider this code:

Handler(Looper.getMainLooper()).postDelayed({
    println("Run after 3 seconds")
}, 3000)

Here is what happens step by step:

  1. Looper.getMainLooper() gets the main thread's message queue.
  2. Handler(...) creates a handler attached to that queue.
  3. postDelayed(...) schedules the lambda to run later.
  4. 3000 means the task should wait about 3,000 milliseconds.
  5. The main thread continues doing other work.
  6. After about 3 seconds, the main thread processes the queued task.
  7. The lambda runs and prints Run after 3 seconds.

Trace example

println("Start")

Handler(Looper.getMainLooper()).postDelayed({
    println("Delayed")
}, 3000)

println("End")

Output order:

Start
End
Delayed

Why?

  • Start prints immediately
  • The delayed block is scheduled, not run right away
  • End prints immediately after scheduling
  • Delayed prints after about 3 seconds

Real World Use Cases

Delayed execution is common in Android apps. Some practical examples are:

  • Splash screens or short transitions
    • Wait briefly before moving to another screen
  • Temporary messages
    • Hide a banner, tooltip, or loading state after a few seconds
  • Debouncing UI actions
    • Delay search requests until the user pauses typing
  • Retry logic
    • Wait before retrying a failed operation
  • UI feedback
    • Show success text, then revert after a delay

Example: hide a message after 3 seconds

textView.text = "Saved"
textView.postDelayed({
    textView.text = ""
}, 3000)

Example: coroutine-based retry delay

lifecycleScope.launch {
    try {
        api.loadData()
    } catch (e: Exception) {
        delay(2000)
        api.loadData()
    }
}
Take Quiz

Real Codebase Usage

In real Android projects, developers choose the delayed-execution tool based on ownership and lifecycle.

Common patterns

1. Explicit main-thread Handler

Used when working with Android framework code or older codebases:

private val handler = Handler(Looper.getMainLooper())

fun scheduleWork() {
    handler.postDelayed({
        // update UI
    }, 3000)
}

2. Remove callbacks to avoid leaks

If delayed work should not run after a screen is destroyed:

private val handler = Handler(Looper.getMainLooper())
private val runnable = Runnable {
    // work
}

override fun onStart() {
    super.onStart()
    handler.postDelayed(runnable, 3000)
}

override fun onStop() {
    super.onStop()
    handler.removeCallbacks(runnable)
}

3. Lifecycle-aware coroutines

This is very common in modern apps:

viewLifecycleOwner.lifecycleScope.launch {
    delay(3000)
    // safe UI work while lifecycle is active
}

4. Guard clauses before UI work

Developers often check whether the fragment is still attached or the view still exists:

Handler(Looper.getMainLooper()).postDelayed({
    val ctx = context ?: return@postDelayed
    // use ctx
}, 3000)

5. View-bound scheduling

If the work belongs to a specific UI element:

recyclerView.postDelayed({
    recyclerView.scrollToPosition(0)
}, 500)

This keeps the code close to the object it affects.

Common Mistakes

1. Using deprecated Handler()

Broken code:

Handler().postDelayed({
    // code
}, 3000)

Why it is a problem:

  • It does not clearly state which thread should run the code
  • It triggers a deprecation warning

Fix:

Handler(Looper.getMainLooper()).postDelayed({
    // code
}, 3000)

2. Forgetting to import Looper

If you use Looper.getMainLooper(), make sure this import exists:

import android.os.Looper

3. Updating UI after a Fragment or Activity is gone

Broken pattern:

Handler(Looper.getMainLooper()).postDelayed({
    textView.text = "Done"
}, 3000)

If the screen is destroyed before 3 seconds pass, this can cause problems.

Safer options:

  • Cancel the callback in onStop() or onDestroyView()
  • Use lifecycle-aware coroutines

4. Creating delays on the wrong thread

If you attach a handler to a background thread accidentally, UI updates may fail.

Use the main looper for UI work:

Handler(Looper.getMainLooper())

5. Using context without checking nullability

Safer version:

Handler(Looper.getMainLooper()).postDelayed({
    val ctx = context ?: return@postDelayed
    // use ctx
}, 3000)

6. Not cancelling delayed work

If the work should no longer happen, remove it:

handler.removeCallbacks(runnable)

Without this, delayed code may run at the wrong time.

Comparisons

OptionBest forLifecycle-awareEasy to cancelNotes
Handler(Looper.getMainLooper()).postDelayed()Simple delayed work on main threadNoYes, with removeCallbacksGood direct replacement for deprecated Handler()
view.postDelayed()Work tied to a specific ViewNoLimited, depends on usageShort and convenient for UI elements
lifecycleScope.launch { delay(...) }Modern Android screensYesYesPreferred in many Kotlin Android apps
GlobalScope.launch { delay(...) }Rarely appropriateNoHarder to manageUsually avoid in UI code

Handler vs coroutines

  • Use Handler when you need a direct Android message-queue API or are maintaining existing code.
  • Use coroutines when your app already uses Kotlin async patterns and lifecycle-aware scopes.

Handler(Looper.getMainLooper()) vs view.postDelayed()

  • Use Handler(Looper.getMainLooper()) when the task is generally tied to the main thread.
  • Use view.postDelayed() when the task is specifically about that view.

Which one should you pick?

A practical rule:

  • Need a quick drop-in replacement? Use Handler(Looper.getMainLooper())
  • Working in a Fragment/Activity with coroutines? Use lifecycleScope
  • Updating one view after a delay? Use view.postDelayed()
Take Quiz

Cheat Sheet

// Deprecated
Handler()

// Correct explicit replacement
Handler(Looper.getMainLooper())

// Delayed execution
Handler(Looper.getMainLooper()).postDelayed({
    // code
}, 3000)

// Kotlin trailing lambda style
Handler(Looper.getMainLooper()).postDelayed(3000) {
    // code
}

// View-based delay
myView.postDelayed({
    // code
}, 3000)

// Coroutine-based delay
lifecycleScope.launch {
    delay(3000)
    // code
}

Rules to remember

  • Handler() with no arguments is deprecated
  • Be explicit about the Looper
  • Use Looper.getMainLooper() for UI work
  • Cancel delayed callbacks if the screen can go away
  • Prefer lifecycle-aware coroutines in modern Android apps

Useful imports

import android.os.Handler
import android.os.Looper
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch

Edge cases

  • context in a Fragment may be null
  • A delayed callback can run after the UI is destroyed
  • postDelayed() time is in milliseconds

FAQ

Why was Handler() deprecated in Android?

Because it was unclear which thread's Looper it would use, making bugs and misuse more likely.

What is the direct replacement for deprecated Handler()?

Use Handler(Looper.getMainLooper()) if the code should run on the main thread.

Should I use Handler or coroutines in Kotlin Android?

If you already use coroutines, lifecycleScope.launch { delay(...) } is often the better modern choice.

Is view.postDelayed() better than Handler?

It can be, if the delayed work is clearly tied to one specific view.

Can postDelayed() cause memory leaks?

Yes. If the callback holds references to a destroyed activity, fragment, or view, it can cause leaks or invalid UI updates.

How do I cancel a delayed Handler task?

Store the Runnable, then call handler.removeCallbacks(runnable).

Can I update the UI from any Handler?

Only if that Handler is attached to the main thread's Looper.

Is context?.let {} enough to make delayed code safe?

Not always. It only checks whether context is non-null at that moment. You may still need lifecycle-aware cancellation.

Take Quiz

Mini Project

Description

Build a small Android Fragment feature that shows a temporary status message for 3 seconds, then clears it automatically. This demonstrates how to replace deprecated Handler() usage with a safe, explicit approach and how to cancel delayed work when the view goes away.

Goal

Create a Fragment that displays a message, clears it after 3 seconds, and avoids running the callback after the view is destroyed.

Requirements

  • Create a Handler using Looper.getMainLooper().
  • Show a message in a TextView when the view is created.
  • Clear the message after 3 seconds using postDelayed().
  • Store the delayed work in a Runnable so it can be cancelled.
  • Remove the callback in onDestroyView().
Take Quiz

Keep learning

Related questions

View more