鸿蒙 Flutter 开发:从方舟卡片集成到跨设备交互实战

鸿蒙系统的方舟卡片(服务卡片) 是其核心特色之一,能够让应用功能以轻量化的形式展现在手机桌面,提升用户触达效率;而跨设备交互则是鸿蒙分布式能力的核心体现。此前我们探讨了鸿蒙 Flutter 的基础开发、分布式能力集成和混合开发,本文将聚焦鸿蒙方舟卡片与 Flutter 的集成以及Flutter 应用的跨设备交互,通过完整代码案例,带你实现鸿蒙 Flutter 应用的轻量化和分布式体验升级。

一、鸿蒙方舟卡片与 Flutter 集成的核心逻辑

方舟卡片是鸿蒙系统的原生轻量化组件,运行在桌面端,而 Flutter 应用是独立的跨端应用。两者的集成主要通过以下两种方式实现:

  1. 数据联动:Flutter 应用更新数据后,通过鸿蒙的分布式数据管理事件通知同步到方舟卡片。
  2. 交互跳转:点击方舟卡片,跳转到 Flutter 应用的对应页面,实现轻量化展示与完整功能的联动。
  3. Flutter 驱动卡片:通过鸿蒙原生代码封装 Flutter 逻辑,间接驱动方舟卡片的内容更新(本文重点采用此方案)。

需要注意的是,方舟卡片目前暂不支持直接运行 Flutter 代码,因此需通过Flutter 与鸿蒙原生的通信桥梁,实现 Flutter 应用与方舟卡片的双向联动。

二、案例 1:Flutter 应用与鸿蒙方舟卡片的数据联动

本案例将实现一个天气信息展示的场景:Flutter 应用获取并更新天气数据,鸿蒙方舟卡片实时展示该数据;点击方舟卡片,跳转到 Flutter 应用的天气详情页面。

前置条件

  1. 已配置鸿蒙 DevEco Studio 和 Flutter 环境,支持鸿蒙 SDK 7.0 及以上版本。
  2. 已创建鸿蒙 Flutter 项目,并开启分布式数据管理能力(参考前文分布式数据管理配置)。
  3. 已掌握鸿蒙方舟卡片的基础开发流程(创建卡片模板、配置卡片信息)。

步骤 1:配置鸿蒙方舟卡片基础结构

首先在鸿蒙原生项目中创建方舟卡片,这里以静态卡片 + 动态数据更新为例。

1. 配置卡片信息(config.json)

entry > src > main > config.json中添加方舟卡片的配置:

{
  "module": {
    "abilities": [
      {
        "name": ".MainAbility",
        "type": "page",
        "visible": true,
        "skills": [
          {
            "entities": [
              "entity.system.home"
            ],
            "actions": [
              "action.system.home"
            ]
          }
        ]
      },
      {
        "name": ".WeatherCardAbility",
        "type": "service",
        "visible": true,
        "metadata": [
          {
            "name": "ohos.extra.param.app.type",
            "value": "service"
          }
        ],
        "skills": [
          {
            "entities": [
              "entity.system.servicecard"
            ],
            "actions": [
              "action.system.servicecard"
            ]
          }
        ]
      }
    ],
    "serviceCards": [
      {
        "name": "weather_card",
        "description": "天气信息卡片",
        "icon": "$media:icon",
        "preview": "$media:preview",
        "styles": [
          {
            "name": "style_1",
            "dimensions": "2*2",
            "description": "2x2尺寸的天气卡片"
          }
        ],
        "abilities": [
          {
            "name": ".WeatherCardAbility"
          }
        ]
      }
    ]
  }
}
2. 创建方舟卡片布局(weather_card.xml)

entry > src > main > resources > base > layout中创建卡片布局文件:

<?xml version="1.0" encoding="utf-8"?>
<DirectionalLayout
    xmlns:ohos="http://schemas.huawei.com/res/ohos"
    ohos:width="match_parent"
    ohos:height="match_parent"
    ohos:orientation="vertical"
    ohos:padding="10vp">

    <Text
        ohos:id="$+id:city_name"
        ohos:width="match_parent"
        ohos:height="40vp"
        ohos:text="北京"
        ohos:text_size="18fp"
        ohos:text_alignment="center"/>

    <Text
        ohos:id="$+id:weather_info"
        ohos:width="match_parent"
        ohos:height="40vp"
        ohos:text="晴 25℃"
        ohos:text_size="16fp"
        ohos:text_alignment="center"/>

    <Text
        ohos:id="$+id:update_time"
        ohos:width="match_parent"
        ohos:height="30vp"
        ohos:text="更新时间:2025-06-01 10:00"
        ohos:text_size="12fp"
        ohos:text_alignment="center"
        ohos:text_color="#999999"/>

</DirectionalLayout>

步骤 2:鸿蒙原生端实现卡片数据管理与跳转逻辑

创建WeatherCardAbility.java,负责方舟卡片的数据源管理,并实现点击卡片跳转到 Flutter 应用的逻辑:

package com.example.harmonyflutter;

import ohos.aafwk.ability.Ability;
import ohos.aafwk.ability.ServiceCardAbility;
import ohos.aafwk.content.Intent;
import ohos.aafwk.content.Operation;
import ohos.data.distributed.common.KvStoreException;
import ohos.data.distributed.user.SingleKvStore;
import ohos.data.distributed.user.KvStoreManager;
import ohos.data.distributed.user.KvStoreManagerFactory;
import ohos.data.distributed.user.Options;
import ohos.hiviewdfx.HiLog;
import ohos.hiviewdfx.HiLogLabel;
import ohos.agp.components.Component;
import ohos.agp.components.Text;
import java.util.Optional;

public class WeatherCardAbility extends ServiceCardAbility {
    private static final HiLogLabel LABEL = new HiLogLabel(HiLog.LOG_APP, 0, "WeatherCardAbility");
    private static final String KV_STORE_NAME = "weather_card_store";
    private static final String WEATHER_KEY = "weather_data";
    private SingleKvStore kvStore;

    @Override
    public void onStart(Intent intent) {
        super.onStart(intent);
        // 初始化分布式KvStore,用于接收Flutter传递的天气数据
        initKvStore();
    }

    @Override
    protected Component onRender(Intent intent) {
        // 加载卡片布局
        Component cardLayout = super.loadLayout(intent, "$layout:weather_card");
        if (cardLayout == null) {
            return null;
        }

        // 从KvStore获取天气数据并更新卡片
        updateWeatherCard(cardLayout);

        // 设置卡片点击事件:跳转到Flutter应用
        cardLayout.setClickedListener(component -> {
            // 跳转到Flutter应用的MainAbility
            Intent flutterIntent = new Intent();
            Operation operation = new Intent.OperationBuilder()
                    .withBundleName("com.example.harmonyflutter")
                    .withAbilityName("com.example.harmonyflutter.MainAbility")
                    .build();
            flutterIntent.setOperation(operation);
            startAbility(flutterIntent);
        });

        return cardLayout;
    }

    /**
     * 初始化分布式KvStore
     */
    private void initKvStore() {
        try {
            KvStoreManager manager = KvStoreManagerFactory.getInstance().createKvStoreManager(getContext());
            Options options = new Options();
            options.setCreateIfMissing(true);
            options.setKvStoreType(Options.KvStoreType.SINGLE_VERSION);
            Optional<SingleKvStore> optional = manager.getKvStore(options, KV_STORE_NAME);
            if (optional.isPresent()) {
                kvStore = optional.get();
                // 监听KvStore数据变化,实时更新卡片
                kvStore.registerChangeListener((key, value) -> {
                    if (key.equals(WEATHER_KEY)) {
                        // 重新渲染卡片
                        refreshCard();
                    }
                });
                HiLog.info(LABEL, "KvStore初始化成功");
            }
        } catch (KvStoreException e) {
            HiLog.error(LABEL, "KvStore初始化失败:%{public}s", e.getMessage());
        }
    }

    /**
     * 更新卡片天气数据
     */
    private void updateWeatherCard(Component cardLayout) {
        if (kvStore == null || !kvStore.containsKey(WEATHER_KEY)) {
            return;
        }

        try {
            // 从KvStore获取天气数据(格式:城市|天气|温度|更新时间)
            String weatherData = kvStore.getString(WEATHER_KEY);
            String[] dataArray = weatherData.split("\\|");
            if (dataArray.length < 4) {
                return;
            }

            // 更新卡片文本
            Text cityName = (Text) cardLayout.findComponentById(ResourceTable.Id_city_name);
            Text weatherInfo = (Text) cardLayout.findComponentById(ResourceTable.Id_weather_info);
            Text updateTime = (Text) cardLayout.findComponentById(ResourceTable.Id_update_time);

            if (cityName != null) {
                cityName.setText(dataArray[0]);
            }
            if (weatherInfo != null) {
                weatherInfo.setText(dataArray[1] + " " + dataArray[2]);
            }
            if (updateTime != null) {
                updateTime.setText("更新时间:" + dataArray[3]);
            }
        } catch (Exception e) {
            HiLog.error(LABEL, "更新卡片失败:%{public}s", e.getMessage());
        }
    }
}

步骤 3:Flutter 端实现天气数据更新与同步

在 Flutter 项目中,创建天气数据页面,通过MethodChannel将天气数据同步到鸿蒙原生的分布式 KvStore,进而更新方舟卡片。

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:intl/intl.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: '鸿蒙Flutter天气应用',
      theme: ThemeData(primarySwatch: Colors.blue),
      home: const WeatherPage(),
    );
  }
}

class WeatherPage extends StatefulWidget {
  const WeatherPage({super.key});

  @override
  State<WeatherPage> createState() => _WeatherPageState();
}

class _WeatherPageState extends State<WeatherPage> {
  // 天气数据
  String _city = "北京";
  String _weather = "晴";
  String _temperature = "25℃";
  // 与原生通信的MethodChannel
  static const MethodChannel _channel = MethodChannel('com.example.weather_card');

  // 同步天气数据到鸿蒙KvStore,更新方舟卡片
  Future<void> _syncWeatherToCard() async {
    try {
      // 获取当前时间
      String updateTime = DateFormat('yyyy-MM-dd HH:mm').format(DateTime.now());
      // 拼接数据:城市|天气|温度|更新时间
      String weatherData = "$_city|$_weather|$_temperature|$updateTime";
      // 调用原生方法同步数据
      await _channel.invokeMethod('syncWeatherData', {'weatherData': weatherData});
      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(content: Text('天气数据已同步到方舟卡片!')),
      );
    } on PlatformException catch (e) {
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text('同步失败:${e.message}')),
      );
    }
  }

  // 模拟切换城市和天气
  void _changeWeather() {
    setState(() {
      if (_city == "北京") {
        _city = "上海";
        _weather = "多云";
        _temperature = "28℃";
      } else if (_city == "上海") {
        _city = "广州";
        _weather = "雷阵雨";
        _temperature = "30℃";
      } else {
        _city = "北京";
        _weather = "晴";
        _temperature = "25℃";
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('鸿蒙Flutter天气')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text(
              _city,
              style: const TextStyle(fontSize: 32, fontWeight: FontWeight.bold),
            ),
            const SizedBox(height: 20),
            Text(
              _weather,
              style: const TextStyle(fontSize: 24, color: Colors.grey),
            ),
            const SizedBox(height: 10),
            Text(
              _temperature,
              style: const TextStyle(fontSize: 20),
            ),
            const SizedBox(height: 40),
            Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                ElevatedButton(
                  onPressed: _changeWeather,
                  child: const Text('切换天气'),
                ),
                const SizedBox(width: 20),
                ElevatedButton(
                  onPressed: _syncWeatherToCard,
                  child: const Text('同步到方舟卡片'),
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }
}

步骤 4:鸿蒙原生端实现数据接收方法

MainAbilitySlice.java中添加接收 Flutter 数据并存储到 KvStore 的方法:

// 新增MethodChannel处理天气数据同步
private static final String WEATHER_CHANNEL = "com.example.weather_card";
private SingleKvStore weatherKvStore;

// 在onStart方法中初始化
@Override
public void onStart(Intent intent) {
    super.onStart(intent);
    // 初始化Flutter引擎
    FlutterEngine flutterEngine = new FlutterEngine(getContext());
    // 注册天气数据同步的MethodChannel
    new MethodChannel(flutterEngine.getDartExecutor().getBinaryMessenger(), WEATHER_CHANNEL)
            .setMethodCallHandler((call, result) -> {
                if (call.method.equals("syncWeatherData")) {
                    String weatherData = call.argument("weatherData");
                    syncWeatherDataToKvStore(weatherData, result);
                } else {
                    result.notImplemented();
                }
            });
    flutterEngine.getDartExecutor().executeDartEntrypoint(
            DartExecutor.DartEntrypoint.createDefault()
    );
    // 初始化天气KvStore
    initWeatherKvStore();
}

/**
 * 初始化天气KvStore
 */
private void initWeatherKvStore() {
    try {
        KvStoreManager manager = KvStoreManagerFactory.getInstance().createKvStoreManager(getContext());
        Options options = new Options();
        options.setCreateIfMissing(true);
        options.setKvStoreType(Options.KvStoreType.SINGLE_VERSION);
        Optional<SingleKvStore> optional = manager.getKvStore(options, "weather_card_store");
        if (optional.isPresent()) {
            weatherKvStore = optional.get();
        }
    } catch (KvStoreException e) {
        HiLog.error(LABEL, "天气KvStore初始化失败:%{public}s", e.getMessage());
    }
}

/**
 * 将Flutter传递的天气数据存储到KvStore
 */
private void syncWeatherDataToKvStore(String weatherData, MethodChannel.Result result) {
    if (weatherKvStore == null) {
        result.error("KV_STORE_NULL", "天气存储初始化失败", null);
        return;
    }
    try {
        weatherKvStore.putString("weather_data", weatherData);
        result.success(null);
    } catch (Exception e) {
        result.error("SYNC_FAILED", e.getMessage(), null);
    }
}

案例说明

  1. 方舟卡片端:通过分布式 KvStore 监听数据变化,实时更新卡片上的天气信息;点击卡片时,跳转到 Flutter 应用的天气页面。
  2. Flutter 端:提供天气数据的切换和同步功能,通过MethodChannel将数据传递到鸿蒙原生端,存储到 KvStore 后触发卡片更新。
  3. 核心联动:实现了 Flutter 应用与鸿蒙方舟卡片的数据双向同步交互跳转,充分利用了鸿蒙的分布式能力和 Flutter 的跨端优势。

三、案例 2:Flutter 应用的鸿蒙跨设备交互

鸿蒙的分布式能力允许应用在多个设备(如手机、平板、智慧屏)之间实现数据共享和操作同步。本案例将实现一个跨设备的图片传输功能:在手机端的 Flutter 应用中选择图片,点击发送后,平板端的 Flutter 应用实时接收并展示该图片。

核心技术:鸿蒙分布式文件服务 + Flutter 跨设备通信

  1. 鸿蒙分布式文件服务:用于在设备之间传输图片文件。
  2. MethodChannel + 鸿蒙分布式事件:用于触发跨设备的图片接收事件。

步骤 1:配置鸿蒙分布式文件服务权限

config.json中添加分布式文件服务和网络权限:

{
  "module": {
    "reqPermissions": [
      {
        "name": "ohos.permission.DISTRIBUTED_DATA_SYNC",
        "reason": "需要分布式数据同步权限",
        "usedScene": {
          "abilities": [".MainAbility"],
          "when": "always"
        }
      },
      {
        "name": "ohos.permission.INTERNET",
        "reason": "需要网络权限",
        "usedScene": {
          "abilities": [".MainAbility"],
          "when": "always"
        }
      },
      {
        "name": "ohos.permission.READ_USER_STORAGE",
        "reason": "需要读取存储权限",
        "usedScene": {
          "abilities": [".MainAbility"],
          "when": "always"
        }
      }
    ]
  }
}

步骤 2:鸿蒙原生端实现分布式文件传输

创建DistributedFileManager.java,封装鸿蒙分布式文件服务的文件传输逻辑:

package com.example.harmonyflutter;

import ohos.distributedschedule.interwork.DeviceInfo;
import ohos.distributedschedule.interwork.DeviceManager;
import ohos.file.fs.File;
import ohos.file.fs.FileSystem;
import ohos.file.fs.Stream;
import ohos.hiviewdfx.HiLog;
import ohos.hiviewdfx.HiLogLabel;
import java.io.ByteArrayOutputStream;
import java.util.List;

public class DistributedFileManager {
    private static final HiLogLabel LABEL = new HiLogLabel(HiLog.LOG_APP, 0, "DistributedFileManager");
    private static final String DISTRIBUTED_FILE_PATH = "/sdcard/DistributedFiles/";

    /**
     * 获取已配对的设备列表
     */
    public static List<DeviceInfo> getPairedDevices() {
        return DeviceManager.getDeviceList(DeviceInfo.FLAG_GET_ALL_DEVICE);
    }

    /**
     * 发送文件到指定设备
     */
    public static boolean sendFileToDevice(String deviceId, String localFilePath) {
        try {
            // 打开本地文件
            File localFile = FileSystem.openFile(localFilePath);
            if (!localFile.exists()) {
                HiLog.error(LABEL, "本地文件不存在:%{public}s", localFilePath);
                return false;
            }

            // 读取文件字节
            Stream inputStream = localFile.openStream(File.OpenMode.READ_ONLY);
            ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
            byte[] buffer = new byte[1024];
            int len;
            while ((len = inputStream.read(buffer)) != -1) {
                outputStream.write(buffer, 0, len);
            }
            byte[] fileBytes = outputStream.toByteArray();
            inputStream.close();
            outputStream.close();

            // 分布式文件服务:将文件写入目标设备
            String remoteFilePath = DISTRIBUTED_FILE_PATH + localFile.getName();
            File remoteFile = FileSystem.createDistributedFile(deviceId, remoteFilePath);
            Stream outputStreamRemote = remoteFile.openStream(File.OpenMode.WRITE_ONLY);
            outputStreamRemote.write(fileBytes);
            outputStreamRemote.close();

            HiLog.info(LABEL, "文件发送成功:%{public}s", remoteFilePath);
            return true;
        } catch (Exception e) {
            HiLog.error(LABEL, "文件发送失败:%{public}s", e.getMessage());
            return false;
        }
    }

    /**
     * 读取分布式文件
     */
    public static byte[] readDistributedFile(String deviceId, String remoteFilePath) {
        try {
            File remoteFile = FileSystem.openDistributedFile(deviceId, remoteFilePath);
            if (!remoteFile.exists()) {
                return null;
            }
            Stream inputStream = remoteFile.openStream(File.OpenMode.READ_ONLY);
            ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
            byte[] buffer = new byte[1024];
            int len;
            while ((len = inputStream.read(buffer)) != -1) {
                outputStream.write(buffer, 0, len);
            }
            byte[] fileBytes = outputStream.toByteArray();
            inputStream.close();
            outputStream.close();
            return fileBytes;
        } catch (Exception e) {
            HiLog.error(LABEL, "读取分布式文件失败:%{public}s", e.getMessage());
            return null;
        }
    }
}

步骤 3:Flutter 端实现跨设备图片传输与接收

在 Flutter 项目中,使用image_picker插件选择图片,通过MethodChannel调用原生的分布式文件传输方法,同时监听跨设备图片接收事件。

1. 添加依赖(pubspec.yaml)

dependencies:
  flutter:
    sdk: flutter
  image_picker: ^1.0.4 # 图片选择插件
  flutter_image_compress: ^1.1.0 # 图片压缩(可选)
2. Flutter 端核心代码

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:image_picker/image_picker.dart';
import 'dart:typed_data';
import 'dart:io';

void main() {
  runApp(const MyDistributedApp());
}

class MyDistributedApp extends StatelessWidget {
  const MyDistributedApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: '鸿蒙Flutter跨设备交互',
      theme: ThemeData(primarySwatch: Colors.blue),
      home: const DistributedImagePage(),
    );
  }
}

class DistributedImagePage extends StatefulWidget {
  const DistributedImagePage({super.key});

  @override
  State<DistributedImagePage> createState() => _DistributedImagePageState();
}

class _DistributedImagePageState extends State<DistributedImagePage> {
  static const MethodChannel _channel = MethodChannel('com.example.distributed_file');
  // 已配对设备列表
  List<String> _deviceList = [];
  String? _selectedDevice;
  // 选择的图片路径
  XFile? _selectedImage;
  // 接收的图片字节数据
  Uint8List? _receivedImageBytes;

  @override
  void initState() {
    super.initState();
    // 获取已配对设备列表
    _getPairedDevices();
    // 监听跨设备图片接收事件
    _channel.setMethodCallHandler((call) async {
      if (call.method == "onImageReceived") {
        String deviceId = call.argument("deviceId");
        String filePath = call.argument("filePath");
        // 读取图片字节数据
        Uint8List? bytes = await _channel.invokeMethod('readDistributedFile', {
          'deviceId': deviceId,
          'filePath': filePath,
        });
        setState(() {
          _receivedImageBytes = bytes;
        });
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(content: Text('收到来自设备$deviceId的图片!')),
        );
      }
      return null;
    });
  }

  // 获取已配对的鸿蒙设备列表
  Future<void> _getPairedDevices() async {
    try {
      List<dynamic> devices = await _channel.invokeMethod('getPairedDevices');
      setState(() {
        _deviceList = devices.map((e) => e.toString()).toList();
        if (_deviceList.isNotEmpty) {
          _selectedDevice = _deviceList.first;
        }
      });
    } on PlatformException catch (e) {
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text('获取设备列表失败:${e.message}')),
      );
    }
  }

  // 选择图片
  Future<void> _pickImage() async {
    final ImagePicker picker = ImagePicker();
    final XFile? image = await picker.pickImage(source: ImageSource.gallery);
    if (image != null) {
      setState(() {
        _selectedImage = image;
        _receivedImageBytes = null; // 清空接收的图片
      });
    }
  }

  // 发送图片到选中的设备
  Future<void> _sendImage() async {
    if (_selectedImage == null || _selectedDevice == null) {
      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(content: Text('请选择图片和设备!')),
      );
      return;
    }

    try {
      bool success = await _channel.invokeMethod('sendFileToDevice', {
        'deviceId': _selectedDevice,
        'filePath': _selectedImage!.path,
      });
      if (success) {
        ScaffoldMessenger.of(context).showSnackBar(
          const SnackBar(content: Text('图片发送成功!')),
        );
      } else {
        ScaffoldMessenger.of(context).showSnackBar(
          const SnackBar(content: Text('图片发送失败!')),
        );
      }
    } on PlatformException catch (e) {
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text('发送失败:${e.message}')),
      );
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('跨设备图片传输')),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          children: [
            // 设备选择下拉框
            DropdownButton<String>(
              value: _selectedDevice,
              hint: const Text('选择目标设备'),
              items: _deviceList.map((String value) {
                return DropdownMenuItem<String>(
                  value: value,
                  child: Text(value),
                );
              }).toList(),
              onChanged: (String? newValue) {
                setState(() {
                  _selectedDevice = newValue;
                });
              },
            ),
            const SizedBox(height: 20),
            // 图片选择和发送按钮
            Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                ElevatedButton(
                  onPressed: _pickImage,
                  child: const Text('选择图片'),
                ),
                const SizedBox(width: 20),
                ElevatedButton(
                  onPressed: _sendImage,
                  child: const Text('发送图片'),
                ),
              ],
            ),
            const SizedBox(height: 30),
            // 展示选择的图片或接收的图片
            Expanded(
              child: SingleChildScrollView(
                child: Column(
                  children: [
                    if (_selectedImage != null)
                      Column(
                        children: [
                          const Text('已选择的图片:'),
                          const SizedBox(height: 10),
                          Image.file(
                            File(_selectedImage!.path),
                            width: 300,
                            height: 300,
                            fit: BoxFit.cover,
                          ),
                        ],
                      ),
                    if (_receivedImageBytes != null)
                      Column(
                        children: [
                          const Text('接收的图片:'),
                          const SizedBox(height: 10),
                          Image.memory(
                            _receivedImageBytes!,
                            width: 300,
                            height: 300,
                            fit: BoxFit.cover,
                          ),
                        ],
                      ),
                  ],
                ),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

步骤 4:鸿蒙原生端实现设备列表获取与事件通知

MainAbilitySlice.java中添加设备列表获取和跨设备事件通知的逻辑:

// 新增分布式文件的MethodChannel
private static final String DISTRIBUTED_CHANNEL = "com.example.distributed_file";

// 在onStart方法中注册
new MethodChannel(flutterEngine.getDartExecutor().getBinaryMessenger(), DISTRIBUTED_CHANNEL)
        .setMethodCallHandler((call, result) -> {
            switch (call.method) {
                case "getPairedDevices":
                    // 获取已配对设备ID列表
                    List<DeviceInfo> devices = DistributedFileManager.getPairedDevices();
                    List<String> deviceIds = new ArrayList<>();
                    for (DeviceInfo device : devices) {
                        deviceIds.add(device.getDeviceId());
                    }
                    result.success(deviceIds);
                    break;
                case "sendFileToDevice":
                    // 发送文件到指定设备
                    String deviceId = call.argument("deviceId");
                    String filePath = call.argument("filePath");
                    boolean success = DistributedFileManager.sendFileToDevice(deviceId, filePath);
                    // 发送事件通知目标设备接收图片
                    sendImageReceivedEvent(deviceId, filePath);
                    result.success(success);
                    break;
                case "readDistributedFile":
                    // 读取分布式文件
                    String dId = call.argument("deviceId");
                    String fPath = call.argument("filePath");
                    byte[] bytes = DistributedFileManager.readDistributedFile(dId, fPath);
                    result.success(bytes);
                    break;
                default:
                    result.notImplemented();
                    break;
            }
        });

/**
 * 发送图片接收事件到目标设备
 */
private void sendImageReceivedEvent(String deviceId, String filePath) {
    // 此处简化处理,实际项目中可使用鸿蒙分布式事件服务实现跨设备事件通知
    // 这里直接在本地触发事件(跨设备需结合鸿蒙分布式事件服务)
    new MethodChannel(flutterEngine.getDartExecutor().getBinaryMessenger(), DISTRIBUTED_CHANNEL)
            .invokeMethod("onImageReceived", Map.of("deviceId", deviceId, "filePath", filePath));
}

案例说明

  1. 设备发现:Flutter 端通过调用原生方法获取鸿蒙已配对的设备列表,供用户选择目标设备。
  2. 图片传输:选择图片后,通过鸿蒙分布式文件服务将图片发送到目标设备,并触发事件通知。
  3. 图片接收:目标设备的 Flutter 应用监听事件,读取分布式文件并展示图片,实现跨设备的图片交互。
  4. 扩展场景:此逻辑可扩展到跨设备的视频、文档传输,甚至跨设备的操作同步(如手机端点击按钮,平板端触发对应动作)。

四、鸿蒙 Flutter 开发的进阶建议

  1. 方舟卡片优化:针对不同尺寸的方舟卡片(1x1、2x2、4x4)设计适配布局,结合鸿蒙的动态卡片能力实现更丰富的交互。
  2. 跨设备权限管理:在跨设备交互时,需处理设备之间的权限验证,确保数据传输的安全性。
  3. 性能优化:跨设备传输大文件时,采用分片传输和压缩技术,减少网络开销;Flutter 端使用CachedNetworkImage缓存接收的图片,提升渲染性能。
  4. 鸿蒙生态适配:结合鸿蒙的原子化服务能力,将 Flutter 应用封装为原子化服务,实现免安装运行,提升用户体验。

五、总结

本文通过方舟卡片集成和跨设备图片传输两个案例,展示了鸿蒙 Flutter 开发的进阶场景。方舟卡片的集成实现了应用轻量化展示,跨设备交互则充分发挥了鸿蒙的分布式能力,两者结合能让 Flutter 应用在鸿蒙生态中具备更强的竞争力。

随着鸿蒙系统的持续升级和 Flutter 对鸿蒙支持的深化,未来还将有更多的原生能力(如车载交互、智能穿戴适配)与 Flutter 融合,开发者可基于本文的思路,探索更多鸿蒙 Flutter 的创新应用场景

欢迎大家加入[开源鸿蒙跨平台开发者社区](https://openharmonycrossplatform.csdn.net),一起共建开源鸿蒙跨平台生态。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐