Callback là một hàm được truyền vào một hàm khác như một đối số và được thực thi sau khi hoàn thành một thao tác bất đồng bộ. Trong phát triển di động, callback được sử dụng để xử lý kết quả của các yêu cầu mạng, thao tác cơ sở dữ liệu và hoạt ảnh. Theo Tài liệu Apple (2025), closures trong Swift là dạng callback chính và được sử dụng trong URLSession, GCD và Combine. Trong Android, callback được triển khai thông qua các interface, lambda Kotlin và ListenableFuture.
Các Điểm Chính
Callback (hàm gọi lại) là mã thực thi được truyền vào một hàm khác và được gọi sau khi hoàn thành một hành động cụ thể. Trong phát triển di động, callback là một cơ chế cơ bản của lập trình bất đồng bộ, cho phép phản ứng với việc hoàn thành các yêu cầu mạng, bộ đếm thời gian, hoạt ảnh và thao tác I/O mà không chặn luồng chính. Swift và Kotlin cung cấp các cấu trúc cú pháp tích hợp để tạo callback — closures và lambda tương ứng.
Một hàm bậc cao chấp nhận một hàm khác làm tham số và gọi nó sau khi thực thi logic chính của mình. Luồng điều khiển được trả lại cho người gọi thông qua callback, do đó có tên gọi này. Trong iOS, callback được sử dụng trong UIKit (hoạt ảnh UIView.animate), Foundation (URLSession.dataTask) và Combine (sink). Trong Android, callback được sử dụng trong View.OnClickListener, Retrofit Callback và Room DAO. Các API hiện đại ngày càng thay thế callback bằng async/await hoặc coroutine, nhưng hiểu về callback là cần thiết để làm việc với mã cũ và API cấp thấp.
Callback có thể đồng bộ (được gọi ngay lập tức bên trong hàm) và bất đồng bộ (được gọi sau đó từ một luồng hoặc hàng đợi khác). Callback đồng bộ được sử dụng cho sắp xếp (bộ so sánh) và duyệt tập hợp. Callback bất đồng bộ được sử dụng cho các yêu cầu mạng, đọc tệp và làm việc với cảm biến. Sự khác biệt rất quan trọng để hiểu về luồng: callback đồng bộ thực thi trên cùng một luồng, callback bất đồng bộ thực thi trên luồng do bộ điều phối xác định (DispatchQueue trong iOS, Dispatchers trong Kotlin).
Cơ chế callback trên cả hai nền tảng đều dựa trên cùng một nguyên tắc: một hàm được truyền như một đối tượng hạng nhất và được lưu trữ cho đến thời điểm thực thi. Tuy nhiên, cách triển khai khác nhau do các mô hình ngôn ngữ khác nhau. Trong iOS, callback là một closure capture các biến từ ngữ cảnh xung quanh. Trong Android, callback thường được triển khai thông qua các lớp ẩn danh hoặc biểu thức lambda Kotlin, được biên dịch thành FunctionalInterface.
Khi một hàm bất đồng bộ được gọi, closure được lưu trữ trên heap cùng với các biến đã capture. Khi thao tác hoàn tất, hệ thống GCD hoặc OperationQueue đặt callback vào hàng đợi thích hợp (hàng đợi chính hoặc hàng đợi nền). Sau khi thực thi, callback bị xóa khỏi bộ nhớ khi không còn tham chiếu mạnh. Capture list ([weak self]) ngăn đối tượng bị giữ lại sau khi giải phóng. Nếu không có capture list, retain cycle xảy ra khi đối tượng và callback tham chiếu lẫn nhau.
func fetchData(completion: @escaping (Result<Data, Error>) -> Void) {
let task = URLSession.shared.dataTask(with: url) { data, response, error in
if let error = error {
completion(.failure(error))
return
}
completion(.success(data))
}
task.resume()
}
// Sử dụng với [weak self]
fetchData { [weak self] result in
guard let self else { return }
switch result {
case .success(let data):
self.updateUI(data)
case .failure(let error):
self.showError(error)
}
}
Trong Android, callback được truyền qua interface hoặc lambda. Khi thực thi một thao tác bất đồng bộ qua ExecutorService hoặc coroutine, callback được lưu trong bộ nhớ cho đến khi công việc nền hoàn tất. Lambda Kotlin được biên dịch thành các lớp ẩn danh capture các biến bên ngoài. Việc thiếu tham chiếu yếu trong JVM đòi hỏi quản lý thủ công: hủy callback trong onDestroy() hoặc hủy coroutine qua Job.cancel(). ViewModel và LiveData giải quyết vấn đề này ở cấp độ thành phần kiến trúc.
interface Callback<T> {
fun onSuccess(data: T)
fun onError(error: Throwable)
}
class Repository {
fun loadData(callback: Callback<List<User>>) {
thread {
try {
val result = api.fetchUsers()
runOnUiThread { callback.onSuccess(result) }
} catch (e: Exception) {
runOnUiThread { callback.onError(e) }
}
}
}
}
// Sử dụng với lambda
repository.loadData(object : Callback<List<User>> {
override fun onSuccess(data: List<User>) { showUsers(data) }
override fun onError(error: Throwable) { showError(error.message) }
})
Cú pháp callback được xác định bởi khả năng của ngôn ngữ làm việc với các hàm như đối tượng hạng nhất. Trong Swift, closures có cú pháp ngắn gọn với tên đối số tự động ($0, $1). Trong Kotlin, lambda hỗ trợ it cho một đối số duy nhất. Sự khác biệt thể hiện trong xử lý capture biến (capture list trong Swift so với tham chiếu có thể thay đổi trong Kotlin) và kiểu dữ liệu (Result
Closure Swift là một khối mã độc lập có thể được truyền và sử dụng trong một hàm khác. Closures có thể là toàn cục (có tên), lồng nhau và ở cấp biểu thức. @escaping đánh dấu một closure sẽ được thực thi sau khi hàm trả về — đây là yêu cầu bắt buộc đối với callback bất đồng bộ. Nếu không có @escaping, closure chỉ có thể được thực thi bên trong thân hàm. Cú pháp trailing closure cho phép truyền closure sau dấu ngoặc đơn: fetchData { result in ... }.
typealias NetworkResult = (Result<[String: Any], Error>) -> Void
func performRequest(
url: URL,
then handler: @escaping NetworkResult
) {
let task = URLSession.shared.dataTask(with: url) { data, _, error in
handler(Result {
guard let json = try JSONSerialization.jsonObject(with: data)
else { throw NetworkError.invalidData }
return json as! [String: Any]
})
}
task.resume()
}
performRequest(url: url) { result in
switch result {
case .success(let json): process(json)
case .failure(let error): log(error.localizedDescription)
}
}
Kotlin hỗ trợ các hàm bậc cao chấp nhận các hàm khác làm tham số. Callback trong Kotlin được truyền qua tham số kiểu (T) -> Unit hoặc (T) -> R cho giá trị trả về. Các hàm suspend của coroutine Kotlin thay thế callback bằng mã tuần tự, nhưng callback vẫn tồn tại trong các API tương thích Java và Android SDK (View.setOnClickListener, TextWatcher). Lambda Kotlin tự động capture các biến val, các biến var yêu cầu wrapper có thể thay đổi.
fun <T, R> processWithCallback(
input: T,
transform: (T) -> R,
onResult: (R) -> Unit
) {
thread {
val result = transform(input)
runOnUiThread { onResult(result) }
}
}
// Ví dụ với lambda
processWithCallback(
input = "Hello",
transform = { it.length },
onResult = { length ->
textView.text = "Length: $length"
}
)
Retain cycle là tình huống hai đối tượng giữ tham chiếu mạnh đến nhau, ngăn bộ quản lý bộ nhớ giải phóng chúng. Trong Swift, retain cycle xảy ra khi viewController capture một closure và closure capture self. Trong Kotlin/Java, rò rỉ xảy ra khi Activity truyền một lớp nội bộ hoặc lambda vào một thao tác nền kéo dài. Theo Phiên WWDC 10216 (2024), quản lý closure không đúng cách là nguyên nhân phổ biến thứ ba gây rò rỉ bộ nhớ trong ứng dụng iOS.
Swift sử dụng Đếm tham chiếu tự động (ARC), giải phóng đối tượng khi bộ đếm tham chiếu về không. Capture list [weak self] hoặc [unowned self] trong closure ngăn retain cycle. weak self tạo một tham chiếu tùy chọn trở thành nil khi đối tượng được giải phóng. unowned self giả định đối tượng tồn tại lâu hơn closure — vi phạm giả định này gây ra crash. Nên sử dụng weak self làm tùy chọn an toàn mặc định.
class DataController {
var onDataUpdate: ((String) -> Void)?
func setupCallback() {
// Retain cycle!
onDataUpdate = { text in
self.process(text)
}
// Đã sửa: [weak self]
onDataUpdate = { [weak self] text in
guard let self else { return }
self.process(text)
}
}
func process(_ input: String) { }
}
Trong Android, rò rỉ bộ nhớ do callback xảy ra khi Activity hoặc Fragment truyền một listener đến thành phần singleton (ví dụ: EventBus hoặc Service). WeakReference cho phép bộ thu gom rác giải phóng Activity ngay cả khi có tham chiếu yếu đến nó. Các thành phần lifecycle-aware (LiveData, Flow) tự động giải quyết vấn đề. Lambda Kotlin capture ngữ cảnh Activity cũng có thể gây rò rỉ: lambda ngầm lưu trữ tham chiếu đến this.
class SafeCallbackManager {
private val listeners = mutableListOf<WeakReference<(String) -> Unit>>()
fun addListener(callback: (String) -> Unit) {
listeners.add(WeakReference(callback))
}
fun notifyAll(data: String) {
val iterator = listeners.iterator()
while (iterator.hasNext()) {
val ref = iterator.next().get()
if (ref != null) ref(data)
else iterator.remove()
}
}
}
// Sử dụng trong Fragment
manager.addListener { result ->
// WeakReference không giữ Fragment
updateUI(result)
}
Callback Hell (còn gọi là Kim tự tháp diệt vong) là tình huống nhiều callback lồng nhau tạo ra cấu trúc mã lồng nhau sâu, khó đọc và gỡ lỗi. Mỗi bước tiếp theo yêu cầu chờ bước trước hoàn thành, dẫn đến 5-10 cấp độ lồng nhau. Vấn đề này đặc trưng cho các thao tác bất đồng bộ tuần tự: tải dữ liệu → phân tích → lưu vào DB → cập nhật UI.
Swift 5.5 giới thiệu các hàm bất đồng bộ (async/await), cho phép viết mã bất đồng bộ một cách tuần tự. AsyncSequence và AsyncStream thay thế các vòng lặp dựa trên callback. Framework Combine cung cấp các toán tử flatMap, merge, combineLatest để kết hợp các luồng bất đồng bộ mà không cần lồng nhau. Tuy nhiên, callback vẫn cần thiết để làm việc với API Objective-C và thư viện bên thứ ba không hỗ trợ async.
// Callback lồng nhau — Callback Hell
loginUser(credentials) { user in
fetchProfile(user.id) { profile in
downloadAvatar(profile.avatarUrl) { image in
cacheImage(image) { success in
updateUI(user, profile, image)
}
}
}
}
// async/await — giải pháp
func loadUserExperience() async throws {
let user = try await loginUser(credentials)
let profile = try await fetchProfile(user.id)
let image = try await downloadAvatar(profile.avatarUrl)
try await cacheImage(image)
updateUI(user, profile, image)
}
Coroutine Kotlin thay thế callback bằng các hàm suspend để thực thi tuần tự. Flow cung cấp các luồng lạnh với các toán tử map, flatMapConcat, combine. CoroutineScope cho phép hủy tất cả coroutine đang chạy khi một thành phần bị phá hủy. Room, Retrofit và các thư viện Jetpack khác có hỗ trợ tích hợp cho các hàm suspend, loại bỏ nhu cầu callback trong các thao tác tiêu chuẩn.
// Callback tuần tự — Callback Hell
api.login(credentials) { user ->
api.fetchProfile(user.id) { profile ->
api.download(profile.avatarUrl) { bytes ->
file.save(bytes) { result ->
textView.text = result.toString()
}
}
}
}
// Coroutine — giải pháp
suspend fun loadUserData() {
val user = withContext(Dispatchers.IO) { api.login(credentials) }
val profile = withContext(Dispatchers.IO) { api.fetchProfile(user.id) }
val bytes = withContext(Dispatchers.IO) { api.download(profile.avatarUrl) }
withContext(Dispatchers.IO) { file.save(bytes) }
textView.text = "Done"
}
Callback và Delegate là hai cách tiếp cận thông báo bất đồng bộ, và việc lựa chọn phụ thuộc vào yêu cầu kiến trúc. Callback phù hợp cho các thao tác một lần với một kết quả duy nhất. Delegate được thiết kế cho nhiều sự kiện với các chữ ký phương thức khác nhau. Apple khuyến nghị delegate cho các giao thức phức tạp với nhiều phương thức và callback cho các closure đơn giản với một kết quả. Trong Android, callback thay thế delegate trong hầu hết các trường hợp nhờ hỗ trợ lambda.
Callback tối ưu cho các thao tác với một kết quả duy nhất: yêu cầu mạng, đọc tệp, hoạt ảnh với khối hoàn thành. Ưu điểm: cú pháp nhỏ gọn, không cần giao thức riêng, capture ngữ cảnh trực tiếp. Nhược điểm: phức tạp với nhiều kết quả (tiến trình, tạm dừng, hủy), không thể gửi nhiều lần (nếu callback có thể được gọi nhiều lần — hãy sử dụng publisher).
Delegate phù hợp cho các giao thức với nhiều phương thức bắt buộc và tùy chọn: UITableViewDelegate, CLLocationManagerDelegate, kết nối Bluetooth. Ưu điểm: kiểu dữ liệu rõ ràng cho mỗi phương thức, tài liệu hóa qua giao thức, hỗ trợ phương thức tùy chọn qua @objc optional. Nhược điểm: mã boilerplate, tham chiếu yếu đến delegate bắt buộc (weak var delegate), phức tạp khi capture ngữ cảnh.
Câu Hỏi Thường Gặp
Callback là một trường hợp đặc biệt của hàm bậc cao. Hàm bậc cao chấp nhận một hàm khác làm đối số hoặc trả về nó. Callback là một hàm được truyền đặc biệt để thực thi bất đồng bộ sau khi hoàn thành một thao tác. Tất cả callback được triển khai thông qua các hàm bậc cao, nhưng không phải mọi hàm bậc cao đều là callback.
Theo quy ước, callback chỉ nên được gọi đúng một lần — success hoặc failure. Gọi nhiều lần cùng một callback được coi là lỗi thiết kế. Đối với nhiều sự kiện (tiến trình, luồng dữ liệu), hãy sử dụng Observable, Publisher hoặc Flow — chúng hỗ trợ phát ra nhiều giá trị. Một số API vi phạm quy tắc này, dẫn đến các lỗi khó tìm.
Trailing closure là cú pháp đường của Swift cho phép truyền closure sau dấu ngoặc đơn của lời gọi hàm. Nếu một hàm chấp nhận closure làm đối số cuối cùng, nó có thể được đặt bên ngoài dấu ngoặc: fetchData { result in ... }. Đối với nhiều closure, trailing closure chỉ áp dụng cho closure cuối cùng; các closure còn lại được đặt tên bên trong dấu ngoặc. Điều này cải thiện khả năng đọc của API dựa trên callback.
Sử dụng WeakReference cho các listener tồn tại lâu, hủy coroutine qua Job.cancel() trong onDestroy(), sử dụng lifecycleScope để tự động hủy. ViewModel + LiveData/Flow giải quyết vấn đề ở cấp độ kiến trúc. Tránh truyền ngữ cảnh Activity vào callback tĩnh — sử dụng Application context. Lambda Kotlin capture this ngầm định, hãy kiểm tra bằng trình phân tích bộ nhớ.
Async/await thay thế callback cho mã bất đồng bộ tuần tự, nhưng không cho kiến trúc hướng sự kiện. Callback vẫn tồn tại trong API hệ thống (View.OnClickListener, delegate URLSession), callback tiến trình và thư viện bên thứ ba. Việc thay thế hoàn toàn là không thể do tương thích ngược. Chiến lược hiện đại là sử dụng async/await với wrapper callback (continuation trong Swift, suspendCancellableCoroutine trong Kotlin).
Tổng Kế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