Mobile application development often requires precise time management of tasks. When you create an interface, you may need to delay opening the screen, animation, or sending a request to the server. There is no one-size-fits-all button for this in Android Studio, but there are several proven software approaches.

The correct method depends on where the code is running: in the UI thread or in the background. Mistakes in this selection may result in application freezing or system errors. Therefore, it is important to understand the difference between classic tools and modern asynchronous solutions.

In this article we will look at the main ways to create timers and deferred actions. We will look at working with Handler, using Kotlin Coroutines and modern approaches with LiveData. You will learn how to choose the best option for your architecture.

Using the Handler class for a UI thread

The most traditional way to implement code execution delay in Android is to use the class android.os.Handler. This tool allows you to send messages or run objects Runnable on the main thread after a specified time has elapsed. The method postDelayed is a de facto standard for simple interface tasks.

When working with Handler If you create it on the main thread, the tasks will be executed on the UI thread, which is safe for updating widgets. However, if you forget to delete the task, this may result in a memory leak or code execution after the activity has already been destroyed.

To cancel a scheduled action, you must save a reference to Runnable and call the method removeCallbacks. It is critical to do this in the onDestroy or onStop lifecycle method of an activity or fragment. Ignoring this rule is a common cause of floating bugs.

โš ๏ธ Warning: Using static or global Handler instances without binding to the activity lifecycle may cause a Memory Leak, since the Handler will hold a reference to the activity context even after it is closed.

The following is an example of how to safely use delay to change text in a TextView via 2 seconds:

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

private val runnable = Runnable {

textView.text = "Delay completed!"

}

override fun onStart() {

super.onStart()

handler.postDelayed(runnable, 2000) // 2000 ms = 2 seconds

}

override fun onStop() {

super.onStop()

handler.removeCallbacks(runnable) // Be sure to cancel the task

}

Modern approach with Kotlin Coroutines

With the advent of the language Kotlin and the introduction of Coroutines, the approach to asynchrony has changed dramatically. Now you don't need to create separate threads or complex handlers to create a delay. The function delay() is a suspending function that suspends the execution of a coroutine without blocking the main thread.

The main advantage Kotlin Coroutines is the automatic cancellation of tasks. If you run a coroutine within lifecycleScope an activity or viewModelScope in a ViewModel, then when the component is destroyed, all running tasks will be automatically canceled. This relieves the developer from manual life cycle management.

Usage delay makes the code linear and easy to read. You don't need to write nested lambda expressions or separate Runnable classes. The code appears as if it is running synchronously, although in fact the thread is not blocked.

What is the difference between Thread.sleep and Coroutine delay?

The Thread.sleep() function blocks the thread on which it is called. If you call it in the UI thread, the application will freeze and stop responding to touches. The delay() function in coroutines only pauses the execution of the current coroutine, allowing the UI thread to continue processing events and rendering.

Consider an example of using delay internally viewModelScope to simulate a network request:

fun loadDataWithDelay() {

viewModelScope.launch {

showLoading()

try {

delay(3000) // Wait 3 seconds

val data = repository.getData()

updateUI(data)

} catch (e: Exception) {

showError(e.message)

} finally {

hideLoading()

}

}

}

Delays with LiveData and StateFlow

Modern Android architecture, built on the principles of reactive programming, often uses a combination of ViewModel and LiveData or StateFlow. Direct use of timers in UI components is not recommended here. Instead, the delay logic is moved to the ViewModel layer.

You can use LiveData transformations or stream operators to implement delayed state changes. For example, in Flow there is an operator debouncethat ignores values โ€‹โ€‹if they arrive too often, or delayFlow. This is especially useful when searching with text input, so you don't have to send a request to the server after every keystroke.

If you just need to show a message or change the state of the interface after a certain time, it is best to run a coroutine inside a ViewModel, do delay, and then update MutableLiveData or MutableStateFlow. This preserves separation of concerns and code testability.

Example debounce for a search query:

searchQuery

.debounce(500) // Wait 500 ms after last input

.distinctUntilChanged()

.collect { query ->

searchRepository.search(query)

}

Task Scheduler Timer and TimerTask

Class java.util.Timer and associated with it TimerTask represent an older, but still common mechanism for scheduling recurring tasks. Unlike Handler, the timer runs in a separate background thread. This means that you cannot directly update the UI inside a run task method.

Using Timer requires caution when dealing with the lifecycle. The timer continues to tick even if the activity has entered the state Paused or Stopped. If you don't cancel the timer (cancel()) when stopping the activity, it may try to update the UI, which will cause the application to crash with an error CalledFromWrongThreadException.

For periodic execution of tasks in modern Android, it is better to use Handler with reposting or Coroutine with loop while. However, for simple background tasks not related to the interface, Timer remains a lightweight solution.

Method Execution Flow Life Cycle Recommended Usage
Handler.postDelayed UI (by default) Requires manual cancellation Animations, simple UI delays
Coroutine delay Non-blocking Automatic cancellation Network requests, complex logic
Timer Background thread Requires manual cancellation Background periodic tasks
CountDownTimer UI thread Requires manual cancellation Reverse timers countdown

Countdown with CountDownTimer

When it comes to countdown timers, for example for the lock screen or displaying the time until the SMS code is resent, standard delays are not suitable. You need to update the interface every second. For this purpose, there is a special class in the Android SDK CountDownTimer.

This class works in the UI thread and provides two methods for overriding: onTick (called every second or specified interval) and onFinish (called upon completion). This eliminates the need to write loops and manually manage update intervals.

As with Handler, it is important to call the method cancel()when the timer is no longer needed (for example, the user has closed the screen). Otherwise, it will continue to execute code and consume CPU resources in the background.

Example implementation of a timer for 10 seconds:

object : CountDownTimer(10000, 1000) {

override fun onTick(millisUntilFinished: Long) {

textView.text = "Remaining: " + millisUntilFinished / 1000

}

override fun onFinish() {

textView.text = "Done!"

}

}.start()

Typical errors and debugging

One of the most common problems is performing heavy operations in the UI thread delay method. Even if you use delay in a coroutine, the after delay code will be executed in the same context. If you launch a UI coroutine and after a delay start parsing large JSON, the interface will still freeze.

An error with the time format is also common. In Android methods, the time is usually specified in milliseconds. Beginners often confuse them with seconds, passing the value 5 instead of 5000, which leads to instantaneous, invisible execution of the task.

To debug delays, it is useful to use logging with timestamps. This will help you understand whether the task was actually completed with the required pause, or whether it was canceled by the system or rescheduled.

โš ๏ธ Attention: Intervals in Android do not guarantee absolute accuracy down to the millisecond. The system may delay execution of a task if the main thread is busy drawing a frame or processing input. Do not use timers for critical time intervals that depend on physical reality.

Always check which thread your code is running on using Log.d("Thread", Thread.currentThread().name). This will help avoid errors accessing the UI from a background thread.

Frequently asked questions (FAQ)

How to make the delay in milliseconds rather than seconds?

All standard Android methods (postDelayed, delay, CountDownTimer) accept time in milliseconds. To get a delay of 1 second, you need to pass the value 1000. For 500 milliseconds, pass 500.

What happens if you do not cancel the Handler or Timer?

If you do not cancel the task, it may run after the activity is destroyed (for example, the user maximized the application or closed the screen). This will result in an attempt to update a non-existent UI and a crash of the application (Crash) with an exception. In a background thread IllegalArgumentException or NullPointerException.

Is it possible to use Thread.sleep() for delay?

Technically it is possible, but it is strictly not recommended in the main thread, as this will โ€œfreezeโ€ the interface. On a background thread Thread.sleep is acceptable, but in Kotlin it is better to use delay()as it is more efficient and correct in terms of asynchrony.

How to delay the launch of another activity?

Use Handler or Coroutines in the current activity. Inside the delay block, call startActivity(intent). Don't forget to check if the activity is destroyed (isFinishing) before running it to avoid errors.