您现在的位置是:网站首页> Flutter

Flutter项目实用技术收集

  • Flutter
  • 2025-06-28
  • 536人已阅读
摘要

Flutter项目实用技术收集



创建一个Flutter项目后,打开MainActivity 出现Can't resolve symbol FlutterActivity 错误

基础知识

Flutter 判断运行平台

Dart基本语法

搭建Fluter Android Studio开发环境

Flutter快速回顾

硬件操作

读写NFC的例子

低功耗蓝牙例子

Android串口通讯

Windows串口通讯

常见问题

Flutter实践问题及经验汇总

Flutter发布应用

技术积累

Flutter重要类参考说明

Flutter实用库收集



读写NFC的例子

在Flutter中读写NFC需要使用第三方插件。一个常用的插件是 nfc_manager。以下是使用这个插件读写NFC的详细例子和说明:

1.首先,在 pubspec.yaml 文件中添加依赖:

dependencies:

  flutter:

    sdk: flutter

  nfc_manager: ^3.2.0


2.在Android的 AndroidManifest.xml 文件中添加权限:

<uses-permission android:name="android.permission.NFC" />

<uses-feature android:name="android.hardware.nfc" android:required="true" />


3.在iOS的 Info.plist 文件中添加权限:

<key>NFCReaderUsageDescription</key>

<string>需要NFC权限来读取和写入NFC标签</string>


4.现在,我们可以开始编写代码了。以下是一个包含读取和写入NFC功能的完整示例:

import 'package:flutter/material.dart';

import 'package:nfc_manager/nfc_manager.dart';


void main() {

  runApp(MyApp());

}


class MyApp extends StatefulWidget {

  @override

  _MyAppState createState() => _MyAppState();

}


class _MyAppState extends State<MyApp> {

  ValueNotifier<dynamic> result = ValueNotifier(null);


  @override

  Widget build(BuildContext context) {

    return MaterialApp(

      home: Scaffold(

        appBar: AppBar(title: Text('NFC Demo')),

        body: SafeArea(

          child: FutureBuilder<bool>(

            future: NfcManager.instance.isAvailable(),

            builder: (context, ss) => ss.data != true

                ? Center(child: Text('NFC 不可用'))

                : Flex(

                    mainAxisAlignment: MainAxisAlignment.spaceBetween,

                    direction: Axis.vertical,

                    children: [

                      Flexible(

                        flex: 2,

                        child: Container(

                          margin: EdgeInsets.all(4),

                          constraints: BoxConstraints.expand(),

                          decoration: BoxDecoration(border: Border.all()),

                          child: SingleChildScrollView(

                            child: ValueListenableBuilder<dynamic>(

                              valueListenable: result,

                              builder: (context, value, _) =>

                                  Text('${value ?? ''}'),

                            ),

                          ),

                        ),

                      ),

                      Flexible(

                        flex: 3,

                        child: GridView.count(

                          padding: EdgeInsets.all(4),

                          crossAxisCount: 2,

                          childAspectRatio: 4,

                          crossAxisSpacing: 4,

                          mainAxisSpacing: 4,

                          children: [

                            ElevatedButton(

                                child: Text('Tag Read'), onPressed: _tagRead),

                            ElevatedButton(

                                child: Text('Ndef Write'),

                                onPressed: _ndefWrite),

                          ],

                        ),

                      ),

                    ],

                  ),

          ),

        ),

      ),

    );

  }


  void _tagRead() {

    NfcManager.instance.startSession(onDiscovered: (NfcTag tag) async {

      result.value = tag.data;

      NfcManager.instance.stopSession();

    });

  }

void _ndefWrite() {

    NfcManager.instance.startSession(onDiscovered: (NfcTag tag) async {

      var ndef = Ndef.from(tag);

      if (ndef == null || !ndef.isWritable) {

        result.value = 'Tag is not ndef writable';

        NfcManager.instance.stopSession(errorMessage: result.value);

        return;

      }


      NdefMessage message = NdefMessage([

        NdefRecord.createText('Hello NFC'),

        NdefRecord.createUri(Uri.parse('https://flutter.dev')),

        NdefRecord.createMime('text/plain', Uint8List.fromList('Hello'.codeUnits)),

        NdefRecord.createExternal(

            'com.example', 'mytype', Uint8List.fromList('mydata'.codeUnits)),

      ]);


      try {

        await ndef.write(message);

        result.value = 'Success to "Ndef Write"';

        NfcManager.instance.stopSession();

      } catch (e) {

        result.value = e;

        NfcManager.instance.stopSession(errorMessage: result.value.toString());

        return;

      }

    });

  }

}

这个例子展示了如何读取NFC标签和写入NDEF消息到NFC标签。让我们详细解释一下这个代码:


1._tagRead 函数:

使用 NfcManager.instance.startSession 开始NFC会话。

当发现NFC标签时,将标签数据存储在 result 中并停止会话。

2._ndefWrite 函数:

同样使用 startSession 开始NFC会话。

检查标签是否支持NDEF格式并且可写。

创建一个 NdefMessage,包含多种类型的 NdefRecord:

文本记录

URI记录

MIME类型记录

外部类型记录

尝试将消息写入标签,如果成功则更新 result 并停止会话。

3.主UI:

使用 FutureBuilder 检查NFC是否可用。

如果NFC可用,显示一个包含结果显示区域和两个按钮的界面。

使用 ValueListenableBuilder 来动态显示 result 的值。


注意事项:

1.NFC功能需要真实设备测试,模拟器无法使用NFC功能。

2.在iOS上,NFC读取功能只在iPhone 7及以上机型支持,而写入功能则需要iPhone XS及以上机型。

3.在Android上,需要确保设备支持NFC并且已经打开NFC功能。

4.不同的NFC标签可能支持不同的操作,在实际应用中需要进行相应的错误处理。

5.NDEF是NFC数据交换格式的标准,但并非所有NFC标签都支持NDEF格式。在处理非NDEF格式的标签时,可能需要使用其他方法。

这个例子提供了NFC读写的基本框架,你可以根据具体需求进行修改和扩展。例如,你可能需要处理不同类型的NFC标签,或者实现更复杂的数据交换逻辑。



低功耗蓝牙例子

首先,你需要在pubspec.yaml文件中添加flutter_blue包:

dependencies:

  flutter_blue: ^0.8.0


然后,这里是一个完整的示例代码:

import 'package:flutter/material.dart';

import 'package:flutter_blue/flutter_blue.dart';


void main() {

  runApp(MyApp());

}


class MyApp extends StatelessWidget {

  @override

  Widget build(BuildContext context) {

    return MaterialApp(

      home: BluetoothApp(),

    );

  }

}


class BluetoothApp extends StatefulWidget {

  @override

  _BluetoothAppState createState() => _BluetoothAppState();

}


class _BluetoothAppState extends State<BluetoothApp> {

  FlutterBlue flutterBlue = FlutterBlue.instance;

  List<ScanResult> scanResults = [];

  bool isScanning = false;

  BluetoothDevice? connectedDevice;

  List<BluetoothService> services = [];


  @override

  void initState() {

    super.initState();

    // 监听蓝牙扫描结果

    flutterBlue.scanResults.listen((results) {

      setState(() {

        scanResults = results;

      });

    });

  }


  // 开始扫描蓝牙设备

  void startScan() {

    setState(() {

      isScanning = true;

      scanResults.clear();

    });

    flutterBlue.startScan(timeout: Duration(seconds: 4));

    Future.delayed(Duration(seconds: 4), () {

      stopScan();

    });

  }


  // 停止扫描

  void stopScan() {

    flutterBlue.stopScan();

    setState(() {

      isScanning = false;

    });

  }


  // 连接到设备

  void connect(BluetoothDevice device) async {

    try {

      await device.connect();

      setState(() {

        connectedDevice = device;

      });

      // 获取服务

      services = await device.discoverServices();

    } catch (e) {

      print('连接失败: $e');

    }

  }


  // 断开连接

  void disconnect() {

    if (connectedDevice != null) {

      connectedDevice!.disconnect();

      setState(() {

        connectedDevice = null;

        services.clear();

      });

    }

  }


  // 读取特征值

  void readCharacteristic(BluetoothCharacteristic characteristic) async {

    try {

      List<int> value = await characteristic.read();

      print('读取值: ${String.fromCharCodes(value)}');

    } catch (e) {

      print('读取失败: $e');

    }

  }


  // 写入特征值

  void writeCharacteristic(BluetoothCharacteristic characteristic, List<int> value) async {

    try {

      await characteristic.write(value);

      print('写入成功');

    } catch (e) {

      print('写入失败: $e');

    }

  }


  @override

  Widget build(BuildContext context) {

    return Scaffold(

      appBar: AppBar(

        title: Text('蓝牙 BLE 示例'),

      ),

      body: Column(

        children: <Widget>[

          ElevatedButton(

            child: Text(isScanning ? '停止扫描' : '开始扫描'),

            onPressed: isScanning ? stopScan : startScan,

          ),

          Expanded(

            child: ListView.builder(

              itemCount: scanResults.length,

              itemBuilder: (context, index) {

                ScanResult result = scanResults[index];

                return ListTile(

                title: Text(result.device.name ?? 'Unknown device'),

                subtitle: Text(result.device.id.toString()),

                trailing: ElevatedButton(

                  child: Text(connectedDevice == result.device ? '断开' : '连接'),

                  onPressed: () {

                    if (connectedDevice == result.device) {

                      disconnect();

                    } else {

                      connect(result.device);

                    }

                  },

                ),

              );

            },

          ),

          if (connectedDevice != null)

            Expanded(

              child: ListView.builder(

                itemCount: services.length,

                itemBuilder: (context, index) {

                  BluetoothService service = services[index];

                  return ExpansionTile(

                    title: Text('Service: ${service.uuid}'),

                    children: service.characteristics.map((c) {

                      return ListTile(

                        title: Text('Characteristic: ${c.uuid}'),

                        subtitle: Text('Properties: ${c.properties}'),

                        trailing: Row(

                          mainAxisSize: MainAxisSize.min,

                          children: <Widget>[

                            if (c.properties.read)

                              IconButton(

                                icon: Icon(Icons.visibility),

                                onPressed: () => readCharacteristic(c),

                              ),

                            if (c.properties.write)

                              IconButton(

                                icon: Icon(Icons.edit),

                                onPressed: () => writeCharacteristic(c, [0x12, 0x34]), // 示例数据

                              ),

                          ],

                        ),

                      );

                    }).toList(),

                  );

                },

              ),

            ),

        ],

      ),

    );

  }

}


这个示例提供了以下功能:

1.扫描附近的BLE设备

2.显示扫描结果列表

3.连接到选定的设备

4.断开连接

5.显示连接设备的服务和特征

6.读取和写入特征值


下面是对代码的详细解释:

1.初始化:

我们使用FlutterBlue.instance获取FlutterBlue的单例。

在initState中,我们监听蓝牙扫描结果。

2.扫描:

startScan方法开始扫描,设置4秒超时。

stopScan方法停止扫描。

3.连接:

connect方法尝试连接到选定的设备,连接成功后发现服务。

disconnect方法断开当前连接的设备。

4.读写操作:

readCharacteristic方法读取特征值。

writeCharacteristic方法写入特征值。

5.UI构建:

主界面包含一个开始/停止扫描的按钮。

扫描结果显示在一个ListView中,每个项目都有一个连接/断开按钮。

当设备连接后,显示其服务和特征。

对于每个特征,如果支持读操作,显示一个"读取"按钮;如果支持写操作,显示一个"写入"按钮。


注意事项:

确保在Android的AndroidManifest.xml和iOS的Info.plist中添加必要的蓝牙权限。

这个示例仅供参考,实际应用中可能需要根据具体的BLE设备协议进行调整。

错误处理在这个示例中比较简单,实际应用中应该更加健



Android串口通讯


首先,我们需要创建一个新的 Flutter 项目。

Flutter 端代码

在 lib/main.dart 文件中:

import 'package:flutter/material.dart';

import 'package:flutter/services.dart';


void main() {

  runApp(MyApp());

}


class MyApp extends StatelessWidget {

  @override

  Widget build(BuildContext context) {

    return MaterialApp(

      home: SerialPortPage(),

    );

  }

}


class SerialPortPage extends StatefulWidget {

  @override

  _SerialPortPageState createState() => _SerialPortPageState();

}


class _SerialPortPageState extends State<SerialPortPage> {

  static const platform = MethodChannel('com.example.serial_port');

  

  String _portStatus = 'Closed';

  String _receivedData = '';

  

  Future<void> _openPort() async {

    try {

      final String result = await platform.invokeMethod('openPort');

      setState(() {

        _portStatus = result;

      });

    } on PlatformException catch (e) {

      setState(() {

        _portStatus = "Failed to open port: '${e.message}'.";

      });

    }

  }


  Future<void> _closePort() async {

    try {

      final String result = await platform.invokeMethod('closePort');

      setState(() {

        _portStatus = result;

      });

    } on PlatformException catch (e) {

      setState(() {

        _portStatus = "Failed to close port: '${e.message}'.";

      });

    }

  }


  Future<void> _sendData() async {

    try {

      await platform.invokeMethod('sendData', {'data': 'Hello, Serial Port!'});

    } on PlatformException catch (e) {

      print("Failed to send data: '${e.message}'.");

    }

  }


  @override

  void initState() {

    super.initState();

    platform.setMethodCallHandler((MethodCall call) async {

      if (call.method == 'receiveData') {

        setState(() {

          _receivedData = call.arguments;

        });

      }

    });

  }


  @override

  Widget build(BuildContext context) {

    return Scaffold(

      appBar: AppBar(

        title: Text('Serial Port Communication'),

      ),

      body: Center(

        child: Column(

          mainAxisAlignment: MainAxisAlignment.center,

          children: <Widget>[

            Text('Port Status: $_portStatus'),

            ElevatedButton(

              child: Text('Open Port'),

              onPressed: _openPort,

            ),

            ElevatedButton(

              child: Text('Close Port'),

              onPressed: _closePort,

            ),

            ElevatedButton(

              child: Text('Send Data'),

              onPressed: _sendData,

            ),

            Text('Received Data: $_receivedData'),

          ],

        ),

      ),

    );

  }

}

Android 端代码:

在 android/app/src/main/kotlin/com/example/your_project_name/MainActivity.kt 文件中:


package com.example.your_project_name

import android.hardware.usb.UsbManager

import android.content.Context

import androidx.annotation.NonNull

import io.flutter.embedding.android.FlutterActivity

import io.flutter.embedding.engine.FlutterEngine

import io.flutter.plugin.common.MethodChannel

import com.hoho.android.usbserial.driver.UsbSerialPort

import com.hoho.android.usbserial

import com.hoho.android.usbserial.driver.UsbSerialProber

import kotlinx.coroutines.*


class MainActivity: FlutterActivity() {

    private val CHANNEL = "com.example.serial_port"

    private var port: UsbSerialPort? = null

    private var readJob: Job? = null


    override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {

        super.configureFlutterEngine(flutterEngine)

        MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler { call, result ->

            when (call.method) {

                "openPort" -> {

                    openPort(result)

                }

                "closePort" -> {

                    closePort(result)

                }

                "sendData" -> {

                    val data = call.argument<String>("data")

                    if (data != null) {

                        sendData(data, result)

                    } else {

                        result.error("INVALID_ARGUMENT", "Data is null", null)

                    }

                }

                else -> {

                    result.notImplemented()

                }

            }

        }

    }


    private fun openPort(result: MethodChannel.Result) {

        val manager = getSystemService(Context.USB_SERVICE) as UsbManager

        val availableDrivers = UsbSerialProber.getDefaultProber().findAllDrivers(manager)

        if (availableDrivers.isEmpty()) {

            result.error("NO_DRIVER", "No available drivers", null)

            return

        }


        val driver = availableDrivers[0]

        val connection = manager.openDevice(driver.device)

        if (connection == null) {

            result.error("CONNECTION_FAILED", "Could not open connection", null)

            return

        }


        port = driver.ports[0]

        try {

            port?.open(connection)

            port?.setParameters(115200, 8, UsbSerialPort.STOPBITS_1, UsbSerialPort.PARITY_NONE)

            

            startReading()

            

            result.success("Port opened successfully")

        } catch (e: Exception) {

            result.error("OPEN_FAILED", "Failed to open port: ${e.message}", null)

        }

    }


    private fun closePort(result: MethodChannel.Result) {

        readJob?.cancel()

        try {

            port?.close()

            port = null

            result.success("Port closed successfully")

        } catch (e: Exception) {

            result.error("CLOSE_FAILED", "Failed to close port: ${e.message}", null)

        }

    }


    private fun sendData(data: String, result: MethodChannel.Result) {

        port?.let {

            try {

                it.write(data.toByteArray(), 1000)

                result.success(null)

            } catch (e: Exception) {

                result.error("WRITE_FAILED", "Failed to write data: ${e.message}", null)

            }

        } ?: result.error("PORT_CLOSED", "Port is not open", null)

    }


    private fun startReading() {

        readJob = CoroutineScope(Dispatchers.IO).launch {

            val buffer = ByteArray(1024)

            while (isActive) {

                try {

                    val bytesRead = port?.read(buffer, 1000) ?: 0

                    if (bytesRead > 0) {

                        val receivedData = String(buffer, 0, bytesRead)

                        withContext(Dispatchers.Main) {

                            MethodChannel(flutterEngine?.dartExecutor?.binaryMessenger, CHANNEL)

                                .invokeMethod("receiveData", receivedData)

                        }

                    }

                } catch (e: Exception) {

                    e.printStackTrace()

                }

            }

        }

    }

}

这个例子使用了 usb-serial-for-android 库来处理 Android 上的串口通信。你需要在 android/app/build.gradle 文件中添加以下依赖:

dependencies {

    implementation 'com.github.mik3y:usb-serial-for-android:3.4.0'

}


现在让我们详细解释这个例子:

Flutter 端:

我们创建了一个 MethodChannel 来与 Android 原生代码通信。

提供了三个主要功能: 打开端口、关闭端口和发送数据。

使用 setMethodCallHandler 来接收来自 Android 的数据。

Android 端:

我们实现了与 Flutter 端对应的三个方法: openPort, closePort, 和 sendData。

openPort 方法查找可用的 USB 串口设备,打开连接,并设置串口参数。

closePort 方法关闭串口连接。

sendData 方法向串口写入数据。

startReading 方法在后台持续读取串口数据,并通过 MethodChannel 发送给 Flutter。

错误处理:

在每个操作中,我们都进行了适当的错误处理,并将错误信息返回给 Flutter。

协程:

我们使用 Kotlin 协程来处理串口的持续读取,这样可以避免阻塞主线程。

权限:

你需要在 android/app/src/main/AndroidManifest.xml 文件中添加 USB 权限:


<manifest ...>

    <uses-feature android:name="android.hardware.usb.host" />

    ...

</manifest>

使用说明:


运行应用后,点击 "Open Port" 按钮来打开串口连接。

使用 "Send Data" 按钮发送预设的数据 ("Hello, Serial Port!")。

接收到的数据会显示在界面上。

完成后,点击 "Close Port" 按钮关闭连接。


注意事项:

这个例子假设只有一个串口设备连接。在实际应用中,你可能需要处理多个设备的情况。

串口参数 (波特率等) 是固定的。在实际应用中,你可能需要让用户能够配置这些参数。

这个例子只实现了 Android 端。如果你需要在 iOS 上使用串口,你需要编写相应的 iOS 原生代码。

在生产环境中,你可能需要添加更多的错误处理和日志记录。

这个例子使用了 USB 串口。如果你需要使用蓝牙串口或其他类型的串口,可能需要使用不同的库和 API。

最后,记得在使用 USB 设备时,要处理 USB 权限请求。你可能需要实现一个 BroadcastReceiver 来处理 UsbManager.ACTION_USB_DEVICE_ATTACHED 和 UsbManager.ACTION_USB_DEVICE_DETACHED 事件,以便在设备连接和断开时做出相应的处理。


当然,让我们继续深入讨论这个例子,并提供一些额外的建议和改进:

USB 权限处理:

在实际应用中,你需要处理 USB 权限请求。这里是一个简单的实现:


在 MainActivity.kt 中添加:



private val ACTION_USB_PERMISSION = "com.example.your_project_name.USB_PERMISSION"


private val usbReceiver = object : BroadcastReceiver() {

    override fun onReceive(context: Context, intent: Intent) {

        if (ACTION_USB_PERMISSION == intent.action) {

            synchronized(this) {

                val device: UsbDevice? = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE)

                if (intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false)) {

                    device?.apply {

                        // 权限被授予,可以打开设备

                        openPort(null)

                    }

                } else {

                    // 权限被拒绝

                    Log.d("USB", "permission denied for device $device")

                }

            }

        }

    }

}


override fun onCreate(savedInstanceState: Bundle?) {

    super.onCreate(savedInstanceState)

    val filter = IntentFilter(ACTION_USB_PERMISSION)

    registerReceiver(usbReceiver, filter)

}


override fun onDestroy() {

    super.onDestroy()

    unregisterReceiver(usbReceiver)

}


private fun requestUsbPermission() {

    val manager = getSystemService(Context.USB_SERVICE) as UsbManager

    val availableDrivers = UsbSerialProber.getDefaultProber().findAllDrivers(manager)

    if (availableDrivers.isEmpty()) {

        return

    }

    val driver = availableDrivers[0]

    val permissionIntent = PendingIntent.getBroadcast(this, 0, Intent(ACTION_USB_PERMISSION), 0)

    manager.requestPermission(driver.device, permissionIntent)

}

多设备处理:

如果你需要处理多个串口设备,你可以修改 openPort 方法来显示一个设备选择对话框:

private fun openPort(result: MethodChannel.Result?) {

    val manager = getSystemService(Context.USB_SERVICE) as UsbManager

    val availableDrivers = UsbSerialProber.getDefaultProber().findAllDrivers(manager)

    if (availableDrivers.isEmpty()) {

        result?.error("NO_DRIVER", "No available drivers", null)

        return

    }


    AlertDialog.Builder(this)

        .setTitle("Select a device")

        .setItems(availableDrivers.map { it.device.deviceName }.toTypedArray()) { _, which ->

            val driver = availableDrivers[which]

            val connection = manager.openDevice(driver.device)

            if (connection == null) {

                result?.error("CONNECTION_FAILED", "Could not open connection", null)

                return@setItems

            }


            port = driver.ports[0]

            try {

                port?.open(connection)

                port?.setParameters(115200, 8, UsbSerialPort.STOPBITS_1, UsbSerialPort.PARITY_NONE)

                

                startReading()

                

                result?.success("Port opened successfully")

            } catch (e: Exception) {

                result?.error("OPEN_FAILED", "Failed to open port: ${e.message}", null)

            }

        }

        .show()

}


可配置的串口参数 (续):

在 Flutter 端,我们可以添加一个设置页面:

class SettingsPage extends StatefulWidget {

  @override

  _SettingsPageState createState() => _SettingsPageState();

}


class _SettingsPageState extends State<SettingsPage> {

  int _baudRate = 115200;


  @override

  Widget build(BuildContext context) {

    return Scaffold(

      appBar: AppBar(title: Text('Settings')),

      body: ListView(

        children: [

          ListTile(

            title: Text('Baud Rate'),

            trailing: DropdownButton<int>(

              value: _baudRate,

              items: [9600, 19200, 38400, 57600, 115200].map((int value) {

                return DropdownMenuItem<int>(

                  value: value,

                  child: Text(value.toString()),

                );

              }).toList(),

              onChanged: (int? newValue) {

                if (newValue != null) {

                  setState(() {

                    _baudRate = newValue;

                  });

                }

              },

            ),

          ),

        ],

      ),

    );

  }

}

然后在主页面添加一个按钮来打开设置页面,并在打开端口时传入波特率:

ElevatedButton(

  child: Text('Settings'),

  onPressed: () async {

    await Navigator.push(

      context,

      MaterialPageRoute(builder: (context) => SettingsPage()),

    );

  },

),


// 在 _openPort 方法中:

final int result = await platform.invokeMethod('openPort', {'baudRate': _baudRate});

在 Android 端,修改 openPort 方法以接受波特率参数:



private fun openPort(call: MethodCall, result: MethodChannel.Result) {

    val baudRate = call.argument<Int>("baudRate") ?: 115200

    // ... 其他代码保持不变

    port?.setParameters(baudRate, 8, UsbSerialPort.STOPBITS_1, UsbSerialPort.PARITY_NONE)

    // ...

}

添加数据发送输入框:

在 Flutter 端,我们可以添加一个输入框让用户输入要发送的数据:

class _SerialPortPageState extends State<SerialPortPage> {

  TextEditingController _dataController = TextEditingController();


  // ... 其他代码保持不变


  Future<void> _sendData() async {

    try {

      await platform.invokeMethod('sendData', {'data': _dataController.text});

      _dataController.clear();

    } on PlatformException catch (e) {

      print("Failed to send data: '${e.message}'.");

    }

  }


  @override

  Widget build(BuildContext context) {

    return Scaffold(

      // ... 其他代码保持不变

      body: Column(

        children: <Widget>[

          // ... 其他控件

          Padding(

            padding: EdgeInsets.all(8.0),

            child: TextField(

              controller: _dataController,

              decoration: InputDecoration(

                labelText: 'Data to send',

                border: OutlineInputBorder(),

              ),

            ),

          ),

          ElevatedButton(

            child: Text('Send Data'),

            onPressed: _sendData,

          ),

          // ... 其他控件

        ],

      ),

    );

  }

}

添加数据接收显示区域:

我们可以使用一个 ListView 来显示接收到的数据:

class _SerialPortPageState extends State<SerialPortPage> {

  List<String> _receivedDataList = [];

  ScrollController _scrollController = ScrollController();


  // ... 其他代码保持不变


  @override

  void initState() {

    super.initState();

    platform.setMethodCallHandler((MethodCall call) async {

      if (call.method == 'receiveData') {

        setState(() {

          _receivedDataList.add(call.arguments);

        });

        // 滚动到列表底部

        WidgetsBinding.instance.addPostFrameCallback((_) {

          if (_scrollController.hasClients) {

            _scrollController.animateTo(

              _scrollController.position.maxScrollExtent,

              duration: Duration(milliseconds: 300),

              curve: Curves.easeOut,

            );

          }

        });

      }

    });

  }


  @override

  Widget build(BuildContext context) {

    return Scaffold(

      appBar: AppBar(title: Text('Serial Port Communication')),

      body: Column(

        children: <Widget>[

          // ... 其他控件

          Expanded(

            child: ListView.builder(

              controller: _scrollController,

              itemCount: _receivedDataList.length,

              itemBuilder: (context, index) {

                return ListTile(

                  title: Text(_receivedDataList[index]),

                );

              },

            ),

          ),

          // ... 其他控件

        ],

      ),

    );

  }

}


添加清除接收数据的功能:

ElevatedButton(

  child: Text('Clear Received Data'),

  onPressed: () {

    setState(() {

      _receivedDataList.clear();

    });

  },

),


添加数据发送格式选择:

我们可以让用户选择发送数据的格式 (ASCII 或 HEX):

enum DataFormat { ASCII, HEX }


class _SerialPortPageState extends State<SerialPortPage> {

  DataFormat _dataFormat = DataFormat.ASCII;


  // ... 其他代码保持不变


  Future<void> _sendData() async {

    try {

      String dataToSend = _dataController.text;

      if (_dataFormat == DataFormat.HEX) {

        // 将 HEX 字符串转换为字节数组

        dataToSend = String.fromCharCodes(

          _dataController.text.split(' ').map((e) => int.parse(e, radix: 16))

        );

      }

      await platform.invokeMethod('sendData', {'data': dataToSend});

      _dataController.clear();

    } on PlatformException catch (e) {

      print("Failed to send data: '${e.message}'.");

    }

  }


  @override

  Widget build(BuildContext context) {

    return Scaffold(

      // ... 其他代码保持不变

      body: Column(

        children: <Widget>[

          // ... 其他控件

          Row(

            children: [

              Text('Data Format:'),

              Radio<DataFormat>(

                value: DataFormat.ASCII,

                groupValue: _dataFormat,

                onChanged: (DataFormat? value) {

                  setState(() {

                    _dataFormat = value!;

                  });

                },

              ),

              Text('ASCII'),

              Radio<DataFormat>(

                value: DataFormat.HEX,

                groupValue: _dataFormat,

                onChanged: (DataFormat? value) {

                  setState(() {

                    _dataFormat = value!;

                  });

                },

              ),

              Text('HEX'),

            ],

          ),

          // ... 其他控件

        ],

      ),

    );

  }

}


添加数据接收格式显示:

我们可以让用户选择以 ASCII 或 HEX 格式查看接收到的数据:

enum DisplayFormat { ASCII, HEX }


class _SerialPortPageState extends State<SerialPortPage> {

  DisplayFormat _displayFormat = DisplayFormat.ASCII;


  // ... 其他代码保持不变


  String _formatReceivedData(String data) {

    if (_displayFormat == DisplayFormat.ASCII) {

      return data;

    } else {

      return data.codeUnits.map((e) => e.toRadixString(16).padLeft(2, '0')).join(' ');

    }

  }


  @override

  Widget build(BuildContext context) {

    return Scaffold(

      // ... 其他代码保持不变

      body: Column(

        children: <Widget>[

          // ... 其他控件

          Row(

            children: [

              Text('Display Format:'),

              Radio<DisplayFormat>(

                value: DisplayFormat.ASCII,

                groupValue: _displayFormat,

                onChanged: (DisplayFormat? value) {

                  setState(() {

                    _displayFormat = value!;

                  });

                },

              ),

              Text('ASCII'),

              Radio<DisplayFormat>(

                value: DisplayFormat.HEX,

                groupValue: _displayFormat,

                onChanged: (DisplayFormat? value) {

                  setState(() {

                    _displayFormat = value!;

                  });

                },

              ),

              Text('HEX'),

            ],

          ),

          Expanded(

            child: ListView.builder(

              controller: _scrollController,

              itemCount: _receivedDataList.length,

              itemBuilder: (context, index) {

                return ListTile(

                  title: Text(_formatReceivedData(_receivedDataList[index])),

                );

              },

            ),

          ),

          // ... 其他控件

        ],

      ),

    );

  }

}


添加自动重连功能:

在 Android 端,我们可以添加一个自动重连的功能:

private var autoReconnect = false

private var reconnectJob: Job? = null


private fun startAutoReconnect() {

    autoReconnect = true

    reconnectJob = CoroutineScope(Dispatchers.IO).launch {

        while (isActive && autoReconnect) {

            if (port == null || !port!!.isOpen) {

                try {

                    openPort(null)

                } catch (e: Exception) {

                    Log.e("SerialPort", "Failed to reconnect: ${e.message}")

                }

            }

            delay(5000) // 每5秒尝试重连一次

        }

    }

}


private fun stopAutoReconnect() {

    autoReconnect = false

    reconnectJob?.cancel()

}


在 Flutter 端,我们可以添加一个开关来控制自动重连:

bool _autoReconnect = false;


// ... 其他代码保持不变


Switch(

  value: _autoReconnect,

  onChanged: (value) {

    setState(() {

      _autoReconnect = value;

    });

    platform.invokeMethod('setAutoReconnect', {'enabled': value});

  },

  title: Text('Auto Reconnect'),

),


添加数据记录功能:

我们可以添加一个功能来将接收到的数据保存到文件中:

import 'dart:io';

import 'package:path_provider/path_provider.dart';


// ... 其他代码保持不变


Future<void> _saveDataToFile() async {

  final directory = await getApplicationDocumentsDirectory();

  final file = File('${directory.path}/serial_data_${DateTime.now().millisecondsSinceEpoch}.txt');

  

  String dataToSave = _receivedDataList.join('\n');

  await file.writeAsString(dataToSave);

  

  ScaffoldMessenger.of(context).showSnackBar(

    SnackBar(content: Text('Data saved to ${file.path}')),

  );

}


// 在 build 方法中添加一个保存按钮

ElevatedButton(

  child: Text('Save Data'),

  onPressed: _saveDataToFile,

),

注意: 你需要在 pubspec.yaml 文件中添加 path_provider 依赖。


添加数据统计功能:

我们可以添加一个简单的数据统计功能,显示接收和发送的字节数:

class _SerialPortPageState extends State<SerialPortPage> {

  int _receivedBytes = 0;

  int _sentBytes = 0;


  // ... 其他代码保持不变


  @override

  void initState() {

    super.initState();

    platform.setMethodCallHandler((MethodCall call) async {

      if (call.method == 'receiveData') {

        setState(() {

          _receivedDataList.add(call.arguments);

          _receivedBytes += call.arguments.length;

        });

        // ... 其他代码保持不变

      }

    });

  }


  Future<void> _sendData() async {

    // ... 其他代码保持不变

    setState(() {

      _sentBytes += dataToSend.length;

    });

    // ... 其他代码保持不变

  }


  @override

  Widget build(BuildContext context) {

    return Scaffold(

      // ... 其他代码保持不变

      body: Column(

        children: <Widget>[

          // ... 其他控件

          Text('Received: $_receivedBytes bytes, Sent: $_sentBytes bytes'),

          // ... 其他控件

        ],

      ),

    );

  }

}


添加连接状态指示器:

我们可以添加一个简单的指示器来显示当前的连接状态:

class _SerialPortPageState extends State<SerialPortPage> {

  bool _isConnected = false;


  // ... 其他代码保持不变


  Future<void> _openPort() async {

    try {

      final bool result = await platform.invokeMethod('openPort');

      setState(() {

        _isConnected = result;

      });

    } on PlatformException catch (e) {

      // ... 错误处理

    }

  }


  Future<void> _closePort() async {

    try {

      final bool result = await platform.invokeMethod('closePort');

      setState(() {

        _isConnected = !result;

      });

    } on PlatformException catch (e) {

      // ... 错误处理

    }

  }


  @override

  Widget build(BuildContext context) {

    return Scaffold(

      appBar: AppBar(

        title: Text('Serial Port Communication'),

        actions: [

          Icon(_isConnected ? Icons.link : Icons.link_off),

        ],

      ),

      // ... 其他代码保持不变

    );

  }

}



Windows串口通讯

对于 Windows 端的串口通信,我们可以使用 dart:ffi 来调用 Windows API 进行串口操作。这里是一个基本的实现示例:

首先,在 pubspec.yaml 文件中添加以下依赖:

dependencies:

  ffi: ^2.0.1

  win32: ^3.0.0


然后,创建一个新的 Dart 文件,比如 windows_serial_port.dart:

import 'dart:ffi';

import 'package:ffi/ffi.dart';

import 'package:win32/win32.dart';


class WindowsSerialPort {

  int? _handle;

  bool _isOpen = false;


  bool open(String portName, int baudRate) {

    final lpFileName = TEXT(portName);

    _handle = CreateFile(

      lpFileName,

      GENERIC_READ | GENERIC_WRITE,

      0,

      nullptr,

      OPEN_EXISTING,

      FILE_ATTRIBUTE_NORMAL,

      NULL,

    );


    if (_handle == INVALID_HANDLE_VALUE) {

      print('Failed to open port');

      return false;

    }


    var dcb = calloc<DCB>();

    GetCommState(_handle!, dcb);

    dcb.ref.BaudRate = baudRate;

    dcb.ref.ByteSize = 8;

    dcb.ref.StopBits = ONESTOPBIT;

    dcb.ref.Parity = NOPARITY;

    SetCommState(_handle!, dcb);

    free(dcb);


    var timeouts = calloc<COMMTIMEOUTS>();

    GetCommTimeouts(_handle!, timeouts);

    timeouts.ref.ReadIntervalTimeout = 50;

    timeouts.ref.ReadTotalTimeoutConstant = 50;

    timeouts.ref.ReadTotalTimeoutMultiplier = 10;

    timeouts.ref.WriteTotalTimeoutConstant = 50;

    timeouts.ref.WriteTotalTimeoutMultiplier = 10;

    SetCommTimeouts(_handle!, timeouts);

    free(timeouts);


    _isOpen = true;

    return true;

  }


  void close() {

    if (_handle != null) {

      CloseHandle(_handle!);

      _handle = null;

      _isOpen = false;

    }

  }


  int write(List<int> data) {

    if (!_isOpen) return 0;

    final buffer = calloc<Uint8>(data.length);

    for (var i = 0; i < data.length; i++) {

      buffer[i] = data[i];

    }

    int bytesWritten = 0;

    final lpNumberOfBytesWritten = calloc<Uint32>();

    WriteFile(_handle!, buffer, data.length, lpNumberOfBytesWritten, nullptr);

    bytesWritten = lpNumberOfBytesWritten.value;

    free(lpNumberOfBytesWritten);

    free(buffer);

    return bytesWritten;

  }


  List<int> read(int maxLength) {

    if (!_isOpen) return [];

    final buffer = calloc<Uint8>(maxLength);

    final lpNumberOfBytesRead = calloc<Uint32>();

    ReadFile(_handle!, buffer, maxLength, lpNumberOfBytesRead, nullptr);

    final bytesRead = lpNumberOfBytesRead.value;

    final result = List<int>.generate(bytesRead, (index) => buffer[index]);

    free(lpNumberOfBytesRead);

    free(buffer);

    return result;

  }


  bool get isOpen => _isOpen;

}


现在,你可以在你的 Flutter 应用中使用这个类:

import 'package:flutter/material.dart';

import 'windows_serial_port.dart';


void main() {

  runApp(MyApp());

}

class MyApp extends StatelessWidget {

  @override

  Widget build(BuildContext context) {

    return MaterialApp(

      home: SerialPortPage(),

    );

  }

}


class SerialPortPage extends StatefulWidget {

  @override

  _SerialPortPageState createState() => _SerialPortPageState();

}


class _SerialPortPageState extends State<SerialPortPage> {

  final WindowsSerialPort _serialPort = WindowsSerialPort();

  final TextEditingController _portController = TextEditingController(text: 'COM1');

  final TextEditingController _baudRateController = TextEditingController(text: '9600');

  final TextEditingController _sendDataController = TextEditingController();

  String _receivedData = '';


  @override

  void dispose() {

    _serialPort.close();

    super.dispose();

  }


  void _openPort() {

    if (_serialPort.open(_portController.text, int.parse(_baudRateController.text))) {

      setState(() {});

      _startReading();

    } else {

      ScaffoldMessenger.of(context).showSnackBar(

        SnackBar(content: Text('Failed to open port')),

      );

    }

  }


  void _closePort() {

    _serialPort.close();

    setState(() {});

  }


  void _sendData() {

    if (_serialPort.isOpen) {

      final data = _sendDataController.text.codeUnits;

      _serialPort.write(data);

      _sendDataController.clear();

    }

  }


  void _startReading() {

    Future.doWhile(() async {

      await Future.delayed(Duration(milliseconds: 100));

      if (_serialPort.isOpen) {

        final data = _serialPort.read(1024);

        if (data.isNotEmpty) {

          setState(() {

            _receivedData += String.fromCharCodes(data);

          });

        }

        return true;

      }

      return false;

    });

  }


  @override

  Widget build(BuildContext context) {

    return Scaffold(

      appBar: AppBar(

        title: Text('Windows Serial Port'),

      ),

      body: Padding(

        padding: EdgeInsets.all(16.0),

        child: Column(

          crossAxisAlignment: CrossAxisAlignment.start,

          children: [

            TextField(

              controller: _portController,

              decoration: InputDecoration(labelText: 'Port Name'),

            ),

            TextField(

              controller: _baudRateController,

              decoration: InputDecoration(labelText: 'Baud Rate'),

              keyboardType: TextInputType.number,

            ),

            SizedBox(height: 16),

            Row(

              children: [

                ElevatedButton(

                  onPressed: _serialPort.isOpen ? _closePort : _openPort,

                  child: Text(_serialPort.isOpen ? 'Close Port' : 'Open Port'),

                ),

                SizedBox(width: 16),

                Text(_serialPort.isOpen ? 'Connected' : 'Disconnected'),

              ],

            ),

            SizedBox(height: 16),

            TextField(

              controller: _sendDataController,

              decoration: InputDecoration(labelText: 'Data to Send'),

            ),

            ElevatedButton(

              onPressed: _sendData,

              child: Text('Send Data'),

            ),

            SizedBox(height: 16),

            Text('Received Data:'),

            Expanded(

              child: SingleChildScrollView(

                child: Text(_receivedData),

              ),

            ),

          ],

        ),

      ),

    );

  }

}

这个示例提供了以下功能:

输入端口名称和波特率

打开和关闭串口

发送数据

接收并显示数据



Flutter 判断运行平台

import 'dart:io'; // 导入 dart:io 库

void checkPlatform() {

  if (Platform.isIOS) {

    print('当前平台是:iOS'); // 如果是 iOS,则输出对应信息

  } else if (Platform.isAndroid) {

    print('当前平台是:Android'); // 如果是 Android,则输出对应信息

  } else {

    print('当前平台不支持'); // 如果是其他平台,则输出不支持信息

  }

}









Top