Những điểm chính
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.
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.
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.
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.
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.
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.
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.
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.
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.
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ớ.
SwiftUI hiện đại cung cấp bổ ngữ .refreshable tự động thêm Pull-to-Refresh vào List hoặc ScrollView.
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
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.
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ỉ.
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
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.
Đọc thêm