最近工作上需要開發在 Windows 上的 Flutter app,中間也遇到了需要開發 Windows 平台上的 plug-in 需求。
狀況大概是,幾個月前,客戶需要一套軟體,初期只需要 Windows 版本,但提到了再下個階段會需要手機版本,然後我們的設計師用 Material Design 做了整套設計,想了想,即使從來沒有用過 Flutter 在 Windows 上做一整個應用服務上線過,還是硬著頭皮拿 Flutter 上了。雖然說如果接下來在 Windows 11 上,你想要寫一套 Windows app,最好的方式是直接去寫 Android app。
就跟在其他平台一樣,Flutter Plug-in 可以分成三類:從 Flutter 端叫 Native 端做事的 method channel;第二種則是讓 Native 端呼叫 Flutter 端做事,這邊可以用 method channel 或 event channel;第三種則是 native view,在手機平台上,就可以用 native view 嵌入 web view、map view 等等。由於 Windows 上還沒有 native view 支援,我們先講前兩種:我遇到的需求是
- Flutter 端可以列出目前電腦上的所有連上的 USB 裝置
- 有新的 USB 裝置連上時,Flutter 端可以收到通知
透過 Method Channel 呼叫 Native Code
在 Windows 上開發 Flutter plug-in,主要使用 C++ 以及 MFC API,讓 native plug-in 與 Flutter 之間透過 Flutter codec 溝通。
另外一種作法是讓 Dart 透過 FFI 呼叫 Windows API,像是 win32 這個 Dart package,就是用 FFI 對許多 win32 API 做了 Dart 的封裝,甚至可以直接用 Dart 去寫 win32 app。不過我需要用到的 API 看來 win32 package 沒有包起來,包裝這些 API 也花點功夫,所以我還是寫了一個 plug-in 來滿足需求。
我們在 IDE(Android Studio、VS Code、IntelliJ)裡頭建立新的 Flutter plugin 專案的時候,裡頭就已經包含了一個用 method channel 呼叫 native code 的範例,用來抓取目前的系統版本,在 Windows 上就會回傳像是Windows 7、Windows 10 等等。
所以,我們只要稍微改寫一下這段,換個命令的名字,就可以改成回傳電腦上的 USB 裝置列表
HandleUSB 裡頭像這樣:
請注意,Windows 本身用的編碼是 UTF16,但 Flutter 是 UTF8,所以從 Windows 收到的資料,需要先轉成 UTF8,才傳送到 Flutter 上。你在每個現在可以看到的 Flutter Windows plug-in 上,大概都可以看到這一段編碼轉換。在這個例子中,雖然 USB 裝置的名稱一定只會是數字與英文字母組成,但還是保險起見做了編碼轉換。
在 Dart/Flutter 這端就可以這麼呼叫:
裝置的名稱會像是 “ \\?\usb#vid_045e&pid_00cb#6&a8d79ca&0&3#{a5dcbf10–6530–11d2–901f-00c04fb951ed}”,我們主要關心當中的 VID(Vendor ID)與 PID(Product ID),用這兩個 ID,就可以判斷是不是我們想要使用的裝置。處理這種名稱的工作,在 Dart 這邊總比在 C++ 容易許多,寫個 Regular Expression 就可以把 VID、PID parse 出來了。
讓 Flutter 接收來自 Windows 的通知
在不同平台上,要讓 Flutter 收到來自系統通知,方法大概都是先在 native 端攔截通知,然後用 method channel 或 event channel 送到 Flutter 端。不過,每個平台的作法都不太一樣。
以 iOS 或 macOS 來說,有些通知會透過 Notification Center 發送,那麼讓 plug-in 接收這些通知即可;而有些通知會送到 AppDelegate 上,Flutter 在 iOS/macOS 的範本 runner 中,就實做了一個 AppDelagte,會負責將收到的訊息送到不同的 plug-in 上,由 plug-in 處理完再透過 method/event channel 送到 Fluter,而各個 app 可以決定要註冊接收哪些訊息。
在 Windows 上,各種通知則是透過 WinProc 接收。Windows 上,使用 CreateWindow 建立視窗之後,關於這個視窗的各種訊息,會送到註冊到這個視窗的一個 callback function 中,這個 function 我們叫做 WinProc。
我們在 Flutter Windows 中,就得攔截 WinProc。在 Windows 上的 Flutter runner 中,建立了兩個 C++ class 用來處理跟視窗有關的工作,首先是 Win32Window,負責包括建立視窗、設置 WinProc 等,然後,有一個繼承自 Win32Window 的 FlutterWindow,負責把 Flutter 引擎放到我們所建立的 視窗上,也另外實做了一部份 WndProc 的工作。
以開發 plug-in 的角度來說,我們就會希望 WinProc 收到的訊息可以發送出來,而不是去修改 Win32Window 或 FlutterWindow。而目前可以看到的作法是,plug-in 可以先去取得目前視窗上的 WinProc 的指標,把他換掉,我們可以看一下 desktop_window 的作法,在 desktop_window_plugin.cpp 裡頭,有這麼幾行:
SetWindowLongPtr 這行就是要把 WndProc 換掉,而換掉的實做像這樣:
意思就是:如果是 WM_GETMINMAXINFO 這個訊息,我們就處理掉,不然就用原本的 WndProc 處理。
我們想要接收的是 USB 插拔的訊息,在 Windows 上,我們可以用 RegisterDeviceNotification 達成,微軟在 MSDN 上也有一整個範例,我們大概就把這段 code 抄過來。
即使如此還不夠,我們這邊少了兩個東西,首先 _channel 沒有定義,另外,從上面微軟的說明文件中可以知道,我們得要額外向 Windows 註冊,才能在 WndProc 收到硬體插拔事件。補上這兩段:
我們在 RegisterWithRegistrar 做了一些額外的事情,包括
- 替換 WndProc
- 建立另外一個從 native 送訊息的 method channel
- 呼叫 DoRegisterDeviceInterfaceToHwnd 跟系統要求設備插拔訊息
在 Dart 這端就可以這麼接收
這樣只要去監聽 FlutterUsbPlugin.stream,就可以收到插拔訊息了。這邊就不示範怎麼將裝置名稱轉成物件的部分。
大功告成。