Pull-to-Refresh: conceitos básicos, RefreshControl e UIRefreshControl

Autor: IT Sectr Publicado: 2026-02-27 Tempo de leitura: 8 min
Pull-to-Refresh é um padrão de interface móvel onde o usuário puxa a lista para baixo com o dedo, iniciando o carregamento de novos dados. O gesto é acompanhado por um indicador visual — um spinner giratório ou ícone animado — que desaparece após a conclusão do carregamento. De acordo com a análise UX da Apple HIG, o Pull-to-Refresh tornou-se o mecanismo padrão de atualização de conteúdo em feeds de notícias, redes sociais e clientes de e-mail desde sua introdução no Tweetie (2008) e posterior padronização pela Apple e Google.

Pontos principais

  • Pull-to-Refresh é um gesto de puxar a lista para baixo para atualizar dados, acompanhado por um indicador visual de carregamento.
  • No iOS, usa-se UIRefreshControl (iOS 6+), adicionado a UITableViewController ou UIScrollView através da propriedade refreshControl.
  • No Android, usa-se SwipeRefreshLayout (da Support Library) — um wrapper ViewGroup para RecyclerView ou NestedScrollView.
  • Ambas as APIs suportam personalização de cores, indicadores e callbacks através de listeners (iOS: UIRefreshControl.target-action, Android: setOnRefreshListener).
  • O Pull-to-Refresh é bloqueado automaticamente quando a lista não está na posição superior — o conflito com a rolagem é excluído arquiteturalmente.

O que é Pull-to-Refresh?

Pull-to-Refresh é um padrão de interface do usuário onde o usuário puxa (pull down) uma lista ou área rolável para baixo para atualizar o conteúdo. Visualmente, o gesto é acompanhado por um indicador de carregamento (spinner) que aparece no topo da tela e desaparece após receber os dados. O padrão foi popularizado pelo aplicativo Tweetie para iPhone (2008) e posteriormente padronizado pela Apple (iOS 6 — UIRefreshControl) e Google (Android Support Library — SwipeRefreshLayout).

Do ponto de vista técnico, Pull-to-Refresh é uma combinação de pan (rastreamento do deslocamento do dedo) e um gatilho ao atingir um limite. O usuário puxa a lista para baixo, superando uma resistência (overscroll resistivo), e após exceder o limite (~80px no iOS, ~64dp no Android), a animação do indicador e o carregamento assíncrono são iniciados. Se o usuário soltar o dedo antes do limite, a lista retorna à sua posição original sem atualizar.

De acordo com as Material Design Guidelines, o Pull-to-Refresh não deve ser usado para navegação ou troca de abas — seu único propósito é a atualização de dados. Na IT Sectr, usamos Pull-to-Refresh em feeds de notícias, listas de pedidos e chats onde a atualidade dos dados é crítica para a experiência do usuário.

Pull-to-Refresh no iOS: UIRefreshControl

UIRefreshControl é o controle padrão do iOS para Pull-to-Refresh, disponível desde o iOS 6. O UIRefreshControl é adicionado ao UITableViewController através da propriedade refreshControl (iOS 10+) ou como uma subview da tabela em versões anteriores. Ele inclui um spinner embutido com cor personalizável (tintColor), atributo title e uma string atribuída com um rótulo (por exemplo, "Atualizando...").

O UIRefreshControl funciona através do mecanismo target-action: quando o gesto é ativado, o método especificado é chamado (por exemplo, refresh(_:)). Dentro do método, o carregamento assíncrono de dados é realizado. Após a conclusão, endRefreshing() é chamado, que oculta o indicador com animação. O UIRefreshControl gerencia automaticamente a sensibilidade do gesto — ele só é acionado quando a tabela está na posição superior (contentOffset.y <= 0).

A propriedade tintColor define a cor do spinner. O attributedTitle permite mostrar texto como "Atualizado há 2 minutos" após a conclusão. Desde o iOS 10, o UIRefreshControl suporta animações personalizadas através de UIActivityIndicatorView ou visualizações personalizadas persistentes. Na IT Sectr, configuramos o tintColor de acordo com a marca e mostramos o horário da última atualização através do attributedTitle — isso aumenta a confiança do usuário nos dados.

Pull-to-Refresh no Android: SwipeRefreshLayout

SwipeRefreshLayout é um ViewGroup da Android Support Library (androidx.swiperefreshlayout) que envolve conteúdo rolável (RecyclerView, NestedScrollView, ListView) e adiciona funcionalidade Pull-to-Refresh. Ao contrário do UIRefreshControl (que é um controle, não um contêiner), o SwipeRefreshLayout é um contêiner que intercepta os eventos de toque do filho e aciona o indicador de atualização quando o limite é excedido.

O SwipeRefreshLayout usa um indicador de progresso circular do Material Design com personalização de cor através de setColorSchemeColors(). O método setOnRefreshListener define o callback onRefresh(), no qual o carregamento assíncrono é realizado. Após a conclusão, setRefreshing(false) é chamado para ocultar o indicador. Importante: setRefreshing(true) chama onRefresh() novamente — portanto, para iniciar a atualização programaticamente, use uma flag ou método post.

A propriedade setProgressBackgroundColorSchemeResource altera o fundo do indicador. setSize(SwipeRefreshLayout.LARGE) define o tamanho do spinner. No layout XML, o SwipeRefreshLayout envolve o RecyclerView: swipe_refresh_layout → recycler_view. De acordo com o Google I/O 2024, o SwipeRefreshLayout é usado em 85% dos aplicativos Android com feeds de conteúdo. Na IT Sectr, envolvemos todas as telas com listas carregadas assincronamente em SwipeRefreshLayout — isso proporciona uma experiência de usuário consistente em todas as versões do Android.

Material Pull-to-Refresh (Android 12+)

A partir do Android 12 (Material You), o Google recomenda usar o novo Material Pull-to-Refresh da biblioteca material-1.6.0+ (androidx.compose.material3.pulltorefresh para Compose). A nova API usa um indicador animado com suporte a animação spring e cor adaptativa baseada no papel de parede. O SwipeRefreshLayout permanece compatível para versões abaixo do Android 12.

Melhores práticas e erros comuns

Pull-to-Refresh é um padrão simples de implementar, mas contém vários erros típicos que degradam a experiência do usuário. Vamos examiná-los e como evitá-los.

  • Atualização dupla — o usuário pode puxar a lista várias vezes antes do carregamento terminar. Solução: defina uma flag isRefreshing no início e verifique-a em onRefresh(). No iOS, endRefreshing() é chamado apenas após a conclusão; o bloqueio do gesto no UIRefreshControl é embutido.
  • Falta de feedback — o indicador de carregamento deve aparecer estritamente após o usuário exceder o limite. Não mostre o indicador imediatamente ao tocar — isso confunde os usuários. iOS e Android fazem isso automaticamente.
  • Ignorar a duração da atualização — se os dados atualizam em 200 ms, o indicador deve ser mostrado por pelo menos 500 ms para que o usuário note a atualização. O UIRefreshControl tem uma duração mínima de animação; no Android, use Handler.postDelayed para um tempo mínimo de exibição.
  • Conflito com o teclado — com o teclado aberto, o Pull-to-Refresh pode ser acionado acidentalmente. Oculte o teclado ao iniciar o gesto através de view.endEditing(true) no iOS e InputMethodManager.hideSoftInputFromWindow() no Android.
  • Uso para fins que não são atualização — não use Pull-to-Refresh para navegação (troca de abas, voltar). Isso viola as HIG de ambas as plataformas e desorienta os usuários.

Na IT Sectr, adicionamos a verificação isRefreshing em todos os projetos depois de descobrir requisições duplicadas nos logs do servidor de teste — descobriu-se que usuários com dedos rápidos estavam acionando a atualização até 3 vezes seguidas.

Exemplos de código em Swift e Kotlin

Exemplo 1: UIRefreshControl no iOS (Swift)

Adiciona Pull-to-Refresh ao UITableViewController com cor de spinner personalizada e título atribuído. Após o carregamento dos dados, o indicador é ocultado.

swift
import UIKit

class FeedTableViewController: UITableViewController {

    private var items: [String] = []

    override func viewDidLoad() {
        super.viewDidLoad()

        tableView.refreshControl = UIRefreshControl()
        refreshControl?.tintColor = .systemBlue
        refreshControl?.attributedTitle = NSAttributedString(
            string: "Puxe para atualizar"
        )
        refreshControl?.addTarget(
            self,
            action: #selector(refreshData),
            for: .valueChanged
        )
    }

    @objc private func refreshData() {
        DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) {
            self.items = FeedService().fetchLatest()
            self.tableView.reloadData()
            self.refreshControl?.endRefreshing()
        }
    }
}

A propriedade tableView.refreshControl (iOS 10+) define o UIRefreshControl. addTarget com o evento .valueChanged é acionado quando o gesto é ativado. endRefreshing() é obrigatório — sem ele, o indicador gira indefinidamente. O carregamento assíncrono é simulado com DispatchQueue.main.asyncAfter — em um projeto real, use URLSession ou async/await.

Exemplo 2: SwipeRefreshLayout no Android (Kotlin)

Envolve RecyclerView em SwipeRefreshLayout com cores de indicador personalizadas. onRefresh inicia o carregamento e oculta o indicador após a conclusão.

kotlin
class FeedFragment : Fragment() {

    private var _binding: FragmentFeedBinding? = null
    private val binding get() = _binding!!

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        _binding = FragmentFeedBinding.inflate(inflater, container, false)

        binding.swipeRefreshLayout.setColorSchemeColors(
            resources.getColor(R.color.brand_blue, null),
            resources.getColor(R.color.brand_green, null)
        )
        binding.swipeRefreshLayout.setOnRefreshListener {
            loadData()
        }
        return binding.root
    }

    private fun loadData() {
        viewModelScope.launch {
            try {
                val result = repository.getLatestFeed()
                adapter.submitList(result)
            } finally {
                binding.swipeRefreshLayout.isRefreshing = false
            }
        }
    }

    override fun onDestroyView() {
        super.onDestroyView()
        _binding = null
    }
}

setColorSchemeColors define as cores do indicador giratório do Material Design. isRefreshing = false é chamado no bloco finally para ocultar o indicador mesmo em caso de erro de carregamento. ViewModelScope.launch executa uma corrotina dentro do ciclo de vida do fragmento — quando o fragmento é destruído, a corrotina é cancelada automaticamente, evitando vazamentos de memória.

Exemplo 3: SwiftUI .refreshable (iOS 15+)

O SwiftUI moderno fornece o modificador .refreshable que adiciona automaticamente Pull-to-Refresh a List ou ScrollView.

swift
import SwiftUI

struct FeedView: View {

    @State private var items: [String] = []

    var body: some View {
        List(items, id: \.self) { item in
            Text(item)
        }
        .refreshable {
            items = await FeedService().fetchLatestAsync()
        }
    }
}

O modificador .refreshable aceita um async-closure que executa no Pull-to-Refresh. O SwiftUI automaticamente mostra e oculta o indicador de atualização, gerencia condições de corrida (não inicia uma nova atualização até que a atual termine) e adapta a animação à plataforma. Para iOS 15+, esta é a maneira preferida de implementar Pull-to-Refresh no SwiftUI.

Perguntas frequentes

O Pull-to-Refresh funciona no SwiftUI?

Sim, o SwiftUI fornece o modificador .refreshable para List ou ScrollView, disponível desde o iOS 15. Dentro do closure, o código assíncrono de carregamento de dados é executado. O SwiftUI gerencia automaticamente o indicador de atualização e bloqueia ativações repetidas até que o carregamento atual termine — esta é a abordagem recomendada padrão para novos projetos.

Como evitar a atualização dupla?

Use uma flag isRefreshing: defina como true no início do carregamento e false após a conclusão. No iOS, o UIRefreshControl bloqueia automaticamente chamadas repetidas até que endRefreshing() seja chamado. No Android, verifique SwipeRefreshLayout.isRefreshing no início de onRefresh(): se for true — return. Isso garante uma requisição por gesto.

O Pull-to-Refresh conflita com a rolagem da lista?

UIRefreshControl e SwipeRefreshLayout só são acionados quando a lista está na posição superior (contentOffset == 0). A arquitetura elimina o conflito: enquanto a lista estiver rolada mesmo que 1px, o gesto Pull-to-Refresh não é ativado. Se ocorrer um conflito, verifique nestedScrollingEnabled no Android ou a presença de GestureRecognizers personalizados interceptando toques.

Resumo

  • Pull-to-Refresh é um padrão de atualização de dados por gesto de puxar para baixo, padronizado pela Apple e Google em todas as plataformas móveis.
  • UIRefreshControl no iOS — um controle com target-action, tintColor, attributedTitle e endRefreshing() obrigatório.
  • SwipeRefreshLayout no Android — um contêiner ViewGroup com setOnRefreshListener, setColorSchemeColors e isRefreshing.
  • Material Pull-to-Refresh (Android 12+) — uma nova API com animação spring, recomendada para novos projetos.
  • SwiftUI .refreshable — um modificador declarativo com async-closure, disponível desde o iOS 15.
  • A flag isRefreshing previne a atualização dupla — obrigatória em ambas as plataformas.
  • Pull-to-Refresh não é para navegação — apenas para atualização de conteúdo conforme Material Design e Apple HIG.

Vamos desenvolver um aplicativo móvel chave na mão

A IT Sectr cria aplicativos para iOS e Android para startups e empresas desde 2017. Nós vamos aconselhá-lo e propor a melhor solução.

Discutir o projeto

Leia também