Hiểu và Sử Dụng Các Trait Đặc Biệt trong RxSwift: Driver, Signal và Toán Tử Chia Sẻ Dòng Dữ Liệu

Khi làm việc với RxSwift, việc phân biệt giữa các loại dòng phát ra dữ liệu (Observable) là bước nền tảng để tránh các vấn đề như gọi lặp API, rò rỉ bộ nhớ hoặc xử lý bất đồng bộ không đúng luồng. Hai khái niệm then chốt là dòng lạnh (cold)dòng nóng (hot):

  • Dòng lạnh khởi tạo lại logic phát dữ liệu mỗi khi có người đăng ký — nghĩa là mỗi subscribe sẽ kích hoạt một chuỗi sự kiện độc lập (ví dụ: một request HTTP mới).
  • Dòng nóng duy trì trạng thái chung và phát dữ liệu theo thời gian thực, bất kể số lượng người đăng ký — tất cả subscriber đều nhận cùng một bản sao dữ liệu tại thời điểm đăng ký.

Để chuyển đổi từ dòng lạnh sang dòng nóng, RxSwift cung cấp các toán tử như publish(), connect(), refCount()replay(_:scheduler:). Trong đó:

  • publish() tạo một ConnectableObservable — chưa bắt đầu phát dữ liệu dù đã được tạo.
  • connect() kích hoạt phát dữ liệu, biến nó thành dòng nóng.
  • refCount() tự động gọi connect() khi có ít nhất một subscriber, và ngắt kết nối khi không còn ai đăng ký.
  • replay(_:) cho phép lưu tạm một số phần tử gần nhất (hoặc toàn bộ), giúp subscriber mới nhận được dữ liệu đã phát trước đó.

Các toán tử chia sẻ phổ biến hơn trong thực tế là:

  • share(): tương đương publish().refCount() — tự động quản lý kết nối dựa trên số lượng subscriber, nhưng không lưu đệm dữ liệu.
  • replay(_:) : lưu đệm tối đa n phần tử, nhưng yêu cầu gọi connect() thủ công hoặc dùng kèm refCount().
  • share(replay:scope:): kết hợp cả hai tính năng — vừa tự động kết nối, vừa lưu đệm. Tham số scope xác định hành vi bộ nhớ đệm:
    • .whileConnected: chỉ lưu trong thời gian dòng đang kết nối — mất kết nối → xóa bộ đệm.
    • .forever: giữ nguyên bộ đệm ngay cả khi không còn subscriber nào — phục vụ cho các trường hợp cần "ghi nhớ" giá trị cuối cùng.

RxCocoa mở rộng mô hình này bằng các Trait — những wrapper đặc biệt được thiết kế cho các ngữ cảnh UI cụ thể. Hai trait quan trọng nhất là DriverSignal:

Driver

Driver<T> là trait dành riêng cho luồng cập nhật giao diện người dùng. Nó đảm bảo ba thuộc tính sau:

  • An toàn lỗi: không bao giờ phát lỗi — mọi ngoại lệ đều được xử lý bên trong (thường qua onErrorJustReturn hoặc catchError).
  • Luồng chính: luôn phát dữ liệu trên MainScheduler, nên có thể gắn trực tiếp vào thuộc tính UI mà không cần observeOn.
  • Chia sẻ hiệu quả: nội bộ sử dụng share(replay: 1, scope: .whileConnected) — lưu giá trị cuối cùng và tái sử dụng cho tất cả người dùng.

Ví dụ minh họa việc tìm kiếm tự động:

let searchResults = searchField.rx.text.orEmpty
    .throttle(.milliseconds(300), scheduler: MainScheduler.instance)
    .flatMapLatest { query in
        fetchSuggestions(for: query)
            .asDriver(onErrorJustReturn: [])
    }

// Gắn kết với hai thành phần UI — chỉ gọi API một lần
searchResults
    .map { "\($0.count) kết quả" }
    .drive(searchCountLabel.rx.text)
    .disposed(by: bag)

searchResults
    .drive(suggestionsTableView.rx.items(cellIdentifier: "SuggestionCell")) { _, item, cell in
        cell.textLabel?.text = item.title
    }
    .disposed(by: bag)

So sánh với cách viết thuần Observable:

  • Không cần observeOn(MainScheduler.instance)Driver đảm bảo điều đó.
  • Không cần catchErrorJustReturn ở ngoài — asDriver(onErrorJustReturn:) đã xử lý.
  • Dùng drive(...) thay vì bind(to:...) — trình biên dịch kiểm tra tính an toàn của trait.

Signal

Signal<T> cũng chạy trên luồng chính và không phát lỗi, nhưng khác với Driver ở hai điểm then chốt:

  • Không lưu đệm: mỗi subscriber mới chỉ nhận dữ liệu phát ra sau thời điểm đăng ký.
  • Chia sẻ tài nguyên tính toán: sử dụng share(scope: .whileConnected) — phù hợp cho các tác vụ như lắng nghe sự kiện nút nhấn, cập nhật vị trí, hoặc phản ứng với thay đổi trạng thái hệ thống.

Một ví dụ điển hình:

let buttonTap = button.rx.tap.asSignal()

buttonTap
    .map { "Đã nhấn!" }
    .emit(to: statusLabel.rx.text)
    .disposed(by: bag)

buttonTap
    .withLatestFrom(currentUser) { _, user in user.id }
    .debug("User ID on tap")
    .emit()
    .disposed(by: bag)

Ở đây, Signal đảm bảo cả hai xử lý đều nhận cùng một sự kiện nhấn, không bị duplicate hay mất dữ liệu do không đồng bộ.

Thẻ: RxSwift RxCocoa ReactiveX Driver Signal

Đăng vào ngày 2 tháng 6 lúc 18:53