Tag: flutter

  • Flutter 中 Camera 與背景執行的那些事

    Flutter 中 Camera 與背景執行的那些事

    在 Flutter 裡用 camera,看似簡單,其實是個多層次的世界。

    要讓影像處理順暢不卡,非得搞清楚幾件事。

    一、await 不等於背景執行

    await 只是語法糖,讓非同步程式看起來「像同步」。

    但它仍在主執行緒裡運行,只是暫時讓出控制權。

    如果背後的工作太重,畫面還是會頓。

    二、Isolate、compute、spawn 的差別

    Flutter 沒有真正的多執行緒。

    要在背景處理資料,得靠 Isolate

    • compute()
      • Flutter 內建的簡易背景工具。
      • 適合「一次性任務」,例如單張影像轉換。 任務結束後自動銷毀。
    • Isolate.spawn()
      • 可以建立「長駐的背景執行緒」。
      • 用 SendPort、ReceivePort 跟主程式保持溝通。
      • 適合連續性任務,例如影像串流、推論、錄影壓縮。

    簡單說:

    await 是語法糖,

    compute 是臨時工,

    spawn 才是常駐員。

    三、Channel 與 FFI 的界線

    Flutter 與原生世界溝通有兩種方式:

    • Platform Channel
      • Flutter ↔ Android / iOS 的橋樑。
      • 執行於主執行緒。
      • 適合拍照、權限、系統呼叫等操作。
    • FFI(Foreign Function Interface)
    • 直接呼叫原生 C/C++ 函式庫。
    • 可在背景 Isolate 執行。
    • 適合高運算任務:影像處理、AI 推論、FFT、加密等。

    一句話:

    Channel 負責「通話」,

    FFI 負責「運算」。

    四、assets 只能在主執行緒載入

    rootBundle.load()、AssetImage 都屬於 Flutter engine 綁定資源。

    背景 Isolate 無法直接操作。

    正確方式:

    在主執行緒載入檔案。 轉成 Uint8List。 傳進背景執行緒處理。

    五、真正的背景執行要靠 FFI

    背景 Isolate 無法使用 Channel、也不能載入 assets。

    若要做長時間、重運算的工作,

    FFI 是唯一穩定的方式。

    這樣才能完全脫離主執行緒,不影響畫面。

    六、結語

    Flutter 的非同步世界其實很單純。

    async / await 是流程控制,

    Isolate 是平行運算,

    Channel 是橋樑,

    FFI 才能真正「分身」。

    理解這幾層,Camera 的畫面就能穩、背景的運算也能快。

    真正流暢的 Flutter,就從這裡開始。

  • Flutter Doctor

    以下是 flutter doctor -v 完全點亮的執行結果(運行於 2024.10.14)

  • Flutter – 透過 FFmpeg 為聲音降噪

    import 'package:ffmpeg_kit_flutter/ffmpeg_kit.dart';
    import 'package:ffmpeg_kit_flutter/return_code.dart';
     
    /// 透過 FFmpeg 後處理對聲音進行降噪音
    /// 使用內建的 afftdn (目前版本無法附加額外參數)
    Future<void> audioNoiseCanceling(String inputFilePath, String outputFilePath)async  {
      // FFmpeg command to remove noise using the 'afftdn' filter
      String command = '-i $inputFilePath -af afftdn $outputFilePath';
     
      // 檢查檔案是否已經存在,若存在則刪除
      final outputFile = File(outputFilePath);
      if (await outputFile.exists()) {
        try {
          await outputFile.delete();
          print('Existing file deleted: $outputFilePath');
        } catch (e) {
          print('Failed to delete existing file: $e');
        }
      }
  • Flutter – 錯誤訊息

    Flutter 會不會太可愛了~ 錯誤訊息竟然直接寫 BUG!

  • Flutter – BLE Scanning Page

    2024/08/18 使用當前最新版 flutter_blue_plus (1.32.12) 寫的無IDE建議或警告的藍牙掃描頁面。

    import 'dart:async';
    import 'package:flutter/foundation.dart';
    import 'package:flutter/material.dart';
    import 'package:flutter_blue_plus/flutter_blue_plus.dart';
    
    class BLEHomePage extends StatefulWidget {
      const BLEHomePage({super.key});
    
      @override
      BLEHomePageState createState() => BLEHomePageState();
    }
    
    class BLEHomePageState extends State<BLEHomePage> {
      BluetoothDevice? connectedDevice;
      List<ScanResult> devicesList = <ScanResult>[];
      BluetoothCharacteristic? characteristic;
      bool isScanning = false;
      StreamSubscription<List<ScanResult>>? scanSubscription;
    
      @override
      void initState() {
        super.initState();
        startScan();
      }
    
      void startScan() async {
        if (isScanning) return;
    
        setState(() {
          isScanning = true;
          devicesList = [];
        });
    
        // 啟動掃描,並訂閱掃描結果 (使用 scanResults)
        scanSubscription?.cancel();
        scanSubscription = FlutterBluePlus.scanResults.listen((results) {
          setState(() {
            devicesList = results;
          });
    
          for (ScanResult r in results) {
            if (r.device.platformName == "AI Glove" && connectedDevice == null) {
              connectToDevice(r.device);
              stopScan();
              break;
            }
          }
        }, onError: (error) {
          // 處理掃描錯誤
          if (kDebugMode) {
            print('Error during scan: $error');
          }
          // 在使用 context 之前檢查 mounted
          if (mounted) {
            ScaffoldMessenger.of(context).showSnackBar(
              SnackBar(content: Text('Scanning error: $error')),
            );
          }
        });
    
        // 啟動掃描 (使用 startScan, without timeout)
        await FlutterBluePlus.startScan();
    
        // 設定掃描超時 (使用 Timer)
        Timer(const Duration(seconds: 4), () {
          stopScan();
        });
      }
    
      void connectToDevice(BluetoothDevice device) async {
        try {
          await FlutterBluePlus.stopScan();
          await device.connect();
          setState(() {
            connectedDevice = device;
            isScanning = false;
          });
    
          List<BluetoothService> services = await device.discoverServices();
          for (BluetoothService service in services) {
            List<BluetoothCharacteristic> characteristics = service.characteristics;
            for (BluetoothCharacteristic c in characteristics) {
              if (c.properties.notify) {
                characteristic = c;
                await c.setNotifyValue(true);
    
                // Use lastValueStream instead of value
                c.lastValueStream.listen((value) {
                 // Do somethings
                });
              }
            }
          }
        } catch (e) {
          // 處理連接錯誤
          if (kDebugMode) {
            print('Error connecting to device: $e');
          }
          // 在使用 context 之前檢查 mounted
          if (mounted) {
            ScaffoldMessenger.of(context).showSnackBar(
              SnackBar(content: Text('Connection error: $e')),
            );
          }
        }
      }
    
      void stopScan() {
        if (!isScanning) return;
    
        FlutterBluePlus.stopScan();
        scanSubscription?.cancel();
        setState(() {
          isScanning = false;
        });
      }
    
      @override
      void dispose() {
        FlutterBluePlus.stopScan();
        scanSubscription?.cancel();
        super.dispose();
      }
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
            title: const Text('BLE'),
            actions: [
              IconButton(
                icon: const Icon(Icons.refresh),
                onPressed: isScanning ? null : startScan,
              ),
            ],
          ),
          body: Center(
            child: connectedDevice == null
                ? isScanning
                    ? const CircularProgressIndicator() // 在掃描時顯示進度指示器
                    : const Text('No devices found. Please try scanning again.')
                : Text('Connected to ${connectedDevice!.platformName}'),
          ),
          floatingActionButton: FloatingActionButton(
            onPressed: isScanning ? null : startScan,
            child: const Icon(Icons.refresh),
          ),
        );
      }
    }
  • Flutter Foreground Service 注意事項

    Flutter Foreground Service 注意事項

    使用 flutter foreground task package 時,為了適配 Android 14 以上環境,有幾個需要留意的地方:

    {project root}\android\app\build.gradle

    1. 最低SDK版本至少為 21:minSdkVersion 21
    2. 目標SDK版本至少為34:targetSdkVersion 34

    {project root}\android\app\src\main\AndroidManifest.xml

    1. 權限宣告建議要有系統通知視窗 SYSTEM_ALERT_WINDOW 和忽略電池最佳化 REQUEST_IGNORE_BATTERY_OPTIMIZATIONS:
      <uses-permission android:name=”android.permission.SYSTEM_ALERT_WINDOW” />
      <uses-permission android:name=”android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS” />
    2. 服務 service 參數的名稱 name 就是 flutter foreground task package 的固定名稱:
      android:name=”com.pravera.flutter_foreground_task.service.ForegroundService”
    3. 服務 service 參數一定要定義服務類型 foregroundServiceType
    4. 舉前景服務使用藍芽掃描為例,服務類型為:
      android:foregroundServiceType=”connectedDevice”
      而對應的權限宣告就需要包含前景服務連線裝置 FOREGROUND_SERVICE_CONNECTED_DEVICE、變更網路狀態 CHANGE_NETWORK_STATE 和 藍芽掃描 BLUETOOTH_SCAN:
      <uses-permission android:name=”android.permission.FOREGROUND_SERVICE_CONNECTED_DEVICE” />
      <uses-permission android:name=”android.permission.BLUETOOTH_SCAN” />
      <uses-permission android:name=”android.permission.CHANGE_NETWORK_STATE” />

    lib\main.dart

    前景服務啟動前,記得要動態取得權限,包含 System Alert、Ignore Battery Optimizations 和最重要的 Notification。(以下節錄自 官方的範例程式碼 )

  • Flutter 如何修改 App 的 namespace

    以下為 Flutter 修改 Android namespace 的位置,:

    1. android\app\build.gradle (有2處)
    2. android\app\src\main\AndroidManifest.xml (不一定有)
    3. android\app\src\main\kotlin\… (根據新的namespace修改資料夾名稱)
    4. android\app\src\main\kotlin\…\ManiActivity.kt
  • 修改 Flutter 對應 Android 的 minSdk

    Flutter 在 Android 上支援最低的 SDK 版本為 19 版,預設的 minSdk 參數也是設定成 19。如果因為功能需求等因素需要調升可支援的最低 SDK 版本,可修改以下文件:

    {project root folder} \ android \ app \ build.gradle

    defaultConfig { } 區段,直接異動 minSdkVersion 參數即可。
    (預設 flutter.minSdkVersion 代表 Flutter 可支援的最低版本 )

  • 如何設定 Flutter App 程式的 Icon 圖示

    如何設定 Flutter App 程式的 Icon 圖示

    透過 flutter_launcher_icons 套件,可以方便的自定義 App 各平台的 icon。以下是使用方式:

    1. 針對各平台準備對應所需要的 icon.png 圖檔,解析度方面,除了 Windows 平台有上限 256*256外,其他的平台建議用 1024*1024 即可

    2. 參閱下圖,專案根目錄下建立資料夾路徑 “asset\icon\”,並把各平台對應的 icon 圖檔放在這裡
    (路徑和檔案名稱可以依需要命名)

    3. 在 pubspec.yaml 的 dependencies 加入相依套件 flutter_launcher_icons

    4. 建立對應的 config 設定,這可以直接新增在 pubspec.yaml 也可以為了管理方便於根目錄建立獨立檔案 flutter_launcher_icons.yaml

    5. 參考下面的範例編輯 config 設定內容即可

    flutter_launcher_icons:
    android: "launcher_icon"
    ios: true
    image_path: "assets/icon/icon.png"
    min_sdk_android: 21 # android min sdk min:16, default 21
    web:
    generate: true
    image_path: "assets/icon/icon_web.png"
    background_color: "#000000"
    theme_color: "#000000"
    windows:
    generate: true
    image_path: "assets/icon/icon_windows.png"
    icon_size: 48 # min:48, max:256, default: 48
    macos:
    generate: true
    image_path: "assets/icon/icon_macos.png"

    6. 演示範例