O Handler é seu amigo

O Handler é seu amigo

Este artigo é técnico, não é sobre política, mas também vai chutar um cachorro morto. Digo isto porque imagino que a grande maioria dos desenvolvedores Android já conhece a classe Handler, então este artigo está alguns anos atrasado. Mas, a quem ainda possa interessar, lá vai.

Atualização 2026: o texto abaixo é da era Java + AsyncTask e foi mantido como estava, pois os conceitos continuam valendo. Mas muita coisa mudou: AsyncTask foi descontinuada, os construtores de Handler sem Looper explícito também, e em Kotlin o jeito idiomático de fazer tudo isto hoje são as corrotinas. Acrescentei uma seção no final mostrando os equivalentes modernos, cujo conteúdo foi gerado pelo Claude (Anthropic) e revisado por mim.

Uma experiência recorrente que tive com desenvolvedores Android é a tendência de usar Thread para muita coisa que não devia. Exemplo: quando uma tarefa precisa ser executada depois de um certo tempo, a tática utilizada é criar uma thread e colocá-la para dormir com Thread.sleep(). Outro exemplo: quando é preciso agendar uma tarefa para execução postergada.

Creio que seja cacoete de desenvolvedor Java experiente, porque a API Java "padrão" não tem muitos recursos assíncronos, mas o suporte a threads na própria linguagem é bom. Então usa-se threads para tudo que cheire a assincronia. Exemplo: o método amplamente aceito para implementar um servidor TCP/IP em Java é criar uma thread por conexão — uma heresia para quem tem background em UNIX, Node.js, Python/Twisted, etc.

Acredito que essa decisão de design do Java se deve ao fato da Sun fabricar processadores SPARC, muito mais otimizados para threads que os processadores Intel x86, por exemplo. Mas é uma conjectura pessoal. (Que me faz desconfiar de qualquer linguagem que tem um único mantenedor, pois este vai impor sua agenda.)

Só que depurar threads é um inferno, threads têm de ser evitadas sempre que possível, não interessa se a linguagem oferece suporte de primeira classe a elas.

Quem conhece Java, segue a trilha usual e acaba encontrando uma classe do Android chamada AsyncTask, que encapsula a conversação entre a thread secundária (da tarefa) e a thread da UI, ou "thread principal", que é a única que pode manipular a UI. Só que AsyncTask continua rodando a tarefa postergada numa thread separada, com todas as complicações que isto pode trazer, principalmente nos corner cases — por exemplo, quando o aplicativo está fechando.

Mas a API do Android também oferece a classe Handler, que resolve 95% dos problemas de execução postergada de tarefas, sem usar threads!

Curiosamente, encontrei esta classe porque minha experiência em Java tendia a zero antes de desenvolver para Android, e minha experiência pregressa gira em torno de UNIX, então por "instinto" busquei uma API que lidasse com assincronia sem envolver threads.

Handler para tarefas postergadas

Basta criar um Handler para agendar todas as tarefas postergadas da classe ou do aplicativo inteiro, e chamar post() ou postDelayed() conforme a necessidade. Estes métodos aceitam objetos Runnable, tal qual threads, o que até facilita a migração.

	// na criação da classse
	h = new Handler();

	// tarefa postergada por 100 milissegundos
	h.postDelayed(new Runnable() {
		public void run() {
			fazQualquerCoisa();
		}
	}, 100);

	// tarefa postergada para tão logo quanto possível
	h.post(new Runnable() {
		public void run() {
			fazQualquerCoisa();
		}
	});

Naturalmente, nos exemplos acima, fazQualquerCoisa() vai rodar no contexto da thread principal, então seu aplicativo vai bloquear até que a tarefa termine. Isto não é um problema para tarefas de curta duração, mas realmente não serve se a tarefa envolve processamento longo, ou comunicação de rede. Nestes casos AsyncTask seria a ferramenta correta.

Rodar a tarefa na thread principal tem a enorme vantagem da previsibilidade. Supondo que você queira executar a tarefa imeditamente após o término do método atual. Basta usar post(tarefa). Não há nenhuma chance da tarefa concorrer com o método em execução, porque este já está ocupando a thread principal. Como post(tarefa) vai entrar na fila de tarefas pendentes, é garantido que a tarefa já pode lidar com a UI (se agendada durante Activity.OnCreate()) porque a tarefa e a construção da UI usam a mesma thread e o segundo item tem prioridade.

A classe Handler faz muita coisa e tem inúmeros métodos; vale a pena ler a documentação; muitos problemas de assincronia encontram solução ali. Handler também implementa filas de mensagens, o que facilita a comunicação assíncrona entre threads ou mesmo entre partes desacopladas do seu aplicativo. (Este recurso em particular eu nunca usei, porque não precisei ainda, mas está lá.)

Handler para conectar threads

A comunicação entre threads diferentes é o grande problema recorrente de usar threads. Para muitos efeitos práticos, devemos pensar a thread como um processo separado, e para dois processos conversarem é preciso usar um canal de dados — compartilhar diretamente objetos é procurar encrenca. Também neste caso, Handler presta um grande serviço.

Bem, quando eu usaria uma Thread em vez de um AsyncTask? Num caso de uso recente, a thread roda continuamente, durante toda a vida do aplicativo, então o código executado pela thread não pode realmente ser encapsulado como uma tarefa, porque ele não tem duração determinada. Além do mais, a comunicação entre thread principal e secundária é bidirecional e constante, enquanto AsyncTask facilita a comunicação apenas em uma direção.

A classe que será executada na thread principal, denominada Principal, que inclusive cria a thread secundária, eu implemento mais ou menos assim:

public class Principal {
	Handler h;

	public Principal() {
		h = new Handler();
		t = new Secundaria();
		t.start();
	}

	/* pode ser invocado por qualquer thread */
	public void g_metodo1() {
		h.post(new Runnable() {
			public void run() {
				metodo1();
			}
		}
	}

	/* garantidamente roda no contexto da thread principal */
	private void metodo1() {
		...
	}
}

O método público g_metodo1() existe sob medida para que a thread Secundaria possa invocar um metodo da classe Principal de forma segura, sem precisar se preocupar com a sincronização. Como a execução de metodo1() só pode acontecer mediante agendamento no Handler, e o objeto h foi criado no contexto da thread principal, é garantido que metodo1() executa na thread principal.

Já a classe Secundaria, fica assim:

public class Secundaria extends Thread {
	Handler h;

	public Secundaria() {
		...
	}

	@Override
	public void run() {
		Looper.prepare();
		h = new Handler();
		Looper.loop();
	}

	/* pode ser invocado por qualquer thread */
	public void g_metodo2() {
		h.post(new Runnable() {
			public void run() {
				metodo2();
			}
		}
	}

	/* garantidamente roda no contexto da thread secundaria */
	private void metodo2() {
		...
	}

	/* deve haver uma forma de 'matar' a thread */
	public void parar() {
		h.post(new Runnable() {
			public void run() {
				Looper.myLooper().quit();
			}
		}
	}
}

A classe Secundaria tem algumas semelhanças com a anterior. A forma de garantir a execução de metodo2() no contexto correto, que é a thread secundária, é o mesmo: agendando a execução através de um Handler. Os clientes da classe devem invocar o método público g_metodo2(), que faz esse agendamento.

Desta forma, as duas classes podem estabelecer comunicação bidirecional, uma invocando os métodos públicos da outra. A única deficiência desta técnica é não poder retornar dados, porque a execução do método real (privado) é sempre postergada. Para o chamador obter um retorno qualquer, seria necessário usar um callback.

O grande detalhe de implementação de Secundaria é o método run(), onde a classe Looper é utilizada para criar uma fila de eventos para a thread secundária. O Handler depende desta fila para funcionar. Note que run() não faz muita coisa por si só; neste exemplo a thread secundária só faz algo útil quando metodo2() é invocado, fora isso fica dormindo.

Outra forma de estabelecer comunicação entre thread principal e secundária seria a troca de mensagens, em vez de invocação de métodos.

Finalização da thread

Um problema bem típico de threads é a finalização. Uma thread sempre deve tomar a iniciativa de terminar por si mesma. Não existe forma segura de "matar" uma thread a partir de outra, e a coleta de lixo do Java não "mata" threads abandonadas.

No próprio exemplo acima, a classe Principal contém um objeto da classe Secundaria, mas que não será destruído quando o objeto Principal tornar-se dispensável. Num caso mais típico, se uma Activity cria uma thread e é destruída em seguida, essa thread continua rodando, e conforme o usuário abre e fecha o aplicativo várias vezes, as threads vão se acumulando.

Como um embrião de solução para este problema, temos o método Secundaria.parar(), que encerra a fila de eventos. Isto permite que o método run() retorne, o que sinaliza o fim da thread. Obviamente, parar() tem de ser invocado num lugar oportuno, como por exemplo em Activity.OnDestroy(). É uma coleta de lixo explícita.

É o tipo de problema que você só enfrenta se usa threads, então reitero que elas devem ser evitadas tanto quanto possível.

E hoje, em Kotlin?

Primeiro, um esclarecimento: a classe Handler em si não foi descontinuada. O que o Android 11 (API 30) descontinuou foi o construtor sem argumentos, Handler(), que pegava implicitamente o Looper da thread corrente — fonte de bugs sutis quando o objeto era criado na thread errada, ou numa thread sem Looper. Hoje o Looper deve ser explícito. Na mesma leva, AsyncTask foi descontinuada de vez.

	// Kotlin, Handler "à moda antiga", mas com Looper explícito
	val h = Handler(Looper.getMainLooper())

	h.postDelayed({ fazQualquerCoisa() }, 100)
	h.post { fazQualquerCoisa() }

Quando só se quer "voltar" para a thread principal a partir de outra thread, sem atraso, nem é preciso ter um Handler à mão: a própria Activity oferece o método runOnUiThread(), que existe desde as primeiras versões do Android e continua válido. (View.post() é um primo próximo, útil quando só se tem acesso a uma View.)

	// em alguma thread secundária
	val dados = baixaDaRede()

	activity.runOnUiThread {
		mostraNaTela(dados)
	}

Há uma diferença sutil em relação a post(): se runOnUiThread() for invocado já na thread principal, o bloco é executado imediatamente, de forma síncrona, em vez de entrar na fila. É conveniente, mas abre mão daquela previsibilidade comentada antes, de que a tarefa só roda depois que o método atual termina. Se esta garantia importa, use post(). Por baixo dos panos, aliás, runOnUiThread() faz justamente um post() num Handler interno da Activity quando chamado de outra thread.

Mas o idioma moderno de Kotlin para tudo que este artigo discute são as corrotinas. O curioso é que elas seguem exatamente a filosofia defendida acima: assincronia sem proliferação de threads. Uma corrotina suspensa não ocupa thread nenhuma; ela é retomada quando o evento esperado acontece. E, no Android, Dispatchers.Main é implementado... em cima de um Handler da thread principal. O velho amigo continua lá, só que escondido.

A tarefa postergada fica assim, dentro de uma Activity ou Fragment:

	// equivalente a postDelayed(..., 100)
	lifecycleScope.launch {
		delay(100)
		fazQualquerCoisa()
	}

O código roda na thread principal, com a mesma previsibilidade descrita antes, e delay() não bloqueia nada — apenas suspende a corrotina. A vantagem sobre o Handler é que lifecycleScope cancela automaticamente a corrotina quando a Activity é destruída. Com Handler, uma tarefa agendada para daqui a 10 segundos executaria mesmo com a Activity já morta, a menos que o programador lembrasse de chamar removeCallbacksAndMessages().

O caso onde antes se usava AsyncTask (processamento longo ou rede) também fica trivial: troca-se de contexto só durante o trecho pesado, e o resultado volta naturalmente para a thread principal, sem callbacks:

	lifecycleScope.launch {
		val dados = withContext(Dispatchers.IO) {
			baixaDaRede()      // roda num pool de threads
		}
		mostraNaTela(dados)    // de volta à thread principal
	}

O padrão Principal/Secundaria também tem tradução direta. A ideia central — confinar cada método a um contexto de execução, e deixar o agendamento cuidar da sincronização — continua idêntica; muda a ferramenta. Em vez de Looper.prepare() e Looper.loop(), cria-se um dispatcher de paralelismo 1, que garante que apenas uma tarefa por vez roda nele:

class Secundaria {
	private val contexto =
		Dispatchers.Default.limitedParallelism(1)

	/* pode ser invocado de qualquer corrotina */
	suspend fun g_metodo2(x: Int): Int = withContext(contexto) {
		metodo2(x)
	}

	/* garantidamente roda confinado em 'contexto' */
	private fun metodo2(x: Int): Int {
		...
	}
}

class Principal(private val scope: CoroutineScope) {
	private val s = Secundaria()

	/* pode ser invocado de qualquer corrotina */
	suspend fun g_metodo1(): String =
		withContext(Dispatchers.Main) {
			metodo1()
		}

	fun usaSecundaria() {
		scope.launch {
			// suspende, não bloqueia
			val r = s.g_metodo2(42)
			...
		}
	}
}

Note que a "única deficiência" apontada antes — não poder retornar dados — desaparece. Como g_metodo2() é uma função suspend, o chamador simplesmente espera o resultado, sem bloquear sua própria thread e sem precisar de callback.

Se a preferência for por troca de mensagens em vez de invocação de métodos, o equivalente moderno da fila de mensagens do Handler é o Channel (ou um Flow, para fluxos de eventos), consumido por uma corrotina que roda continuamente:

	val fila = Channel<Mensagem>(Channel.UNLIMITED)

	scope.launch(contexto) {
		for (msg in fila) {
			// uma mensagem por vez, no mesmo contexto
			trata(msg)
		}
	}

	// de qualquer lugar:
	fila.trySend(Mensagem(...))

E, finalmente, a finalização, que era o calcanhar de Aquiles das threads. Com corrotinas vale a concorrência estruturada: toda corrotina pertence a um escopo, e cancelar o escopo cancela tudo o que foi lançado dentro dele. Se as corrotinas são lançadas no viewModelScope ou no lifecycleScope, não é preciso um método parar() nem lembrar de chamá-lo em onDestroy(): o ciclo de vida do Android cancela o escopo, e nada fica "acumulando" a cada vez que o usuário abre e fecha o aplicativo. Aquela "coleta de lixo explícita" passou a ser feita pelo framework.

Para quem ainda precisa de uma thread de verdade com fila de eventos própria (por exemplo, para conversar com uma API Java que exige Looper), existe a classe HandlerThread, que encapsula exatamente o Looper.prepare()/Looper.loop() que a classe Secundaria fazia na mão. E ela pode virar um dispatcher de corrotinas com Handler(ht.looper).asCoroutineDispatcher(). O círculo se fecha.