最近工作上使用 Flutter 開發一套語一個藍芽玩具搭配的手機 app,很自然地使用 flutter_blue_plus 這個套件處理與藍芽裝置連線的部分,然後踩到一個小小的雷。我們來看一下這段程式:
device.state.listen((event) {
if (event == BluetoothDeviceState.disconnecting) {}
if (event == BluetoothDeviceState.disconnected) {}
if (event == BluetoothDeviceState.connected) {}
});
device.connect();
簡單講就是,我對一個代表藍芽裝置的物件 device,先設定一個 lisener 監聽狀態的改變(state 是一個 Stream),然後開始要求連線。我原本預期應該在真的呼叫了 connect() 之後,這個 listener 才會開始收到東西,因為直接閱讀這段程式的時候,感覺起來,如果沒有呼叫 connect()、disconnect() 之類的 method,不該有任何造成狀態改變的原因,而既然狀態沒有改變,就不會觸發 listener 才對。
不過,在 flutter_blue_plus 的實作中,光是呼叫 state,就會讓 listener 收到一次事件。我們可以看一下 state 的實作:
Stream<BluetoothState> get state async* {
yield await _channel
.invokeMethod('state')
.then((buffer) => protos.BluetoothState.fromBuffer(buffer))
.then((s) => BluetoothState.values[s.state.value]);
_stateStream ??= _stateChannel
.receiveBroadcastStream()
.map((buffer) => protos.BluetoothState.fromBuffer(buffer))
.map((s) => BluetoothState.values[s.state.value])
.doOnCancel(() => _stateStream = null);
yield* _stateStream!;
}
在 state 這個 getter 裡頭,首先會透過 method channel,向 native code 詢問一次目前的連線狀態,然後 yield 出來,丟給 listener;之後,才透過一個 method stream,讓 native code 那邊(Java/Kotlin 的 listener 或 Swift 的 delegate),在收到連線狀態改變的時候,丟到 Flutter 這邊。也就是說,Flutter 這邊會比 native code 那邊,多收到一次連線狀態相關的通知。
這樣的設計,就會在呼叫 connect() 之前,就先收到了一個 disconnected 的狀態通知,這個通知並不代表從連線變成斷線的變化,而是先告訴你一次目前的狀態是什麼。這就讓人覺得—不自在,因為我們可能已經寫過平台的 native code,沒想到在 Flutter 上頭的行為跟 native SDK 的表現不一樣。
所以,我們可能得先把目前處於斷線這個狀態先記錄下來,然後在斷線狀態下收到斷線通知時,就不要處理。
或是,我們乾脆就把第一次的通知丟掉算了。
device.state.skip(1).listen((event) { .... }