Phát triển Bluetooth: Callback vs Reactive Programming
Xây dựng ứng dụng Bluetooth Low Energy liên quan đến việc xử lý nhiều thao tác bất đồng bộ: quét, kết nối, khám phá dịch vụ, đọc/ghi characteristic, và xử lý ngắt kết nối. Cách tiếp cận dựa trên callback truyền thống có thể nhanh chóng trở nên khó quản lý, dẫn đến điều mà các nhà phát triển gọi là “callback hell”. Trong bài viết này, chúng ta sẽ so sánh cách tiếp cận callback với lập trình reactive sử dụng RxSwift và RxJava, và khám phá cách các pattern reactive có thể cải thiện đáng kể code BLE của bạn.
Thách thức của phát triển BLE
Các thao tác Bluetooth Low Energy vốn dĩ là bất đồng bộ. Một luồng điển hình để đọc giá trị characteristic bao gồm:
Bắt đầu quét thiết bị
Kết nối đến peripheral được phát hiện
Khám phá services
Khám phá characteristics
Đọc giá trị characteristic
Xử lý lỗi tiềm ẩn ở mỗi bước
Mỗi bước phụ thuộc vào bước trước hoàn thành thành công, tạo ra một chuỗi các thao tác phụ thuộc cần được điều phối cẩn thận.
Cách tiếp cận Callback
Hãy bắt đầu bằng cách xem xét cách chúng ta triển khai một luồng BLE hoàn chỉnh sử dụng pattern delegate/callback truyền thống trong iOS.
enumBLEError: Error{ case deviceNotFound case scanTimeout case connectionFailed case serviceNotFound case characteristicNotFound }
Vấn đề với cách tiếp cận Callback
Nhìn vào code ở trên, một số vấn đề trở nên rõ ràng:
Callback Hell: Các callback lồng nhau trong readHeartRate() tạo ra “kim tự tháp tử thần” khó đọc và bảo trì.
Quản lý trạng thái: Chúng ta cần nhiều completion handler optional và phải quản lý cẩn thận vòng đời của chúng.
Xử lý lỗi: Xử lý lỗi lặp đi lặp lại và phân tán khắp code.
Quản lý bộ nhớ: Rủi ro retain cycle với closures đòi hỏi sử dụng cẩn thận [weak self].
Xử lý timeout: Mỗi thao tác cần logic timeout riêng.
Không có khả năng kết hợp: Các thao tác không thể dễ dàng kết hợp, thử lại, hoặc biến đổi.
Cách tiếp cận Reactive
Lập trình Reactive là gì?
Lập trình Reactive là một mô hình lập trình khai báo tập trung vào các luồng dữ liệu và sự lan truyền thay đổi. Thay vì viết các hướng dẫn từng bước (mệnh lệnh), bạn mô tả những gì bạn muốn xảy ra khi dữ liệu chảy qua hệ thống của bạn.
Hãy nghĩ về nó như việc thiết lập một đường ống: dữ liệu đi vào một đầu, chảy qua các biến đổi khác nhau, và đi ra đầu kia ở dạng bạn cần. Đường ống tự động xử lý luồng, lỗi, và hoàn thành.
Các khái niệm cốt lõi
1. Observable (Luồng) Observable đại diện cho một luồng dữ liệu có thể phát ra các giá trị theo thời gian. Nó có thể phát ra:
Next: Một giá trị mới trong luồng
Error: Một lỗi xảy ra, luồng kết thúc
Complete: Luồng hoàn thành thành công
1 2
// Một Observable phát ra các giá trị nhịp tim theo thời gian Observable<Int> // phát ra: 72 -> 75 -> 71 -> 68 -> ...
2. Observer (Người đăng ký) Observer đăng ký vào Observable và phản ứng với các giá trị được phát ra:
3. Operators (Toán tử) Operators biến đổi, lọc, và kết hợp các luồng. Các operator phổ biến bao gồm:
Operator
Mục đích
Ví dụ
map
Biến đổi mỗi giá trị
Chuyển đổi bytes thô thành nhịp tim
filter
Chỉ cho qua các giá trị thỏa điều kiện
Chỉ giá trị > 60 BPM
flatMap
Biến đổi thành Observable khác
Kết nối, sau đó khám phá services
take
Chỉ lấy N giá trị đầu tiên
Lấy thiết bị đầu tiên được phát hiện
timeout
Thất bại nếu không có giá trị trong thời gian
Timeout quét sau 10s
retry
Thử lại khi có lỗi
Kết nối lại khi bị ngắt
catch
Xử lý lỗi một cách nhẹ nhàng
Trả về giá trị mặc định khi có lỗi
4. Disposable (Quản lý đăng ký) Disposable đại diện cho một đăng ký đang hoạt động. Hủy nó sẽ hủy đăng ký và dọn dẹp tài nguyên:
1 2 3 4 5
let disposeBag = DisposeBag()
observable .subscribe(onNext: { value in/* xử lý */ }) .disposed(by: disposeBag) // Tự động hủy khi disposeBag bị deallocate
5. Schedulers Schedulers kiểm soát thread/queue nào các thao tác chạy trên:
1 2 3
observable .subscribe(on: ConcurrentDispatchQueueScheduler(qos: .background)) // Làm việc trên background .observe(on: MainScheduler.instance) // Trả kết quả trên main thread
Tại sao Reactive phù hợp cho BLE?
Lập trình reactive đặc biệt phù hợp cho BLE vì:
Phù hợp tự nhiên cho các sự kiện bất đồng bộ: Các thao tác BLE là các luồng sự kiện (kết quả quét, trạng thái kết nối, cập nhật characteristic)
Xử lý lỗi tích hợp: Các operator như retry, catch, và timeout xử lý các kịch bản lỗi BLE phổ biến
Kết hợp dễ dàng: Nối chuỗi các thao tác một cách tự nhiên (quét → kết nối → khám phá → đọc)
Dọn dẹp tài nguyên tự động: Disposables đảm bảo các kết nối và quét được dọn dẹp đúng cách
Bây giờ hãy xem lập trình reactive biến đổi code này như thế nào. Chúng ta sẽ sử dụng RxSwift cho iOS và hiển thị các tương đương RxJava cho Android.
privatefuncparseHeartRate(from data: Data) -> Int { let bytes = [UInt8](data) if bytes[0] & 0x01 == 0 { returnInt(bytes[1]) } else { returnInt(bytes[1]) | (Int(bytes[2]) << 8) } } }
// Sử dụng classHeartRateViewController: UIViewController{ privatelet bleManager = ReactiveBLEManager() privatelet disposeBag = DisposeBag()
overridefuncviewDidLoad() { super.viewDidLoad()
// Đọc một lần bleManager.readHeartRate() .observe(on: MainScheduler.instance) .subscribe( onNext: { heartRate in print("Nhịp tim: \(heartRate) BPM") }, onError: { error in print("Lỗi: \(error)") } ) .disposed(by: disposeBag)
// Theo dõi liên tục với tự động thử lại bleManager.observeHeartRate() .retry(when: { errors in errors.delay(.seconds(5), scheduler: MainScheduler.instance) }) .observe(on: MainScheduler.instance) .subscribe(onNext: { heartRate in self.updateUI(heartRate: heartRate) }) .disposed(by: disposeBag) }
privatefuncupdateUI(heartRate: Int) { // Cập nhật UI } }
// Sử dụng trong Activity publicclassHeartRateActivityextendsAppCompatActivity{ private ReactiveBLEManager bleManager; private CompositeDisposable disposables = new CompositeDisposable();
@Override protectedvoidonCreate(Bundle savedInstanceState){ super.onCreate(savedInstanceState); bleManager = new ReactiveBLEManager(this);
Mặc dù cách tiếp cận callback đơn giản hơn để hiểu ban đầu, lập trình reactive cung cấp những lợi thế đáng kể cho phát triển BLE:
Code sạch hơn đọc như một mô tả về những gì bạn muốn đạt được
Xử lý lỗi tốt hơn với cơ chế retry và timeout có sẵn
Kết hợp dễ dàng hơn các luồng bất đồng bộ phức tạp
Cải thiện khả năng kiểm thử với lập lịch xác định
Đường cong học tập ban đầu đáng giá đầu tư cho bất kỳ dự án BLE nghiêm túc nào. Các thư viện như RxBluetoothKit (iOS) và RxAndroidBle (Android) giúp việc chuyển đổi dễ dàng hơn bằng cách cung cấp các wrapper reactive xung quanh các API Bluetooth gốc của nền tảng.
Bắt đầu với các thao tác đơn giản, dần dần áp dụng các pattern nâng cao hơn, và xem code BLE của bạn biến đổi từ callbacks rối rắm thành các reactive streams thanh lịch, dễ bảo trì.