Pull-to-Refresh: kiến thức cơ bản, RefreshControl và UIRefreshControl

Tác giả: IT Sectr Đã đăng: 2026-02-27 Thời gian đọc: 8 phút
Pull-to-Refresh là một mẫu giao diện di động trong đó người dùng kéo danh sách xuống bằng ngón tay, kích hoạt tải dữ liệu mới. Cử chỉ đi kèm với một chỉ báo trực quan — vòng quay spinner hoặc biểu tượng hoạt hình — biến mất sau khi tải hoàn tất. Theo phân tích UX của Apple HIG, Pull-to-Refresh đã trở thành cơ chế cập nhật nội dung tiêu chuẩn trong bảng tin, mạng xã hội và ứng dụng email kể từ khi được giới thiệu trong Tweetie (2008) và sau đó được Apple và Google tiêu chuẩn hóa.

Những điểm chính

  • Pull-to-Refresh là cử chỉ kéo danh sách xuống để làm mới dữ liệu, kèm theo chỉ báo tải trực quan.
  • Trên iOS, sử dụng UIRefreshControl (iOS 6+), được thêm vào UITableViewController hoặc UIScrollView thông qua thuộc tính refreshControl.
  • Trên Android, sử dụng SwipeRefreshLayout (từ Support Library) — một trình bao bọc ViewGroup cho RecyclerView hoặc NestedScrollView.
  • Cả hai API đều hỗ trợ tùy chỉnh màu sắc, chỉ báo và callback thông qua listener (iOS: UIRefreshControl.target-action, Android: setOnRefreshListener).
  • Pull-to-Refresh tự động bị chặn khi danh sách không ở vị trí trên cùng — xung đột với cuộn được loại trừ về mặt kiến trúc.

Pull-to-Refresh là gì?

Pull-to-Refresh là một mẫu giao diện người dùng trong đó người dùng kéo (pull down) một danh sách hoặc vùng có thể cuộn xuống để làm mới nội dung. Về mặt trực quan, cử chỉ đi kèm với một chỉ báo tải (spinner) xuất hiện ở đầu màn hình và biến mất sau khi nhận được dữ liệu. Mẫu này được phổ biến bởi ứng dụng Tweetie cho iPhone (2008) và sau đó được tiêu chuẩn hóa bởi Apple (iOS 6 — UIRefreshControl) và Google (Android Support Library — SwipeRefreshLayout).

Từ góc độ kỹ thuật, Pull-to-Refresh là sự kết hợp giữa panning (theo dõi độ dịch chuyển của ngón tay) và một bộ kích hoạt khi đạt đến ngưỡng. Người dùng kéo danh sách xuống, vượt qua lực cản (overscroll điện trở), và sau khi vượt quá ngưỡng (~80px trên iOS, ~64dp trên Android), hoạt ảnh chỉ báo và tải không đồng bộ bắt đầu. Nếu người dùng thả ngón tay trước ngưỡng, danh sách trở về vị trí ban đầu mà không làm mới.

Theo Nguyên tắc Material Design, Pull-to-Refresh không nên được sử dụng để điều hướng hoặc chuyển tab — mục đích duy nhất của nó là làm mới dữ liệu. Tại IT Sectr, chúng tôi sử dụng Pull-to-Refresh trong bảng tin, danh sách đơn hàng và trò chuyện nơi độ mới của dữ liệu rất quan trọng cho trải nghiệm người dùng.

Pull-to-Refresh trên iOS: UIRefreshControl

UIRefreshControl là điều khiển iOS tiêu chuẩn cho Pull-to-Refresh, có sẵn từ iOS 6. UIRefreshControl được thêm vào UITableViewController thông qua thuộc tính refreshControl (iOS 10+) hoặc như một subview của bảng trong các phiên bản trước. Nó bao gồm spinner tích hợp với màu sắc có thể tùy chỉnh (tintColor), thuộc tính title và chuỗi có thuộc tính với nhãn (ví dụ: "Đang cập nhật...").

UIRefreshControl hoạt động thông qua cơ chế target-action: khi cử chỉ được kích hoạt, phương thức được chỉ định được gọi (ví dụ: refresh(_:)). Bên trong phương thức, tải dữ liệu không đồng bộ được thực hiện. Sau khi hoàn tất, endRefreshing() được gọi, ẩn chỉ báo với hoạt ảnh. UIRefreshControl tự động quản lý độ nhạy của cử chỉ — nó chỉ kích hoạt khi bảng ở vị trí trên cùng (contentOffset.y <= 0).

Thuộc tính tintColor đặt màu của spinner. attributedTitle cho phép hiển thị văn bản như "Đã cập nhật 2 phút trước" sau khi hoàn tất. Kể từ iOS 10, UIRefreshControl hỗ trợ hoạt ảnh tùy chỉnh thông qua UIActivityIndicatorView hoặc các chế độ xem tùy chỉnh liên tục. Tại IT Sectr, chúng tôi cấu hình tintColor theo thương hiệu và hiển thị thời gian cập nhật lần cuối qua attributedTitle — điều này làm tăng niềm tin của người dùng vào dữ liệu.

Pull-to-Refresh trên Android: SwipeRefreshLayout

SwipeRefreshLayout là một ViewGroup từ Android Support Library (androidx.swiperefreshlayout) bao bọc nội dung có thể cuộn (RecyclerView, NestedScrollView, ListView) và thêm chức năng Pull-to-Refresh. Không giống như UIRefreshControl (là một điều khiển, không phải vùng chứa), SwipeRefreshLayout là một vùng chứa chặn các sự kiện chạm của phần tử con và kích hoạt chỉ báo làm mới khi vượt quá ngưỡng.

SwipeRefreshLayout sử dụng chỉ báo tiến trình hình tròn Material Design với tùy chỉnh màu sắc thông qua setColorSchemeColors(). Phương thức setOnRefreshListener đặt callback onRefresh(), trong đó tải không đồng bộ được thực hiện. Sau khi hoàn tất, setRefreshing(false) được gọi để ẩn chỉ báo. Quan trọng: setRefreshing(true) gọi onRefresh() một lần nữa — vì vậy để bắt đầu làm mới theo chương trình, hãy sử dụng cờ hoặc phương thức post.

Thuộc tính setProgressBackgroundColorSchemeResource thay đổi nền của chỉ báo. setSize(SwipeRefreshLayout.LARGE) đặt kích thước spinner. Trong bố cục XML, SwipeRefreshLayout bao bọc RecyclerView: swipe_refresh_layout → recycler_view. Theo Google I/O 2024, SwipeRefreshLayout được sử dụng trong 85% ứng dụng Android có nguồn cấp dữ liệu nội dung. Tại IT Sectr, chúng tôi bao bọc tất cả màn hình có danh sách được tải không đồng bộ trong SwipeRefreshLayout — điều này mang lại trải nghiệm người dùng nhất quán trên tất cả các phiên bản Android.

Material Pull-to-Refresh (Android 12+)

Bắt đầu từ Android 12 (Material You), Google khuyến nghị sử dụng Material Pull-to-Refresh mới từ thư viện material-1.6.0+ (androidx.compose.material3.pulltorefresh cho Compose). API mới sử dụng chỉ báo hoạt hình với hỗ trợ hoạt ảnh lò xo và màu sắc thích ứng dựa trên hình nền. SwipeRefreshLayout vẫn tương thích cho các phiên bản dưới Android 12.

Thực hành tốt nhất và lỗi thường gặp

Pull-to-Refresh là một mẫu dễ triển khai, nhưng nó chứa một số lỗi điển hình làm giảm trải nghiệm người dùng. Hãy xem xét chúng và cách phòng tránh.

  • Làm mới kép — người dùng có thể kéo danh sách nhiều lần trước khi tải xong. Giải pháp: đặt cờ isRefreshing khi bắt đầu và kiểm tra nó trong onRefresh(). Trên iOS, endRefreshing() chỉ được gọi sau khi hoàn tất; chặn cử chỉ trong UIRefreshControl được tích hợp sẵn.
  • Thiếu phản hồi — chỉ báo tải chỉ nên xuất hiện sau khi người dùng vượt quá ngưỡng. Không hiển thị chỉ báo ngay khi chạm — điều này gây nhầm lẫn cho người dùng. iOS và Android thực hiện việc này tự động.
  • Bỏ qua thời gian làm mới — nếu dữ liệu làm mới trong 200 ms, chỉ báo nên được hiển thị ít nhất 500 ms để người dùng nhận thấy bản cập nhật. UIRefreshControl có thời lượng hoạt ảnh tối thiểu; trên Android, sử dụng Handler.postDelayed cho thời gian hiển thị tối thiểu.
  • Xung đột với bàn phím — khi bàn phím mở, Pull-to-Refresh có thể vô tình kích hoạt. Ẩn bàn phím khi cử chỉ bắt đầu qua view.endEditing(true) trên iOS và InputMethodManager.hideSoftInputFromWindow() trên Android.
  • Sử dụng không đúng mục đích — không sử dụng Pull-to-Refresh để điều hướng (chuyển tab, quay lại). Điều này vi phạm HIG của cả hai nền tảng và gây mất phương hướng cho người dùng.

Tại IT Sectr, chúng tôi đã thêm kiểm tra isRefreshing trong mọi dự án sau khi phát hiện các yêu cầu trùng lặp trong nhật ký máy chủ thử nghiệm — hóa ra người dùng có ngón tay nhanh đã kích hoạt làm mới tới 3 lần liên tiếp.

Ví dụ mã Swift và Kotlin

Ví dụ 1: UIRefreshControl trên iOS (Swift)

Thêm Pull-to-Refresh vào UITableViewController với màu spinner tùy chỉnh và tiêu đề có thuộc tính. Sau khi tải dữ liệu, chỉ báo được ẩn đi.

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: "Kéo để làm mới"
        )
        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()
        }
    }
}

Thuộc tính tableView.refreshControl (iOS 10+) đặt UIRefreshControl. addTarget với sự kiện .valueChanged kích hoạt khi cử chỉ được kích hoạt. endRefreshing() là bắt buộc — nếu không có nó, chỉ báo sẽ quay vô hạn. Tải không đồng bộ được mô phỏng bằng DispatchQueue.main.asyncAfter — trong dự án thực tế, hãy sử dụng URLSession hoặc async/await.

Ví dụ 2: SwipeRefreshLayout trên Android (Kotlin)

Bao bọc RecyclerView trong SwipeRefreshLayout với màu chỉ báo tùy chỉnh. onRefresh bắt đầu tải và ẩn chỉ báo sau khi hoàn tất.

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 đặt màu của chỉ báo Material Design quay. isRefreshing = false được gọi trong khối finally để ẩn chỉ báo ngay cả khi có lỗi tải. ViewModelScope.launch thực thi một coroutine trong vòng đời của fragment — khi fragment bị hủy, coroutine tự động bị hủy, ngăn rò rỉ bộ nhớ.

Ví dụ 3: SwiftUI .refreshable (iOS 15+)

SwiftUI hiện đại cung cấp bổ ngữ .refreshable tự động thêm Pull-to-Refresh vào List hoặc 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()
        }
    }
}

Bổ ngữ .refreshable nhận một async-closure thực thi khi Pull-to-Refresh. SwiftUI tự động hiển thị và ẩn chỉ báo làm mới, quản lý điều kiện đua (không bắt đầu làm mới mới cho đến khi hoàn tất làm mới hiện tại) và điều chỉnh hoạt ảnh theo nền tảng. Đối với iOS 15+, đây là cách ưa thích để triển khai Pull-to-Refresh trong SwiftUI.

Câu hỏi thường gặp

Pull-to-Refresh có hoạt động trong SwiftUI không?

Có, SwiftUI cung cấp bổ ngữ .refreshable cho List hoặc ScrollView, có sẵn từ iOS 15. Bên trong closure, mã tải dữ liệu không đồng bộ được thực thi. SwiftUI tự động quản lý chỉ báo làm mới và chặn các kích hoạt lặp lại cho đến khi tải hiện tại hoàn tất — đây là cách tiếp cận tiêu chuẩn được khuyến nghị cho các dự án mới.

Làm thế nào để ngăn làm mới kép?

Sử dụng cờ isRefreshing: đặt true khi bắt đầu tải và false sau khi hoàn tất. Trên iOS, UIRefreshControl tự động chặn các cuộc gọi lặp lại cho đến khi endRefreshing() được gọi. Trên Android, kiểm tra SwipeRefreshLayout.isRefreshing ở đầu onRefresh(): nếu true — return. Điều này đảm bảo một yêu cầu cho mỗi cử chỉ.

Pull-to-Refresh có xung đột với cuộn danh sách không?

UIRefreshControl và SwipeRefreshLayout chỉ kích hoạt khi danh sách ở vị trí trên cùng (contentOffset == 0). Kiến trúc loại trừ xung đột: miễn là danh sách được cuộn dù chỉ 1px, cử chỉ Pull-to-Refresh không kích hoạt. Nếu xảy ra xung đột, hãy kiểm tra nestedScrollingEnabled trên Android hoặc sự hiện diện của GestureRecognizer tùy chỉnh chặn chạm.

Tóm tắt

  • Pull-to-Refresh là mẫu làm mới dữ liệu bằng cử chỉ kéo xuống, được Apple và Google tiêu chuẩn hóa trên tất cả các nền tảng di động.
  • UIRefreshControl trên iOS — một điều khiển với target-action, tintColor, attributedTitle và endRefreshing() bắt buộc.
  • SwipeRefreshLayout trên Android — một vùng chứa ViewGroup với setOnRefreshListener, setColorSchemeColors và isRefreshing.
  • Material Pull-to-Refresh (Android 12+) — API mới với hoạt ảnh lò xo, được khuyến nghị cho các dự án mới.
  • SwiftUI .refreshable — bổ ngữ khai báo với async-closure, có sẵn từ iOS 15.
  • Cờ isRefreshing ngăn làm mới kép — bắt buộc trên cả hai nền tảng.
  • Pull-to-Refresh không dành cho điều hướng — chỉ để làm mới nội dung theo Material Design và Apple HIG.

Chúng tôi sẽ phát triển ứng dụng di động chìa khóa trao tay

IT Sectr tạo các ứng dụng iOS và Android cho các công ty khởi nghiệp và doanh nghiệp từ năm 2017. Chúng tôi sẽ tư vấn và đề xuất giải pháp tốt nhất cho bạn.

Thảo luận dự án

Đọc thêm