普通视图

发现新文章,点击刷新页面。
昨天 — 2026年2月27日首页

iOS相机权限获取

作者 傅里叶
2026年2月27日 11:25
语言:Flutter
问题:获取相机弹窗的权限不出来,iOS系统设置里面对应app也没有相机选项。是宏没有打开
解决方法:打开宏

Podfile

# post_install do |installer|
#   installer.pods_project.targets.each do |target|
#     flutter_additional_ios_build_settings(target)
#     # 为所有 Pod 目标启用 dSYM 生成
#     target.build_configurations.each do |config|
#       if config.name == 'Release'
#         config.build_settings['DEBUG_INFORMATION_FORMAT'] = 'dwarf-with-dsym'
#       end
#     end
#   end
# end

post_install do |installer|
  installer.pods_project.targets.each do |target|
    flutter_additional_ios_build_settings(target)

    target.build_configurations.each do |config|

      if config.name == 'Release'
        config.build_settings['DEBUG_INFORMATION_FORMAT'] = 'dwarf-with-dsym'
      end

      #  permission_handler 必须的宏配置
      config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= [
        '$(inherited)',

        'PERMISSION_CAMERA=1',

      ]

    end
  end
end

这段 Podfile 的 post_install 配置是正确的


一、post_install 是什么 🔧

post_install do |installer|

意思是:

CocoaPods 安装完成后执行这段脚本。

也就是在执行:

pod install

之后运行。

用途:

  • 修改 iOS 编译参数
  • 给插件加配置
  • 修复 Flutter / iOS 插件问题

Flutter 权限插件基本都用这个。


二、installer.pods_project.targets.each do |target|

installer.pods_project.targets.each do |target|

意思是:

遍历所有 Pod 插件。

例如:

  • permission_handler
  • mobile_scanner
  • ffmpeg_kit
  • camera
  • etc

这段代码会对 所有插件生效

相当于:

给所有插件统一设置编译参数。


三、flutter_additional_ios_build_settings(target)

flutter_additional_ios_build_settings(target)

意思:

Flutter 自动生成的 iOS配置。

它会自动设置:

  • Swift版本
  • 架构支持
  • Bitcode
  • iOS版本兼容性

这是 Flutter 官方必须的一行。

不能删。


四、target.build_configurations.each do |config|

target.build_configurations.each do |config|

意思:

遍历 Debug / Release 编译模式。

iOS有三种模式:

  • Debug(开发)
  • Profile(性能分析)
  • Release(发布)

这段代码:

对每个模式分别设置参数。


五、DEBUG_INFORMATION_FORMAT = dwarf-with-dsym

if config.name == 'Release'
  config.build_settings['DEBUG_INFORMATION_FORMAT'] = 'dwarf-with-dsym'
end

意思:

Release版本生成 dSYM 文件。

作用:

  • 崩溃日志解析
  • Crashlytics
  • TestFlight 崩溃分析

否则:

  • 崩溃日志看不懂

这是一个好习惯配置 👍

和权限无关。


六、最重要的一段(权限宏)⭐

config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= [  '$(inherited)',  'PERMISSION_CAMERA=1',]

这是关键。

意思:

给 iOS 编译器设置宏定义。

等价于:

#define PERMISSION_CAMERA 1

告诉 permission_handler:

编译相机权限代码。


七、$(inherited) 是什么

'$(inherited)',

意思:

保留系统原来的设置。

如果不写:

可能破坏 Flutter 默认配置。

必须保留。

相当于:

在原来的基础上增加权限宏。


八、PERMISSION_CAMERA=1 是什么 ⭐

'PERMISSION_CAMERA=1',

意思:

打开相机权限模块。

permission_handler 内部代码类似:

#if PERMISSION_CAMERA
// 相机权限代码
#endif

如果没有:

#if PERMISSION_CAMERA

条件不成立。

相机代码不会编译进去。

结果就是:

  • request() 不弹窗
  • denied
  • 设置里没有权限

之前就是这个问题。


九、整体逻辑总结(核心理解)

配置现在等价于:

安装所有插件
Flutter配置生效
Release版本生成崩溃符号
开启相机权限模块

流程:

pod install
   ↓
执行 post_install
   ↓
给所有插件加 PERMISSION_CAMERA=1
   ↓
Xcode 编译
   ↓
permission_handler 包含相机权限代码
   ↓
request() 正常弹窗

十、建议升级为完整权限配置(推荐)🔥

建议改成:

config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= [  '$(inherited)',  'PERMISSION_CAMERA=1',  'PERMISSION_PHOTOS=1',  'PERMISSION_MICROPHONE=1',]

原因:

以后大概率会用:

  • 相册
  • 麦克风
  • 视频

否则未来又会踩坑。


十一、最关键结论

现在这段:

'PERMISSION_CAMERA=1',

就是让:

Permission.camera.request();

真正生效的原因。

没有它:

iOS 会表现为:

  • 不弹窗
  • denied
  • 设置没有权限

Flutter—— 本地存储(shared_preferences)

作者 Haha_bj
2026年2月27日 11:10

一、简介

shared_preferences 是 Flutter 官方提供的键值对(Key-Value) 本地存储插件,本质是对原生平台存储的封装:

  • iOS:封装 NSUserDefaults
  • Android:封装 SharedPreferences
  • 桌面端(Windows/macOS):封装本地 JSON 文件
  • 核心特点:轻量、API 简单、持久化(APP 重启 / 卸载前数据不丢失)、仅支持基础数据类型

二、支持的数据类型

类型 对应 API 方法 说明
字符串 setString()/getString() 存储 token、用户名等
布尔值 setBool()/getBool() 存储开关状态、是否登录等
整数 setInt()/getInt() 存储计数、ID 等
浮点数 setDouble()/getDouble() 存储版本号、数值配置等
字符串列表 setStringList()/getStringList() 存储历史记录、标签等

三、基础使用步骤

1. 安装依赖

pub.dev/packages/sh…

pubspec.yaml 中添加:

dependencies:
  flutter:
    sdk: flutter
  shared_preferences: ^2.5.4 # 推荐使用最新稳定版

2. API 使用

import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';

void main() => runApp(const MyApp());

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'SharedPreferences 详解',
      home: const SPExamplePage(),
    );
  }
}

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

  @override
  State<SPExamplePage> createState() => _SPExamplePageState();
}

class _SPExamplePageState extends State<SPExamplePage> {
  // 存储的测试数据
  String _userToken = "";
  bool _isDarkMode = false;
  int _loginCount = 0;
  double _appVersion = 1.0;
  List<String> _historyList = [];

  // 初始化:页面加载时读取存储的数据
  @override
  void initState() {
    super.initState();
    _loadAllData();
  }

  // ========== 核心方法1:读取数据 ==========
  Future<void> _loadAllData() async {
    // 1. 获取 SharedPreferences 实例(必须异步)
    SharedPreferences prefs = await SharedPreferences.getInstance();

    // 2. 读取数据:第二个参数是「默认值」(key不存在时返回)
    setState(() {
      _userToken = prefs.getString("user_token") ?? ""; // 字符串默认空
      _isDarkMode = prefs.getBool("dark_mode") ?? false; // 布尔默认false
      _loginCount = prefs.getInt("login_count") ?? 0; // 整数默认0
      _appVersion = prefs.getDouble("app_version") ?? 1.0; // 浮点数默认1.0
      _historyList = prefs.getStringList("browse_history") ?? []; // 列表默认空
    });
  }

  // ========== 核心方法2:保存数据 ==========
  Future<void> _saveAllData() async {
    SharedPreferences prefs = await SharedPreferences.getInstance();

    // 1. 保存单个数据
    await prefs.setString("user_token", "abc123456789");
    await prefs.setBool("dark_mode", true);
    await prefs.setInt("login_count", _loginCount + 1); // 计数+1
    await prefs.setDouble("app_version", 2.1);
    await prefs.setStringList("browse_history", ["首页", "我的", "设置"]);

    // 2. 保存后刷新页面数据
    _loadAllData();

    // 提示用户
    if (mounted) { // 防止页面销毁后调用context
      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(content: Text("数据保存成功 ✅")),
      );
    }
  }

  // ========== 核心方法3:删除数据 ==========
  Future<void> _deleteData() async {
    SharedPreferences prefs = await SharedPreferences.getInstance();

    // 方式1:删除单个key
    await prefs.remove("user_token");

    // 方式2:清空所有数据(谨慎使用!)
    // await prefs.clear();

    // 刷新数据
    _loadAllData();
    if (mounted) {
      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(content: Text("数据删除成功 ❌")),
      );
    }
  }

  // ========== 核心方法4:检查key是否存在 ==========
  Future<void> _checkKeyExists() async {
    SharedPreferences prefs = await SharedPreferences.getInstance();
    bool hasToken = prefs.containsKey("user_token");
    if (mounted) {
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text("user_token 是否存在:$hasToken")),
      );
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text("SharedPreferences 详解")),
      body: Padding(
        padding: const EdgeInsets.all(20),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            // 显示存储的数据
            Text("用户Token:$_userToken"),
            Text("深色模式:$_isDarkMode"),
            Text("登录次数:$_loginCount"),
            Text("APP版本:$_appVersion"),
            Text("浏览历史:${_historyList.join(", ")}"),
            const SizedBox(height: 30),

            // 操作按钮
            Row(
              children: [
                ElevatedButton(
                  onPressed: _saveAllData,
                  child: const Text("保存数据"),
                ),
                const SizedBox(width: 10),
                ElevatedButton(
                  onPressed: _deleteData,
                  style: ElevatedButton.styleFrom(backgroundColor: Colors.red),
                  child: const Text("删除Token"),
                ),
              ],
            ),
            const SizedBox(height: 10),
            ElevatedButton(
              onPressed: _checkKeyExists,
              child: const Text("检查Token是否存在"),
            ),
          ],
        ),
      ),
    );
  }
}

四、进阶技巧

1. 封装工具类(避免重复代码)

实际项目中建议封装成单例工具类,统一管理存储的 key 和方法:

import 'package:shared_preferences/shared_preferences.dart';

class SPUtil {
  // 单例模式
  static SPUtil? _instance;
  static SharedPreferences? _prefs;

  // 私有化构造函数
  SPUtil._();

  // 获取单例
  static Future<SPUtil> getInstance() async {
    if (_instance == null) {
      _instance = SPUtil._();
    }
    if (_prefs == null) {
      _prefs = await SharedPreferences.getInstance();
    }
    return _instance!;
  }

  // ========== 定义存储的key(统一管理,避免拼写错误) ==========
  static const String KEY_USER_TOKEN = "user_token";
  static const String KEY_DARK_MODE = "dark_mode";
  static const String KEY_LOGIN_COUNT = "login_count";

  // ========== 封装常用方法 ==========
  // 保存字符串
  Future<void> setString(String key, String value) async {
    await _prefs?.setString(key, value);
  }

  // 读取字符串
  String getString(String key, {String defaultValue = ""}) {
    return _prefs?.getString(key) ?? defaultValue;
  }

  // 保存布尔值
  Future<void> setBool(String key, bool value) async {
    await _prefs?.setBool(key, value);
  }

  // 读取布尔值
  bool getBool(String key, {bool defaultValue = false}) {
    return _prefs?.getBool(key) ?? defaultValue;
  }

  // 删除单个key
  Future<void> remove(String key) async {
    await _prefs?.remove(key);
  }

  // 清空所有数据
  Future<void> clear() async {
    await _prefs?.clear();
  }
}

// 使用示例
void useSPUtil() async {
  SPUtil spUtil = await SPUtil.getInstance();
  // 保存
  await spUtil.setString(SPUtil.KEY_USER_TOKEN, "123456");
  // 读取
  String token = spUtil.getString(SPUtil.KEY_USER_TOKEN);
  print("Token:$token");
}

2. 存储复杂对象(序列化 / 反序列化)

shared_preferences 不支持直接存储对象,需先转 JSON 字符串:

import 'dart:convert';

// 定义用户模型
class User {
  String name;
  int age;
  String email;

  User({required this.name, required this.age, required this.email});

  // 转JSON字符串
  String toJson() {
    Map<String, dynamic> map = {
      "name": name,
      "age": age,
      "email": email,
    };
    return json.encode(map);
  }

  // 从JSON字符串转对象
  static User fromJson(String jsonStr) {
    Map<String, dynamic> map = json.decode(jsonStr);
    return User(
      name: map["name"],
      age: map["age"],
      email: map["email"],
    );
  }
}

// 存储/读取对象
Future<void> saveUser() async {
  SharedPreferences prefs = await SharedPreferences.getInstance();
  // 1. 创建对象
  User user = User(name: "张三", age: 25, email: "zhangsan@example.com");
  // 2. 转JSON字符串保存
  await prefs.setString("user_info", user.toJson());
  // 3. 读取并转对象
  String userJson = prefs.getString("user_info") ?? "";
  if (userJson.isNotEmpty) {
    User savedUser = User.fromJson(userJson);
    print("用户名:${savedUser.name},年龄:${savedUser.age}");
  }
}

五、避坑指南(常见问题)

1. 同步 / 异步问题(最容易踩坑)

  • ❌ 错误:在 initState 中同步调用 getStringgetInstance 是异步的)

    @override
    void initState() {
      super.initState();
      // 错误!SharedPreferences.getInstance() 是异步,不能直接同步调用
      String token = SharedPreferences.getInstance().then((prefs) => prefs.getString("token"));
    }
    
  • ✅ 正确:用 async/awaitthen 处理异步

    @override
    void initState() {
      super.initState();
      _loadData(); // 异步方法
    }
    
    Future<void> _loadData() async {
      SharedPreferences prefs = await SharedPreferences.getInstance();
      String token = prefs.getString("token") ?? "";
    }
    

2. 页面销毁后调用 setState

  • 问题:异步操作完成后页面已销毁,调用 setState 会报错

  • 解决:用 mounted 判断页面是否挂载

    Future<void> _loadData() async {
      SharedPreferences prefs = await SharedPreferences.getInstance();
      if (mounted) { // 关键:判断页面是否还在
        setState(() {
          _token = prefs.getString("token") ?? "";
        });
      }
    }
    

3. 数据未及时刷新

  • 问题:保存数据后,UI 没有实时更新

  • 解决:保存后重新调用读取方法,触发 setState

    await prefs.setString("token", "new_token");
    _loadData(); // 重新读取,刷新UI
    
昨天以前首页

flutter存储知识点总结

作者 zhangkai
2026年2月26日 16:36

一、数据存储

1、本地持久化存储SharedPreferences

SharedPreferences为轻量级存储,存储少量简单数据,键值对形式,不适合大量、复杂数据的存储。

import 'package:shared_preferences/shared_preferences.dart'; 
// 存储数据 
Future<void> saveData() async { 
// 获取 SharedPreferences 实例 
final prefs = await SharedPreferences.getInstance(); 
// 存储不同类型的数据 await prefs.setString('user_token', 'abc123456'); // 字符串
await prefs.setInt('user_age', 25); // 整数 a
wait prefs.setBool('is_login', true); // 布尔值 
await prefs.setDouble('height', 1.75); // 浮点数 
await prefs.setStringList('hobbies', ['读书', '运动']); // 字符串列表 } 

// 读取数据 Future<void> readData() async { 
final prefs = await SharedPreferences.getInstance(); 
// 读取数据(第二个参数是默认值,避免 null) 
String? token = prefs.getString('user_token') ?? ''; 
int age = prefs.getInt('user_age') ?? 0;
bool isLogin = prefs.getBool('is_login') ?? false; 
print('token: $token, age: $age, isLogin: $isLogin'); 
} 
// 删除数据 
Future<void> removeData() async { 
final prefs = await SharedPreferences.getInstance(); await prefs.remove('user_token'); 
// 删除单个键 
// await prefs.clear(); 
// 清空所有数据 }

2、Provider存储

Provider 是运行时的内存状态管理工具非本地持久化存储,Provider 存储的数据只在 App 运行时有效,重启后丢失。它的核心价值是让数据在多个 Widget 之间共享、响应式更新,是 “内存级” 的数据存储与共享方案。适用于需要跨组件共享、实时响应更新的运行时数据。 Provider的原理是基于 Flutter 原生的 InheritedWidget 实现的,而 InheritedWidget 的核心特性就是通过 Context 向上查找共享数据

  • 从 Provider 中读取 / 监听数据:必须依赖 context(因为要确定查找的上下文范围);

  • 从 Provider 中修改数据:通常也需要 context,但有替代方案(无需 Context);

  • 初始化 / 注入 Provider:不需要 context(在根节点创建时)。

2.1 定义数据模型
import 'package:flutter/foundation.dart';

class ContractDataModel extends ChangeNotifier {
  // ========== 核心数据字段 (仅保留3个示例) ==========

  // 1. 房源类型名称 (来自第一步)
  String _goodsTypeName = '';

  // 2. 租客姓名 (来自第二步)
  String _customerName = '';

  // 3. 租金单价 (来自第三步)
  String _unitPrice = '';

  // ========== Getters ==========
  String get goodsTypeName => _goodsTypeName;
  String get customerName => _customerName;
  String get unitPrice => _unitPrice;

  // ========== 统一更新方法 ==========
  /// 更新核心数据
  /// 只要传入的值不为 null 就更新,允许空字符串覆盖原有数据
  void updateCoreInfo({
    String? goodsTypeName,
    String? customerName,
    String? unitPrice,
  }) {
    if (goodsTypeName != null) _goodsTypeName = goodsTypeName;
    if (customerName != null) _customerName = customerName;
    if (unitPrice != null) _unitPrice = unitPrice;

    // 通知监听者重建 UI
    notifyListeners();
  }

  // ========== 验证数据是否完整 (示例) ==========
  bool validateCoreInfo() {
    if (_goodsTypeName.isEmpty) return false;
    if (_customerName.isEmpty) return false;
    if (_unitPrice.isEmpty) return false;
    return true;
  }

  // ========== 获取所有数据的 Map ==========
  Map<String, dynamic> toMap() {
    return {
      'goodsTypeName': _goodsTypeName,
      'customerName': _customerName,
      'unitPrice': _unitPrice,
    };
  }

  // ========== 清空所有数据 ==========
  void clear() {
    _goodsTypeName = '';
    _customerName = '';
    _unitPrice = '';

    notifyListeners();
  }
}
2.2 更新provider中的数据
final contractModel = Provider.of<ContractDataModel>(context, listen: false);
contractModel.updatePersonInfo(
  customerCertificateId: _customerCertificateId,
  customerName: _customerName,
);
2.3 获取provider,提取内部的数据
late _contractDataModel = Provider.of<ContractDataModel>(context, listen: false);
if(_contractDataModel.signClientType == '1'){//企业
  _customerCertificateType = 'Z';
}else{
  _customerCertificateType = '';
}

tips: 使用late的作用是什么? late 是用来修饰延迟初始化变量的关键字。

  • late用来声明变量时无法立即赋值的问题,Provider依赖context,而 context 通常在 Widget 的 build 方法、initState的位置才能获取,声明无法获取从而报错。
  • 允许变量非空但延迟复制。(Dart空安全核心)Dart 开启空安全后,未用 ? 标记的变量必须声明时赋值或用 late 修饰。

image.png

二、Provider 流程页面剖析

image.png

应该这样理解:

✅ Provider 是在 CreateReserveStepPage 中创建的(第52行)

✅ 三个 Step Widget 各自有自己独立的 context

✅ 但它们都是 ChangeNotifierProvider 的子孙节点

✅ 所以它们都能通过各自的 context 向上查找,找到同一个 Provider 实例

或者说:

✅ 三个 Widget 的 context 都能访问到在 CreateReserveStepPage 中创建的 Provider,因为它们都在 Provider 的子树中。

如下:

class CreateReserveStepPage extends StatefulWidget {
final Map params;
const CreateReserveStepPage({Key key,this.params}) : super(key: key);
@override
_CreateReserveStepPage createState() => _CreateReserveStepPage();
}

class _CreateReserveStepPage extends State<CreateReserveStepPage> {
final BrnMetaHorizontalStepsManager _stepsManager = BrnMetaHorizontalStepsManager();
int _currentIndex = 0;
bool _isCompleted = false;
Timer _timer;
int _elapsed = 0;
Map<String, dynamic> _contractParams = {};
final List<String> _stepTitles = ['合同信息', '租客/入住人信息', '账单/补充信息'];
// 验证回调函数
Function _validateContractWidget;
Function _validatePersonWidget;
Function _validateBillWidget;
// 保存数据回调函数
Function _savePersonWidget;
Function _saveBillWidget;
// 添加ScrollController
final ScrollController _scrollController = ScrollController();
void initState() {
  super.initState();
}

@override
void dispose() {
  _scrollController.dispose();
  super.dispose();
}

@override
Widget build(BuildContext context) {
  return ChangeNotifierProvider(
    create: (_) => ContractDataModel(),
    builder: (providerContext, child) {
      return Scaffold(
        appBar: BrnAppBar(
          title: '创建签约',
          leading: IconButton(
            icon: Icon(Icons.arrow_back_ios_new, color: Colors.black),
            onPressed: () => BoostUtil.finish(),
          ),
        ),
        body: Column(
          children: [
            _stepsManager.buildSteps(
              steps: _stepTitles.map((title) => BrunoStep(stepContentText: title)).toList(),
              currentIndex: _currentIndex,
              isCompleted: _isCompleted,
            ),
            const SizedBox(height: 24),
            Expanded(
              child: SingleChildScrollView(
                controller: _scrollController,
                child: _buildStepContent(_currentIndex),
              ),
            ),
            Container(
              padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16),
              decoration: BoxDecoration(
                color: Colors.white,
                border: Border(top: BorderSide(color: Color(0xFFF0F0F0), width: 1)),
              ),
              child: _buildBottomButtons(providerContext),
            ),
          ],
        ),
      );
    },
  );
}

Widget _buildStepContent(int index) {
  switch (index) {
    case 0:
      return ReservationStepContractWidget(
        params: this.widget.params,
        onDataChanged: _handleContractDataChanged,
        onValidateCallback: (validateFunc) {
          _validateContractWidget = validateFunc;
        },
      );
    case 1:
      return ReservationStepPersonWidget(
        onDataChanged: _handlePersonDataChanged,
        onValidateCallback: (validateFunc) {
          _validatePersonWidget = validateFunc;
        },
        onSaveCallback: (saveFunc) {
          _savePersonWidget = saveFunc;
        },
      );
    case 2:
      return ReservationStepBillWidget(
        onDataChanged: _handleBillDataChanged,
        onValidateCallback: (validateFunc) {
          _validateBillWidget = validateFunc;
        },
        onSaveCallback: (saveFunc) {
          _saveBillWidget = saveFunc;
        },
      );
    default:
      return SizedBox();
  }
}

Flutter——List.map()

作者 Haha_bj
2026年2月26日 10:53

一、map

map 是 Dart 中 List 集合的核心转换方法,作用是遍历列表中的每一个元素,对每个元素执行指定的转换逻辑,最终返回一个新的可迭代对象(Iterable

  • 核心特点:不会修改原列表,而是返回新的迭代对象(需要手动转成 List);

  • 语法:Iterable<T> map<T>(T Function(E element) convert)

    • convert:转换函数,接收原列表的单个元素,返回转换后的元素;
    • T:转换后元素的类型(可省略,Dart 会自动推导);
    • E:原列表元素的类型。

二、基础用法(必掌握)

1. 基本类型转换

最常见的场景:将列表中的元素做简单转换(如数字转字符串、数值运算等)。

void main() {
  // 原列表:整数列表
  List<int> numbers = [1, 2, 3, 4, 5];
  
  // 1. 转换:每个数字乘以2 → 返回 Iterable<int>
  Iterable<int> doubledIterable = numbers.map((int num) {
    return num * 2;
  });
  
  // 2. 转成 List(关键:map返回的是Iterable,需用toList()转成List)
  List<int> doubledList = doubledIterable.toList();
  
  print("原列表:$numbers"); // 原列表:[1, 2, 3, 4, 5](原列表不变)
  print("转换后:$doubledList"); // 转换后:[2, 4, 6, 8, 10]
  
  // 简化写法(箭头函数):单行逻辑推荐用箭头函数
  List<String> numToString = numbers.map((num) => num.toString()).toList();
  print("数字转字符串:$numToString"); // [1, 2, 3, 4, 5]
}

2. 自定义对象转换

实战中更常用的场景:将自定义对象列表转换为其他格式(如提取对象的某个属性、转成 DTO 等)。

// 定义自定义对象
class User {
  final String name;
  final int age;
  
  User({required this.name, required this.age});
}

void main() {
  List<User> users = [
    User(name: "张三", age: 20),
    User(name: "李四", age: 25),
    User(name: "王五", age: 30),
  ];
  
  // 场景1:提取所有用户的姓名 → 字符串列表
  List<String> userNames = users.map((user) => user.name).toList();
  print("用户姓名:$userNames"); // [张三, 李四, 王五]
  
  // 场景2:转换为新的Map列表(如接口请求参数)
  List<Map<String, dynamic>> userMaps = users.map((user) {
    return {
      "username": user.name,
      "user_age": user.age,
      "is_adult": user.age >= 18, // 新增衍生字段
    };
  }).toList();
  print("转Map列表:$userMaps");
  // 输出:[{username: 张三, user_age: 20, is_adult: true}, ...]
}

三、关键注意事项(避坑)

1. 必须用 toList() 转成列表

map 方法返回的是 Iterable(可迭代对象),不是 List,如果直接使用会导致部分 List 方法(如 addremove)无法调用:

void main() {
  List<int> nums = [1,2,3];
  // 错误用法:Iterable 没有 add 方法
  // nums.map((e) => e*2).add(4); 
  
  // 正确用法:先转List
  List<int> newNums = nums.map((e) => e*2).toList();
  newNums.add(4); // [2,4,6,4]
}

2. 惰性执行特性

map 方法的转换逻辑不会立即执行,而是在遍历 Iterable(如调用 toList()/forEach())时才执行:

void main() {
  List<int> nums = [1,2,3];
  // 定义map转换,但未执行
  Iterable<int> iter = nums.map((e) {
    print("执行转换:$e");
    return e*2;
  });
  
  print("还未执行转换");
  // 调用toList()时,才会遍历并执行转换逻辑
  List<int> list = iter.toList();
  
  // 输出顺序:
  // 还未执行转换
  // 执行转换:1
  // 执行转换:2
  // 执行转换:3
}

3. 原列表修改不影响已生成的 Iterable

map 是基于原列表当时的状态生成迭代对象,后续修改原列表不会改变已生成的 Iterable

void main() {
  List<int> nums = [1,2,3];
  Iterable<int> iter = nums.map((e) => e*2);
  
  // 修改原列表
  nums.add(4);
  
  // 转换后的列表包含原列表的3个元素(1,2,3),不包含新增的4
  List<int> list = iter.toList();
  print(list); // [2,4,6]
}

四、高级用法

1. 链式调用

map 可与其他列表方法(wheresorttake 等)链式调用,实现复杂转换:

void main() {
  List<int> nums = [1,2,3,4,5,6,7,8];
  
  // 需求:筛选偶数 → 乘以10 → 转字符串 → 取前3个
  List<String> result = nums
      .where((e) => e % 2 == 0) // 筛选偶数:[2,4,6,8]
      .map((e) => e * 10) // 乘以10:[20,40,60,80]
      .map((e) => "数值:$e") // 转字符串:["数值:20", ...]
      .take(3) // 取前3个:["数值:20", "数值:40", "数值:60"]
      .toList();
  
  print(result); // [数值:20, 数值:40, 数值:60]
}

2. 处理空值(null safety)

Dart 空安全下,处理可能包含 null 的列表:

void main() {
  List<int?> nums = [1, null, 3, null, 5];
  
  // 方式1:过滤null后转换
  List<int> result1 = nums
      .where((e) => e != null) // 过滤null
      .map((e) => e!) // 非空断言(已过滤,安全)
      .toList();
  print(result1); // [1,3,5]
  
  // 方式2:给null设置默认值
  List<int> result2 = nums.map((e) => e ?? 0).toList();
  print(result2); // [1,0,3,0,5]
}

五、map vs forEach(易混淆对比)

很多新手会混淆 mapforEach,核心区别如下:

特性 map forEach
核心作用 转换元素,返回新的 Iterable 遍历元素执行操作,无返回值
返回值 Iterable<T> void(无返回值)
是否修改原列表 否(但可在回调中手动修改元素)
典型场景 元素类型转换、提取属性 遍历执行副作用(如打印、存储)
void main() {
  List<int> nums = [1,2,3];
  
  // map:转换并返回新列表
  List<int> mapResult = nums.map((e) => e*2).toList();
  
  // forEach:遍历执行操作,无返回值
  nums.forEach((e) {
    print("遍历元素:$e"); // 打印每个元素
  });
}

总结

  1. 核心作用List.map() 是列表元素转换的核心方法,返回 Iterable,需用 toList() 转成列表;
  2. 关键特性:惰性执行、不修改原列表、支持空安全和链式调用;
  3. 避坑点:必须转 List 才能使用 List 方法,空列表调用 map 不会报错(返回空 Iterable);
  4. 使用场景:类型转换、提取对象属性、生成新格式数据(如接口参数)。

掌握 map 方法后,能大幅简化列表转换的代码,是 Dart 开发中最常用的列表操作之一。

iOS + AI ,国外一个叫 Rork Max 的项目打算替换掉 Xcode

2026年2月21日 17:34

最近看到一个很有意思的项目,它是一个由国外 Rork 团队推出的 AI 移动应用开发平台,宣称是“全球首个在浏览器中构建原生 Swift 应用的 AI 工具”,也就是,你可以不需要 Mac 和 Xcode ,同时一次性完成 iPhone、手表、iPad、电视和 Vision Pro 的应用,甚至还有 AR 和 3D 支持。

所以它的产品逻辑是:用户只需在浏览器中输入自然语言描述,AI 就会自动生成 SwiftUI 代码,然后编译并在云端模拟器中运行,最后支持一键发布到 App Store。

什么 swift 版本 uniapp ?

听起来有点玄乎,但是实际上其实就是 Rork 在后端部署了大量的物理 Mac 节点或 Mac 云实例,当你开始一个项目时,系统就会动态分配一台运行着 XcodeiOS SDK 的 Mac 给对应会话。

也就是所有的编译、链接、资产打包过程都在真实的 macOS 环境下完成,生成的是 100% 的原生 Swift/SwiftUI 代码。

所以实际上就是:Cloud 版本的 Xcode/Mac ,然后搭配 Claude Code 和 Opus 4.6 ,然后生成对应的 iOS App 并提交 Apple Store 审核。

而 Rork 在这里也是采用了类似于云游戏的实时视频流协议(低延迟传输),所以你在浏览器里的每一次点击都会传回云端 Mac 的模拟器,画面变化再实时推送到前端

实际上就是一个远程主机,本质和 AI Studio 类似。

当然,Rork Max 的核心肯定还是他们的 Agent 管理和产品流程,这里的 AI Agent 除了利用 Opus 4.6 写代码之外,还要管理它的所有报错,测试运行和工程管理,同时 Rork 内置了 App Store Connect 的自动化流程,用户登录 Apple ID 后,AI 可以代理证书配置、App 打包和提审等流程。

从这里看,Rork Max 的客户更多的可能是非开发者,所以它的目标是将复杂的工程基座(Mac 硬件 + Xcode SDK + 苹果证书体系)完全抽象化,让开发者只需要关注逻辑和创意

另外,这里 Rork 自己强调了“非模版化”。它不是通过预设模版拼凑应用,而是通过大模型实时推理,通过自己实现的“持续上下文注入”的技术,让 AI 记住你之前所有对 UI 的微调,确保跨平台迁移时风格的一致性。

实际上它更多是一个从零构建、测试、安装并上架的 Apple 体系生产平台。它直接把“idea → 上架 App Store 的原生 Swift 应用”压缩成一个网页操作,从而大幅度降低了门槛。

官方演示视频中,从零到可玩的游戏原型大概 30–60 分钟:

另外 Rork 也表示后续会支持直接导入老项目的功能,不过对于这种场景,基本都是已经有开发者维护的项目场景,我比较怀疑是否会有受众,虽然貌似真的有:

目前已经有一些 Rork max 用户开始体验,反馈褒贬不一,但是我是没真实体验的,因为 Rork Max 的价格还是挺感人:

为什么不体验其他的?因为我看到所有说不好用的回复里,官方都是问:你是否打开了 Rork Max

当然,觉得它有意思的原因,也是它这个产品形态或者是未来的代表之一,开发者不再需要装什么 IDE 或者 SDK ,甚至都不需要纠结是 win 还是 mac 甚至 linux ,只需要一个入口,就可以完成需要开发,当然,那时候如果真的到来的话,也许开发者也不是开发者了,可能更多只是 token 账单的消费者。

别再手写 MethodChannel 了:Flutter Pigeon 工程级实践与架构设计

作者 RaidenLiu
2026年2月21日 12:53

123.png

一、为什么 MethodChannel 在中大型项目里会失控?

每一个从 Native 转 Flutter 的开发者,大概都经历过这样的“至暗时刻”:

1.1 字符串 API 的不可维护性

你小心翼翼地在 Dart 端写下 invokeMethod("getUserInfo"),但 Android 同学在实现时写成了 getUserInfo (多了一个空格),或者 iOS 同学随手改成了 fetchUserInfo

  • 结果:编译期一片祥和,运行期直接 MissingPluginException 崩溃。
  • 本质:MethodChannel 是基于“字符串契约”的弱类型通信,它把风险全部推迟到了运行时。

1.2 多人协作时的“数据猜谜”

// Native 返回的数据
{
  "userId": 1001, // Android 传的是 Long
  "userId": "1001", // iOS 传的是 String
  "isActive": 0 // 到底是 bool 还是 int?
}

Flutter 端的解析代码充斥着大量的 dynamic 转换和防御性编程。一旦原生同学修改了某个字段名,Flutter 端没有任何感知,直到线上用户反馈 Bug。

1.3 Add-to-App 场景下的复杂度翻倍

当你进入混合开发(Add-to-App)深水区,面对多 FlutterEngine生命周期分离以及原生/Flutter 页面频繁跳转时,MethodChannel 这种“广播式”或“散乱式”的注册方式,会让代码逻辑像线团一样纠缠不清。

在 Demo 期,MethodChannel 是灵活的;在工程期,它是不可靠的。我们需要一种强契约方案。

二、Pigeon 是什么?它解决的不是“简化代码”,而是“契约问题”

Pigeon 是 Flutter 官方推出的代码生成工具,它的核心理念是 IDL(接口定义语言)

2.1 核心理念:契约驱动开

你不再需要手写 Dart 的 invokeMethod 和原生的 onMethodCall。你只需要写一个 Dart 抽象类(契约),Pigeon 就会为你生成:

  1. Dart 端 的调用代码。
  2. Android (Kotlin/Java) 的接口代码。
  3. iOS (Swift/ObjC) 的协议代码。
  4. C++ (Windows) 的头文件。

2.2 本质差异对比

维度 MethodChannel (手写) Pigeon (自动生成)
类型安全 ❌ 弱类型 (Map<String, dynamic>) 强类型 (Class/Enum)
编译期校验 ❌ 无,拼错字照样跑 ,参数不对直接报错
通信效率 ⚠️ 手动序列化可能有误 ✅ 使用 StandardMessageCodec 二进制传输
线程模型 ⚠️ 默认主线程 ✅ 支持 @TaskQueue 后台执行

注意:Pigeon 生成的通信代码属于内部实现细节,各平台必须使用同版本源码生成代码,否则可能出现运行时错误或数据序列化异常。

2.3 不仅仅是 RPC:拥抱类型安全的 Event Channel

很多人对 Pigeon 的印象还停留在“单次请求-响应(MethodChannel 替代品)”的阶段。但在较新的版本中,Pigeon 已经正式将版图扩张到了 Event Channel (流式通信)

在过去,当原生端需要向 Flutter 高频、持续地推送事件(例如:蓝牙状态监听、大文件下载进度、传感器数据)时,我们只能乖乖回去手写 EventChannel,并在 Dart 端痛苦地处理 Stream<dynamic>,强类型防线在此彻底崩溃。

现在,通过 Pigeon 的 @EventChannelApi() 注解或配合强类型回调,你可以直接生成带有类型签名的 Stream 接口。这意味着:原生端主动推送事件,也终于被纳入了编译期校验的保护伞下。

三、入门示例:3分钟完成一次重构

3.1 定义接口文件 (pigeons/device_api.dart)

import 'package:pigeon/pigeon.dart';

// 定义数据模型(DTO)
class DeviceInfo {
  String? systemVersion;
  String manufacturer;
  bool isTablet;
}

// 定义 Flutter 调用原生的接口
@HostApi()
abstract class DeviceHostApi {
  DeviceInfo getDeviceInfo();
  void vibrate(int durationMs);
}

// 定义 原生调用 Flutter 的接口
@FlutterApi()
abstract class DeviceFlutterApi {
  void onBatteryLow(int level);
}

3.2 生成代码

在终端运行(建议封装进 Makefile 或脚本):

dart run pigeon \
  --input pigeons/device_api.dart \
  --dart_out lib/api/device_api.g.dart \
  --kotlin_out android/app/src/main/kotlin/com/example/app/DeviceApi.g.kt \
  --kotlin_package "com.example.app" \
  --swift_out ios/Runner/DeviceApi.g.swift

3.3 接入(以 Kotlin 为例)

原生端不再需要处理 MethodCall 的 switch-case,而是直接实现接口:

// Android
class DeviceApiImpl : DeviceHostApi {
    override fun getDeviceInfo(): DeviceInfo {
        return DeviceInfo(manufacturer = "Samsung", isTablet = false)
    }
    override fun vibrate(durationMs: Long) {
        // 实现震动逻辑
    }
}

// 注册
DeviceHostApi.setUp(flutterEngine.dartExecutor.binaryMessenger, DeviceApiImpl())

四、工程级接口设计规范(核心价值)

如果你把 Pigeon 当作 MethodChannel 的语法糖,那你就低估了它。使用Pigeon 会迫使你进行架构思考。

4.1 Feature 分层设计:拒绝上帝类

错误做法:创建一个 AppApi,里面塞满了登录、支付、埋点、蓝牙等几十个方法。

推荐做法:按业务领域拆分文件和接口。

pigeons/
  ├── auth_api.dart    // 登录、Token管理
  ├── payment_api.dart // 支付、内购
  ├── trace_api.dart   // 埋点、日志
  └── system_api.dart  // 设备信息、权限

Pigeon 支持多输入文件,生成的代码也会自然解耦。这使得不同业务线的开发同事(如支付组 vs 基础组)可以并行开发,互不冲突。

4.2 DTO 设计原则:协议即文档

  • 严禁使用 Map:在 Pigeon 定义中,不要出现 Map<String, Object>。必须定义具体的 class
  • 善用 Enum:Pigeon 完美支持枚举。将状态码定义为 Enum,Android/iOS 端会自动生成对应的枚举类,彻底告别魔术数字(Magic Number)。(Pigeon 针对复杂泛型、递归数据结构支持有限,若 API 返回过于复杂结构,可以考虑在 DTO 层先做扁平化封装。)
  • 空安全(Null Safety)String?String 在生成的 Native 代码中会被严格区分(如 Kotlin 的 String? vs String,Swift 的 String? vs String)。这强制原生开发者处理空指针问题。

4.3 接口版本演进策略

中大型项目必然面临原生版本滞后于 Flutter 版本的情况(热更新场景)。

  • 原则只增不减

  • 策略

    1. 新增字段必须是 nullable 的。
    2. 废弃字段不要直接删除,而是标记注释,并在 Native 端做兼容处理。
    3. 如果改动极大,建议新建 ApiV2 接口,而不是修改 ApiV1

五、Pigeon 在 Add-to-App 架构中的最佳实践

5.1 多 FlutterEngine 场景

在混合开发中,你可能同时启动了两个 FlutterEngine(一个用于主页,一个用于详情页)。如果直接使用静态注册,会导致消息发错引擎。

关键解法:Scope to BinaryMessenger

Pigeon 生成的 setUp 方法第一个参数就是 BinaryMessenger

// Android: 为每个引擎单独注册实例
class MyActivity : FlutterActivity() {
    override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
        super.configureFlutterEngine(flutterEngine)
        
        // 绑定当前引擎的 Messenger
        val apiImpl = MyFeatureApiImpl(context) 
        MyFeatureApi.setUp(flutterEngine.dartExecutor.binaryMessenger, apiImpl)
    }
}

通过这种方式,API 的实现实例与 Engine 的生命周期严格绑定,互不干扰。

5.2 避免内存泄漏

ActivityViewController 销毁时,切记要解绑:

override fun cleanUpFlutterEngine(flutterEngine: FlutterEngine) {
    // 传入 null 即可解绑,防止持有 Context 导致泄漏
    MyFeatureApi.setUp(flutterEngine.dartExecutor.binaryMessenger, null)
}

5.3 模块化项目结构建议

建议将 Pigeon 定义和生成代码单独抽取为一个 Package(例如 my_app_bridge)。

  • 好处:Native 工程和 Flutter 工程可以依赖同一个 Git Submodule 或私有 Pub 库,确保双方拿到的协议文件永远是一致的。

六、异常处理与错误模型设计

不要只返回 false,要抛出异常。

6.1 Pigeon 的 Error 机制

Pigeon 允许在 Native 端抛出特定的 Error,Flutter 端捕获为 PlatformException

Kotlin 端:

throw FlutterError("AUTH_ERROR", "Token expired", "Details...")

Dart 端:

try {
  await api.login();
} catch (e) {
  if (e is PlatformException && e.code == 'AUTH_ERROR') {
    // 处理 Token 过期
  }
}

6.2 统一错误模型

为了统一三端认知,建议在 Pigeon 里定义通用的 ErrorResult 包装类:

class ApiResult<T> {
  bool success;
  T? data;
  String? errorCode;
  String? errorMessage;
}

虽然这看起来稍微繁琐,但在大型 App 中,这能让原生和 Dart 拥有一套完全一致的错误码字典。


七、性能对比与关键优化

7.1 性能真相

很多开发者问:Pigeon 比 MethodChannel 快吗?

  • 传输层面两者一样快。底层都使用 StandardMessageCodec 进行二进制序列化。
  • 执行层面:Pigeon 省去了手动解析 Map 和类型转换的开销,这部分微小的 CPU 收益在数据量巨大时才明显。

7.2 杀手级特性:@TaskQueue (解决 UI 卡顿)

默认情况下,MethodChannel 的原生方法在 主线程 (Main Thread) 执行。如果你的 Native 方法涉及繁重的 I/O 或计算,会卡住 Flutter 的 UI 渲染。

Pigeon 支持 @TaskQueue 注解(Flutter 3.3+):

@HostApi()
abstract class HeavyWorkApi {
  @TaskQueue(type: TaskQueueType.serialBackgroundThread)
  String calculateHash(String heavyData);
}

加了这一行,原生代码会自动在后台线程执行,计算完后再回调主线程。这在图像处理、文件加密场景下是质的飞跃

要注意的是:该注解受底层平台实现影响,在一些旧版本平台接口或不支持背景线程执行(默认还是 MainThread),因此建议提前验证目标设备支持情况。

八、CI 与自动化生成策略

为了防止“接口漂移”(即 Dart改了,Native 没重新生成):

  1. Do check in:建议将生成的 .g.dart.kt.swift 文件提交到 Git 仓库。

    • 理由:原生开发人员可能没装 Flutter 环境,他们需要直接能跑的代码。
  2. CI 校验:在 CI 流水线中增加一步检查:

    # 重新生成一遍
    dart run pigeon ...
    # 检查是否有文件变动
    git diff --exit-code
    

    如果有变动,说明开发者提交了 Pigeon 定义但没运行生成命令,CI 直接报错。

  3. 团队协作的死穴:严格锁定生成器版本: 你的 CI 跑得很完美,直到有一天发生了这样的灾难:A 同学在本地用 Pigeon v20 生成了代码,B 同学拉取分支后,因为本地环境是 v21 并重新运行了生成命令,导致满屏的 Git 冲突和不可预期的 API 漂移。

    **防坑策略**:绝不能仅仅把 `pigeon` 写进 `pubspec.yaml``dev_dependencies` 就万事大吉。你       必须在团队的构建脚本(如 `Makefile`)或 CI 配置中,**强制锁定 Pigeon 的执行版本**

九、什么时候不该用 Pigeon?

Pigeon 虽好,但不是银弹。以下场景建议保留 MethodChannel:

  1. 非结构化的动态数据:例如透传一段任意结构的 JSON 给前端展示,强类型反而是一种束缚。
  2. 极简单的临时通信:比如这就只是想弹一个 Toast,写个 Pigeon 接口略显“杀鸡用牛刀”。
  3. 插件内部通信:如果你在写一个极简的插件,不想引入 Pigeon 依赖增加包体积(虽然 Pigeon 主要是 dev_dependency,但生成的代码会增加少量体积)。
  4. 复杂插件/SDK 封装(深层多态与自定义 Codec) Pigeon 的本质是基于 IDL(接口定义语言)的生成器,而 IDL 天生对“类继承(Inheritance)”和“多态(Polymorphism)”支持极弱。

如果你在封装一个重型的底层 SDK,通常会遇到两个死穴:

  • 类层次结构复杂:需要传递极度复杂的深层嵌套对象,且高度依赖多态行为。
  • 特殊的异步控制:无法用简单的 callback 处理,需要接管底层的 async token。

建议:在这种极高复杂度的场景下,不要强迫 Pigeon 做它不擅长的事。真正的工程级解法是“混合双打”——对于标准的 CRUD 指令和配置同步,使用 Pigeon 保障开发效率与类型安全;对于极其复杂的对象传输或需要自定义编解码(Codec)的链路,果断退回到手动配置 StandardMessageCodec 甚至 BasicMessageChannel

十、总结:这是架构升级的必经之路

Pigeon 对于 Flutter 项目的意义,不亚于 TypeScript 对于 JavaScript。

  • 小项目用 MethodChannel 是灵活,大项目用它是隐患。
  • Pigeon 将通信模式从 “口头约定” 升级为 “代码契约”
  • 它是 Add-to-App 混合开发中,连接原生与 Flutter 最稳固的桥梁。

如果大家的项目中有超过 5 个 MethodChannel 调用,可以尝试选取其中一个,按照本文的流程进行 Pigeon 化改造。你会发现,那种“编译通过即运行正常”的安全感,是 MethodChannel 永远给不了的。

HarmonyOS 主流跨平台开发框架对比: ArkUI、Flutter、React Native、KMP、UniApp

作者 Bowen_Jin
2026年2月21日 11:13

前言

随着 HarmonyOS(鸿蒙系统)的快速发展,越来越多的团队开始考虑将现有App迁移到鸿蒙平台,或者在鸿蒙上开发新App。目前,鸿蒙生态中有多种主流跨平台开发框架可供选择,包括:

  • ArkUI(ArkUI-X)(鸿蒙原生框架)

  • Flutter

  • React Native

  • KMP(Kotlin Multiplatform )

  • UniApp(UniApp X)

本文将从多个维度对这些框架进行对比,帮助团队做出明智的技术选型决策。

一、框架概览

框架 官方/社区 主要语言 渲染引擎 核心特点
ArkUI (ArkUI-X) 华为官方 ArkTS ArkUI 渲染引擎 鸿蒙原生体验最佳,ArkUI-X 支持跨鸿蒙/Android/iOS
Flutter Google Dart Skia/Impeller 跨平台一致性最好
React Native Meta JavaScript/TypeScript 原生控件 社区生态庞大,华为开发者联盟主导鸿蒙适配
KMP JetBrains Kotlin 平台原生渲染 代码复用,原生性能
UniApp (UniApp X) DCloud UTS (Uni TypeScript) uvue 渲染引擎 编译为原生代码,鸿蒙原生支持

二、性能对比

2.1 渲染性能

满分100分情况下

ArkUI(⭐⭐⭐⭐⭐)

  • ✅ 华为官方优化,与鸿蒙系统深度集成

  • ✅ 完全使用原生渲染管线,无额外开销

  • ✅ ArkTS 编译为字节码,运行时效率高

毫无疑问 性能100

Flutter(⭐⭐⭐⭐)

  • ✅ 使用 Skia/Impeller 自绘引擎,2025年起 Impeller 渲染引擎逐步替代 Skia,性能提升 30%+

  • ✅ 跨平台一致性好

  • ✅ 编译为 AOT 字节码,运行快

因为是原生渲染,没有任何中间层,所以性能大概95

React Native(⭐⭐⭐⭐)

  • ✅ 新版 Fabric 架构 + JSI(JavaScript Interface)显著提升性能

  • ✅ JSI 直接调用原生接口,消除旧版 Bridge 的 JSON 序列化开销

  • ✅ TurboModules 预加载原生模块,启动速度大幅提升

  • ✅ 支持 React 并发模式,复杂动画和交互更流畅

  • ✅ Hermes 引擎优化后性能有明显提升

虽然是原生渲染, 但需经过 JavaScript 层的转换, 所以性能大概90

KMP(⭐⭐⭐⭐)

  • ✅ 编译为原生代码(Kotlin/Native)

  • ✅ 无虚拟机开销,性能接近纯原生

  • ✅ 使用平台原生 UI 组件,渲染效率高

虽然是原生渲染, 但需经过 Kotlin 层的转换, 所以性能大概90分,后面如果更好的适配,可能会提高到95

UniApp(⭐⭐⭐)

  • ✅ 新一代 UniApp X 使用 UTS 编译为原生代码,性能接近原生应用

  • ✅ uvue 渲染引擎实现原生渲染,不再依赖 WebView

  • ✅ 鸿蒙原生支持,直接编译为鸿蒙原生应用

  • ✅ 复杂场景性能瓶颈大幅缓解

虽然是原生渲染, 但需经过 UTS 层的转换, 所以性能大概85

渲染性能差距大致在 5%–15% 区间,没有明显差异, 复杂动画或高频交互场景下差异可能放大。

2.2 启动速度

ArkUI (⭐⭐⭐⭐⭐)

  • 最优:鸿蒙原生框架,与系统深度集成,无任何额外初始化开销

  • ✅ ArkTS 编译为字节码,启动流程完全由系统优化

  • ✅ 无需加载第三方引擎或虚拟机

Flutter (⭐⭐⭐⭐)

  • ✅ AOT 编译为原生代码,冷启动较快

  • ⚠️ 需要初始化 Skia/Impeller 渲染引擎,有少量额外开销

React Native (⭐⭐⭐⭐)

  • ✅ 新版 Fabric + TurboModules 架构大幅优化了冷启动

  • ⚠️ 需要初始化 JavaScript 引擎(Hermes),有一定初始化开销

KMP (⭐⭐⭐⭐)

  • ✅ Kotlin/Native 编译为原生代码,无虚拟机开销

  • ✅ 使用平台原生 UI,无需额外渲染引擎初始化

  • ✅ 启动流程完全原生,性能与纯原生应用一致

UniApp(⭐⭐⭐)

  • ✅ UTS 编译为原生代码,不再依赖 WebView

  • ⚠️ 需要初始化 uvue 渲染引擎和 UTS 运行时


三、鸿蒙适配 (ArkUI > Flutter = UniApp > React Native > KMP)

ArkUI (⭐⭐⭐⭐⭐ 100分)

  • 官方原生:华为官方框架,与鸿蒙系统深度集成

  • ✅ 支持 Harmony NEXT 纯血鸿蒙

  • ✅ 元服务(原子化服务)原生支持

  • ✅ 可调用所有鸿蒙原生 API

Flutter (⭐⭐⭐⭐ 95分)

  • 华为官方维护:OpenHarmony-Flutter Community 项目

  • ✅ 支持 Harmony NEXT 纯血鸿蒙

  • ✅ 通过 Embedder 层实现适配

  • ✅ 完整的 Flutter 生态可用

  • ✅ 大部分原生插件已适配鸿蒙

  • ✅ 2025年起 Impeller 渲染引擎逐步替代 Skia,性能提升 30%+

React Native (⭐⭐⭐⭐ 90分)

  • 华为开发者联盟主导生态建设:RN-OH(React Native for OpenHarmony)项目提供鸿蒙支持

  • ✅ Fabric 新架构适配持续推进

  • ⚠️ 部分原生模块需要重新适配鸿蒙

KMP (⭐⭐⭐80分)

  • JetBrains 官方支持:Kotlin/Native 支持鸿蒙目标平台

  • ⚠️ UI 层(Compose Multiplatform)鸿蒙适配还在早期阶段

  • ⚠️ 生态还在建设中

UniApp(⭐⭐⭐⭐95分)

  • 官方原生支持:HBuilderX 4.61+ 官方支持 Harmony NEXT

  • ✅ 直接编译为鸿蒙原生应用

  • ✅ 同时支持应用和元服务开发

  • ✅ uvue 原生渲染引擎,性能优秀

  • ✅ 国内生态适配完善,900万开发者,月活突破10亿

  • ✅ 华为、阿里、腾讯、抖音、美团、京东、快手、vivo等公司实际业务使用


四、跨平台能力

框架 Android iOS Windows Mac Linux Web 小程序
ArkUI
Flutter ⚠️
React Native ⚠️ ⚠️
KMP
UniApp (UniApp X) ⚠️ ⚠️ ⚠️

✅官方支持 ❌官方不支持 ⚠️需要通过三方库适配,有交付风险

五、社区成熟度与生态

ArkUI(⭐⭐⭐⭐)

  • 官方支持最强:华为全力维护

  • ✅ 官方文档完善,示例丰富

  • ⚠️ 第三方库生态正在建设中

  • ✅ DevEco Studio 官方 IDE 支持完善

Flutter(⭐⭐⭐⭐⭐)

  • 跨平台生态最成熟,GitHub 星标突破 15.5 万

  • ✅ Pub.dev 上有大量第三方包

  • ✅ 鸿蒙版由华为官方维护(OpenHarmony-Flutter Community)

  • ✅ 社区活跃,问题解决快

  • ✅ 2025年起 Impeller 渲染引擎逐步替代 Skia,性能提升 30%+

React Native(⭐⭐⭐⭐⭐)

  • npm 生态最庞大,GitHub 星标 12.5 万

  • ✅ 大量成熟第三方库

  • ✅ 鸿蒙适配由华为开发者联盟主导生态建设(RN-OH 项目)

  • ⚠️ 部分原生模块需要重新适配鸿蒙

KMP(⭐⭐⭐)

  • JetBrains 官方支持,Kotlin 语言 GitHub 星标超过 45k,KMM 相关生态星标累计突破 11 万

  • ✅ Kotlin 生态成熟

  • ✅ 2023年11月达到稳定状态,2024年获得谷歌官方支持

  • ⚠️ 鸿蒙适配还在早期阶段,2026年已有整合方案

  • ⚠️ UI 层(Compose Multiplatform)鸿蒙支持有限

UniApp(⭐⭐⭐⭐)

  • 国内生态丰富

  • ✅ 插件市场(DCloud 插件市场)资源多

  • ✅ 国内开发者社区活跃,900万开发者,月活突破10亿

  • 鸿蒙原生支持:HBuilderX 4.61+ 官方支持编译到 Harmony NEXT

  • ✅ 同时支持鸿蒙应用和元服务开发

  • ✅ 华为、阿里、腾讯、抖音、美团、京东、快手、vivo等公司实际业务使用

  • ⚠️ 国际影响力较小


六 开发效率

ArkUI(⭐⭐⭐⭐)

  • DevEco Studio 一键真机调试、热重载,官方模板齐全;但 ArkTS 特有语法需额外学习

  • ✅ 官方文档与示例更新快,问题响应及时

Flutter(⭐⭐⭐⭐)

  • Hot Reload 秒级生效,Pub 依赖一键集成;但需处理双端差异与插件适配

  • ✅ 丰富模板与开源项目可直接复用

React Native(⭐⭐⭐⭐)

  • Metro 热更新、Expo 零配置运行;npm 生态即装即用

  • ⚠️ 鸿蒙插件需社区版本,可能需自行封装

KMP(⭐⭐)

  • Compose Multiplatform 预览功能尚不完善,需同时维护 common 与 platform 代码

  • ⚠️ 鸿蒙相关示例稀缺,调试周期长

UniApp(⭐⭐⭐⭐⭐)

  • HBuilderX 可视化拖拽、云端打包、插件市场一键安装;Vue 代码几乎零修改直接编译到鸿蒙

  • ✅ 一套代码同时输出 App、小程序、Web,节省 50% 以上人力

  • ✅ 900万开发者,月活突破10亿,华为、阿里、腾讯等大厂实际使用


七、AI 友好性

ArkUI(⭐⭐⭐⭐)

  • ✅ 华为官方 AI 助手支持

  • ✅ DevEco Studio 内置 AI 代码补全

  • ✅ 支持 ArkTS 代码生成

Flutter(⭐⭐⭐⭐⭐)

  • 最佳:Cursor、Cloud Code、OpenCode、Trae等 AI 工具支持最好

  • ✅ 大量开源代码作为训练数据

  • ✅ AI 能生成高质量 Flutter 代码

React Native(⭐⭐⭐⭐)

  • 优秀:JavaScript/TypeScript 生态 AI 支持成熟

  • ✅ 大量开源项目

KMP(⭐⭐⭐)

  • ⚠️ 一般:Kotlin 支持,但跨平台特定代码 AI 理解有限

UniApp ((⭐⭐⭐)

  • ⚠️ 一般:Vue 支持好,但 UTS 和 UniApp X 特定 API 支持有限

八、最终评分

维度 ArkUI Flutter React Native KMP UniApp (UniApp X)
性能 ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐
鸿蒙适配 ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐ ⭐⭐⭐ ⭐⭐⭐⭐
跨平台能力 ⭐⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐⭐⭐
社区成熟度与生态 ⭐⭐⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐⭐ ⭐⭐⭐⭐
开发效率 ⭐⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐ ⭐⭐⭐⭐⭐
AI 友好性 ⭐⭐⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐ ⭐⭐⭐

九、技术选型建议

9.1: 现有项目适配鸿蒙

一条道走到黑, 以前使用什么框架, 只要框架有针对鸿蒙的支持, 那就继续使用

9.2: 新项目开发,需要支持HarmonyOS

鸿蒙优先

推荐:ArkUI

  • ✅ 性能最优

  • ✅ 原生能力调用最方便

  • ✅ 华为官方支持,长期保障

小程序优先

推荐:UniApp (UniApp X)

  • ✅ 开发效率最高

  • ✅ Vue 生态成熟,UTS 语法类似 TypeScript

  • ✅ 国内生态支持好

  • ✅ 鸿蒙原生支持,可直接编译为鸿蒙原生应用

Web前端团队

推荐:React Native

  • ✅ 前端团队上手快

  • ⚠️ 需评估鸿蒙适配进度

Kotlin 团队,追求原生性能

推荐:KMP + Compose Multiplatform

  • ✅ Kotlin 语言统一

  • ✅ 原生性能

  • ⚠️ 鸿蒙适配还在发展中

其他情况

推荐:Flutter

  • ✅ Google官方支持, 性能、生态和跨平台能力一流

  • ✅ 鸿蒙版由华为官方维护

  • ✅ 跨平台一致性好

  • ✅ 社区和三方库活跃,问题解决快

  • ✅ 大厂成熟案例多

参考资料

Flutter 为什么能运行在 HarmonyOS 上

作者 Bowen_Jin
2026年2月18日 13:42

335328e4fabd7656e8f1e9587269d3a4.jpeg

前言

Flutter 是 Google 推出的跨平台 UI 框架,最初只支持 iOS 和 Android。随着 HarmonyOS 的崛起,Flutter 也能在鸿蒙系统上运行了。这背后到底是怎么实现的呢?本文将从源码层面进行解析。


一、核心原理:Flutter 分层架构

要理解 Flutter 如何在 HarmonyOS 上运行,首先需要了解 Flutter 的架构。Flutter 采用分层设计,从上到下分为三层:

┌─────────────────────────────────┐
│   Framework 层(Dart)           │  ← Flutter 代码
├─────────────────────────────────┤
│   Engine 层(C++)               │  ← 渲染引擎(Impeller)
├─────────────────────────────────┤
│   Embedder 层(平台相关)         │  ← 与操作系统交互(调用 HarmonyOS 原生 API)
└─────────────────────────────────┘

前面两层完全复用现有Dart和C++代码,而 Embedder 层则是为 HarmonyOS 定制的。

关键点:Embedder 层

Embedder 层是 Flutter 能够跨平台运行的关键。它负责:

  • 创建和管理窗口

  • 处理输入事件

  • 调用系统 API

  • 管理渲染 Surface

**不同平台有不同的 Embedder 实现: **

  • Android:platform_view_android.cc

  • iOS:platform_view_ios.mm

  • HarmonyOS:platform_view_ohos.cpp


cc和cpp是标准的C++语言代码后缀

鸿蒙的系统API是C++ 实现的,所以鸿蒙platform_view 使用C++实现进行调用最方便**

二、HarmonyOS Embedder 的核心实现

让我们看看 HarmonyOS Embedder 的核心代码结构:

2.1 平台视图(PlatformViewOHOS)

这是 HarmonyOS Embedder 的核心类,位于: engine/src/flutter/shell/platform/ohos/platform_view_ohos.cpp

class PlatformViewOHOS final : public PlatformView {
 public:
  PlatformViewOHOS(PlatformView::Delegate& delegate,
                   const flutter::TaskRunners& task_runners,
                   const std::shared_ptr<PlatformViewOHOSNapi>& napi_facade,
                   const std::shared_ptr<flutter::OHOSContext>& ohos_context);

  // 通知窗口创建
  void NotifyCreate(fml::RefPtr<OHOSNativeWindow> native_window); 

  // 更新显示尺寸
  void UpdateDisplaySize(int width, int height);

  // 分发平台消息
  void DispatchPlatformMessage(std::string name, void* message, ...);
 private:
  std::shared_ptr<OHOSContext> ohos_context_;  // HarmonyOS 图形上下文
  std::shared_ptr<PlatformViewOHOSNapi> napi_facade_;  // NAPI装饰器(NAPI 是 HarmonyOS 提供的 JavaScript 接口, 用于调用 HarmonyOS 系统 API
  std::unique_ptr<OHOSSurface> ohos_surface_;  // HarmonyOS 渲染 Surface, surface 是渲染的目标画布, 可以是窗口, 也可以是离屏缓冲区
};

**这个类做了什么? **

  1. 继承自 PlatformView(Flutter 的通用平台视图接口)

  2. 持有 HarmonyOS 的图形上下文 OHOSContext

  3. 持有 NAPI装饰器 PlatformViewOHOSNapi(用于调用 HarmonyOS 原生 API)

  4. 管理渲染 Surface OHOSSurface

2.2 Shell 持有者(OHOSShellHolder)

Shell 是 Flutter 引擎的核心,负责管理 Flutter 应用的生命周期、渲染循环、事件处理等, OHOSShellHolder 负责创建和管理 Shell:

class OHOSShellHolder {
 public:
  // 构造函数
  // settings: Flutter 引擎启动参数(如是否启用 Impeller、日志级别等)

  // napi_facade: 与 HarmonyOS 原生层交互的 NAPI 装饰器

  // platform_loop: HarmonyOS 平台线程的 looper,用于投递平台任务
  OHOSShellHolder(const flutter::Settings& settings,
                  std::shared_ptr<PlatformViewOHOSNapi> napi_facade,
                  void* platform_loop);

  // 析构函数:确保 Shell 安全退出并释放所有资源
  ~OHOSShellHolder();
 
  // 启动 Flutter 引擎,加载 Dart 代码并开始渲染
  // hap_asset_provider: HarmonyOS HAP 包资源提供器,用于读取 assets、fonts、kernel_blob 等
  // entrypoint: Dart 入口函数名(默认为 main)

  // libraryUrl: Dart 库 URI(如 package:my_app/main.dart)

  // entrypoint_args: 传给 Dart main 的命令行参数列表

  void Launch(std::unique_ptr<OHOSAssetProvider> hap_asset_provider,
              const std::string& entrypoint,
              const std::string& libraryUrl,
              const std::vector<std::string>& entrypoint_args);
  // 优雅地停止 Flutter Shell,等待所有任务完成后退出
  void Shutdown();
  // 获取 PlatformViewOHOS 的弱引用,用于在平台线程安全地访问平台视图
  fml::WeakPtr<PlatformViewOHOS> GetPlatformView();
  // 设置应用生命周期回调,供 HarmonyOS 通知 Flutter 前后台切换
  void SetLifecycleHandler(std::function<void(AppLifecycleState)> handler);
  // 设置平台消息回调,供 HarmonyOS 主动发消息到 Dart 侧
  void SetPlatformMessageHandler(
      std::function<void(const std::string& channel,
                         const std::vector<uint8_t>& message,
                         std::function<void(std::vector<uint8_t>)> reply)> handler);
  // 向 Dart 侧发送平台消息,支持异步回调
  void SendPlatformMessage(const std::string& channel,
                           const std::vector<uint8_t>& message,
                           std::function<void(std::vector<uint8_t>)> reply = nullptr);
  // 通知 Flutter 引擎窗口尺寸变化,触发重新布局
  void NotifyViewportMetricsChanged(const ViewportMetrics& metrics);
  // 通知 Flutter 引擎内存压力,触发 Dart 侧 GC 或资源释放
  void NotifyLowMemoryWarning();
  // 获取当前 Shell 的运行状态
  enum class ShellState { kNotStarted, kRunning, kShuttingDown, kStopped };
  ShellState GetShellState() const;
  // 返回当前线程安全的 Shell 指针,仅用于调试或测试
  Shell* GetShellUnsafe() const { return shell_.get(); }
 private:
  // 创建并配置 Flutter Shell,内部调用 Shell::Create
  void CreateShell(const flutter::Settings& settings,
                   std::unique_ptr<OHOSAssetProvider> asset_provider);
  // 初始化平台任务执行器,将 HarmonyOS 平台任务映射到 Flutter 的任务队列
  void SetupTaskRunners(void* platform_loop);
  // 注册 HarmonyOS 平台视图到 Shell,完成平台桥接
  void RegisterPlatformView();
  // 加载 Dart AOT 或 Kernel,决定运行模式(Release/Profile 使用 AOT,Debug 使用 Kernel)
  void LoadDartCode(const std::string& entrypoint,
                    const std::string& libraryUrl,
                    const std::vector<std::string>& entrypoint_args);
  // 释放所有资源,顺序:PlatformView → Shell → TaskRunners
  void Teardown();
 private:
  std::unique_ptr<Shell> shell_;                         // Flutter 引擎核心
  std::shared_ptr<PlatformViewOHOSNapi> napi_facade_;  // NAPI 装饰器
  fml::WeakPtrFactory<OHOSShellHolder> weak_factory_;    // 弱引用工厂,防止悬空指针
  ShellState state_ = ShellState::kNotStarted;           // 当前 Shell 状态
  flutter::TaskRunners task_runners_;                    // 跨平台任务队列(UI/GPU/IO/Platform)
  std::mutex state_mutex_;                               // 保护 state_ 的线程安全
};

三、图形渲染适配

Flutter 在 HarmonyOS 上支持三种渲染方式:

3.1 鸿蒙三种渲染方式

enum class OHOSRenderingAPI {
  kSoftware,          // 软件渲染, 基于 CPU 进行渲染, 性能较低, 不依赖于 GPU,适用于简单场景。
  kOpenGLES,          // OpenGL ES 渲染(Skia), 基于 OpenGL ES 进行渲染, 性能较高, 依赖于 GPU, 适用于复杂场景。
  kImpellerVulkan,    // Vulkan 渲染(Impeller), 基于 Vulkan 进行渲染, 性能最高, 依赖于 GPU, 适用于需要高性能渲染的场景。
};

platform_view_ohos.cpp 中,根据渲染方式创建不同的Surface

std::unique_ptr<OHOSSurface> OhosSurfaceFactoryImpl::CreateSurface() {
  switch (ohos_context_->RenderingApi()) {
    case OHOSRenderingAPI::kSoftware:
      return std::make_unique<OHOSSurfaceSoftware>(ohos_context_); // 软件渲染, 基于 CPU 进行渲染, 性能较低, 不依赖于 GPU,适用于简单场景。
    case OHOSRenderingAPI::kOpenGLES:
      return std::make_unique<OhosSurfaceGLSkia>(ohos_context_); // OpenGL ES 渲染(Skia), 基于 OpenGL ES 进行渲染, 性能较高, 依赖于 GPU, 适用于复杂场景。
    case flutter::OHOSRenderingAPI::kImpellerVulkan:
      return std::make_unique<OHOSSurfaceVulkanImpeller>(ohos_context_); // Vulkan 渲染(Impeller), 基于 Vulkan 进行渲染, 性能最高, 依赖于 GPU, 适用于需要高性能渲染的场景。
    default:
      return nullptr;
  }
}

3.2 原生窗口(OHOSNativeWindow)

HarmonyOS 的窗口系统通过 OHNativeWindow 暴露给 Flutter:

class OHOSNativeWindow : public fml::RefCountedThreadSafe<OHOSNativeWindow> {
 public:
  Handle Gethandle() const// 获取 HarmonyOS 原生窗口句柄
  bool IsValid() const;      // 检查窗口是否有效
  SkISize GetSize() const;   // 获取窗口尺寸
 private:
  Handle window_;  // OHNativeWindow*
};

**渲染流程: **

Flutter Engine
    ↓
PlatformViewOHOS
    ↓
OHOSSurface(根据渲染方式创建不同的Surface)
    ↓
OHOSNativeWindow(HarmonyOS 原生窗口)
    ↓
HarmonyOS 图形系统

025444

四、输入事件处理

因为事件处理需要在渲染完成后(VSync同步流程)才能触发, 否则会导致事件处理与渲染不一致的问题。

4.1 VSync 同步

VSync(垂直同步)信号是渲染的关键,它是每次屏幕刷新周期开始时发送的信号,用于同步渲染和显示。

Flutter 需要等待系统的 VSync 信号,才能触发下一帧渲染。

class VsyncWaiterOHOS final : public VsyncWaiter {
 public:
  explicit VsyncWaiterOHOS(const flutter::TaskRunners& task_runners,
                           std::shared_ptr<bool>& enable_frame_cache);

 private:
  OH_NativeVSync* vsync_handle_;  // HarmonyOS VSync 句柄
  void AwaitVSync() override// 等待 VSync 信号
  static void OnVsyncFromOHOS(long long timestamp, void* data); // 接收 HarmonyOS VSync 信号, 通知 Flutter Engine 触发下一帧渲染
};

**工作流程: **

HarmonyOS VSync 信号
    ↓
VsyncWaiterOHOS::OnVsyncFromOHOS
    ↓
通知 Flutter Engine
    ↓
触发下一帧渲染
    ↓
渲染完成
    ↓
触发事件处理

4.2 触摸事件处理

HarmonyOS 的输入事件需要转换为 Flutter 的事件格式:

触摸事件通过 OhosTouchProcessor 处理:

class OhosTouchProcessor {
 public:
  // 处理 HarmonyOS 触摸事件
  void ProcessTouchEvent(const OH_NativeXComponent_TouchEvent* event);
 private:
  // 转换为 Flutter 触摸事件格式
  std::vector<PointerData> ConvertToFlutterTouchEvents(
      const OH_NativeXComponent_TouchEvent* event);
};

五、平台消息通信

Flutter 与 HarmonyOS 的通信通过 Platform Channel 实现:

5.1 NAPI 装饰器(PlatformViewOHOSNapi)

NAPI(Native API)是 HarmonyOS 提供的原生 API 接口:

class PlatformViewOHOSNapi {
 public:
  // 发送平台消息到 HarmonyOS
  void SendPlatformMessage(const std::string& channel,
                           const std::vector<uint8_t>& message);
  // 接收来自 HarmonyOS 的平台消息
  void SetPlatformMessageHandler(
      std::function<void(const std::string&, const std::vector<uint8_t>&)> handler);
 private:
  napi_env env_;  // NAPI 环境
};

5.2 消息处理流程

Flutter 代码(Dart)
    ↓
MethodChannel.invokeMethod
    ↓
PlatformViewOHOS::DispatchPlatformMessage
    ↓
PlatformViewOHOSNapi::SendPlatformMessage
    ↓
HarmonyOS 原生代码(ArkTS/C++)
    ↓
返回结果
    ↓
Flutter 接收响应

六、完整的工作流程

让我们把所有部分串联起来,看看 Flutter 应用在 HarmonyOS 上是如何运行的:

6.1 初始化流程

1. HarmonyOS 应用启动
    ↓
2. 调用 OhosMain::NativeInit(NAPI 入口)
    ↓
3. 创建 OHOSShellHolder
    ↓
4. 创建 PlatformViewOHOS
    ↓
5. 创建 OHOSContext(图形上下文)
    ↓
6. 创建 OHOSSurface(渲染表面)
    ↓
7. 创建 Flutter Shell(引擎)
    ↓
8. 加载 Dart 代码
    ↓
9. 开始渲染

6.2 渲染流程

1. Dart 代码构建 Widget 树
    ↓
2. Framework 层生成 Layer 树
    ↓
3. Engine 层生成 Scene
    ↓
4. Impeller 渲染引擎绘制
    ↓
5. 通过 OHOSSurface 提交绘制指令
    ↓
6. OHOSNativeWindow 接收绘制结果
    ↓
7. HarmonyOS 图形系统显示到屏幕

6.3 事件处理流程

1. 用户触摸屏幕
    ↓
2. HarmonyOS 接收触摸事件
    ↓
3. OhosTouchProcessor 处理
    ↓
4. 转换为 Flutter 触摸事件格式
    ↓
5. PlatformViewOHOS 分发事件
    ↓
6. Framework 层处理事件
    ↓
7. Widget 响应用户操作

七、关键代码示例

7.1 创建 HarmonyOS Embedder

// 创建图形上下文
std::unique_ptr<OHOSContext> CreateOHOSContext(
    const flutter::TaskRunners& task_runners,
    OHOSRenderingAPI rendering_api,
    bool enable_vulkan_validation,
    bool enable_opengl_gpu_tracing,
    bool enable_vulkan_gpu_tracing) {
  switch (rendering_api) {
    case OHOSRenderingAPI::kSoftware:
      return std::make_unique<OHOSContext>(OHOSRenderingAPI::kSoftware);
    case OHOSRenderingAPI::kOpenGLES:
      return std::make_unique<OhosContextGLSkia>(OHOSRenderingAPI::kOpenGLES,
                                                 task_runners);
    case OHOSRenderingAPI::kImpellerVulkan:
      return std::make_unique<OHOSContextVulkanImpeller>(
          enable_vulkan_validation, enable_vulkan_gpu_tracing);
    default:
      return nullptr;
  }
}
// 创建平台视图
PlatformViewOHOS::PlatformViewOHOS(
    PlatformView::Delegate& delegate,
    const flutter::TaskRunners& task_runners,
    const std::shared_ptr<PlatformViewOHOSNapi>& napi_facade,
    const std::shared_ptr<flutter::OHOSContext>& ohos_context)
    : PlatformView(delegate, task_runners),
      napi_facade_(napi_facade),
      ohos_context_(ohos_context) {
  // 创建 Surface 工厂
  surface_factory_ = std::make_shared<OhosSurfaceFactoryImpl>(ohos_context_);
  // 创建渲染 Surface
  ohos_surface_ = surface_factory_->CreateSurface();
  // 预加载 GPU Surface(加速首帧渲染)
  task_runners_.GetRasterTaskRunner()->PostDelayedTask(
      [surface = ohos_surface_]() { surface->PrepareGpuSurface(); },
      fml::TimeDelta::FromMicroseconds(1000));
}

7.2 通知窗口创建

void PlatformViewOHOS::NotifyCreate(
    fml::RefPtr<OHOSNativeWindow> native_window) {
  FML_LOG(INFO) << "NotifyCreate start";
  // 缓存原生窗口
  native_window_ = native_window;
  // 通知 Surface 窗口已创建
  ohos_surface_->SetNativeWindow(native_window);
  // 获取窗口尺寸
  SkISize size = native_window->GetSize();
  // 更新视口尺寸
  UpdateDisplaySize(size.width(), size.height());

  // 通知 Flutter 引擎窗口已创建
  NotifyCreated();
}

7.3 处理平台消息

void PlatformViewOHOS::DispatchPlatformMessage(
    std::string name,
    void* message,
    int messageLength,
    int responseId) {
  // 创建平台消息
  fml::MallocMapping buffer = fml::MallocMapping(
      static_cast<const uint8_t*>(message), messageLength);
  auto platform_message = std::make_unique<PlatformMessage>(
      name,
      std::move(buffer),
      responseId,
      fml::TimePoint::Now());
  // 分发到 Flutter 引擎
  DispatchPlatformMessage(std::move(platform_message));
}

八、为什么 Flutter 能在 HarmonyOS 上运行?

通过上面的代码分析,我们可以总结出以下几个关键原因:

8.1 架构设计优势

Flutter 的分层架构设计使得 Embedder 层可以独立适配不同平台:

  • Framework 层Engine 层是平台无关的

  • 只有 Embedder 层需要针对不同平台实现

8.2 HarmonyOS 提供的开放接口

HarmonyOS 提供了丰富的原生 API,使得 Flutter 可以:

  • 通过 OHNativeWindow 获取窗口句柄

  • 通过 OH_NativeVSync 获取 VSync 信号

  • 通过 NAPI 调用系统能力

  • 通过 XComponent 组件集成 Flutter 视图

8.3 图形接口兼容

HarmonyOS 支持标准的图形接口:

  • OpenGL ES:Skia 渲染引擎可以直接使用

  • Vulkan:Impeller 渲染引擎可以直接使用

  • NativeWindow:提供了跨平台的窗口抽象

8.4 社区共同努力

  • 华为官方和 Flutter 社区共同维护 flutter_flutter 项目

  • 基于 Flutter Engine 源码进行适配

  • 提供完整的开发工具链


从代码层面看,核心就是实现了 PlatformViewOHOSOHOSShellHolderOHOSContext 等类,将 Flutter Engine 与 HarmonyOS 系统连接起来。

**一句话总结:Flutter 通过实现 HarmonyOS 专属的 Embedder 层,将 Flutter Engine 与 HarmonyOS 的窗口系统、图形系统、输入系统对接,从而实现了跨平台运行。 **


九、参考资料

1.Flutter 环境配置 & Shell 基础知识笔记

作者 亮哥666
2026年2月15日 17:03

Flutter 环境配置 & Shell 基础知识笔记


一、Flutter 环境变量配置(实践总结)

需要配置哪些环境变量?

环境变量 是否必须 作用
PATH ✅ 必须 让终端能找到 flutterdart 命令
PUB_HOSTED_URL 🇨🇳 国内必须 Dart 包的下载镜像(不配会很慢或下载失败)
FLUTTER_STORAGE_BASE_URL 🇨🇳 国内必须 Flutter SDK 更新的下载镜像

为什么要配置镜像?

Flutter 默认从 Google 服务器下载资源,国内无法直接访问。配置中国镜像后,所有下载都走国内服务器,速度快且稳定。

常用的中国镜像:

镜像 地址
Flutter 社区镜像 https://pub.flutter-io.cn / https://storage.flutter-io.cn
清华大学镜像 https://mirrors.tuna.tsinghua.edu.cn/dart-pub / https://mirrors.tuna.tsinghua.edu.cn/flutter

我的具体配置

Flutter SDK 安装路径:/Users/hongliangchang/development/flutter

~/.zshrc 末尾添加的内容:

# Flutter 中国镜像(解决国内无法访问 Google 服务器的问题)
export PUB_HOSTED_URL=https://pub.flutter-io.cn
export FLUTTER_STORAGE_BASE_URL=https://storage.flutter-io.cn

# Flutter PATH(让终端能直接使用 flutter 命令)
export PATH="$HOME/development/flutter/bin:$PATH"

# Dart SDK PATH(让终端能直接使用 dart 命令)
export PATH="$HOME/development/flutter/bin/cache/dart-sdk/bin:$PATH"

配置完成后

# 1. 让配置生效
source ~/.zshrc

# 2. 验证 flutter 是否可用
flutter --version

# 3. 检查环境是否完整(会列出缺少的依赖)
flutter doctor

踩坑记录

  1. 配置写错文件:macOS 用的是 zsh,环境变量要写在 ~/.zshrc,不是 ~/.bash_profile
  2. Windows 换行符问题:如果 Flutter SDK 是从 Windows 拷贝过来的,脚本文件会带 \r 换行符,macOS 无法执行,需要在 macOS 上重新下载解压

二、为什么要配置环境变量?

核心原因:让系统知道去哪里找程序。

当你在终端输入 flutter --version 时,系统不会搜遍整个电脑找 flutter,它只会去 PATH 环境变量列出的目录 里找。

# 查看当前 PATH 里有哪些目录
echo $PATH

不配置会怎样?

# ❌ 不配置 PATH,每次必须写完整路径
/Users/hongliangchang/development/flutter/bin/flutter --version

# ✅ 配置了 PATH,直接输名字
flutter --version

通俗比喻:好比手机通讯录存了一个人的号码(配置 PATH),以后打电话搜名字就行。不存的话,每次都得手动输完整手机号码(完整路径)。

PATH 之外的环境变量

环境变量不只是 PATH,还能存各种配置信息:

环境变量 作用
PATH 告诉系统去哪些目录找程序
PUB_HOSTED_URL 告诉 Flutter 从哪个镜像下载 Dart 包(中国镜像加速)
FLUTTER_STORAGE_BASE_URL 告诉 Flutter 从哪个镜像下载 SDK(中国镜像加速)

三、配置文件的区别

不同 Shell 读取不同的配置文件,这是环境变量不生效的常见原因:

Shell 配置文件
bash ~/.bash_profile~/.bashrc
zsh ~/.zshrc~/.zprofile

⚠️ 如果你的 Mac 用的是 zsh,环境变量写在 ~/.bash_profile 里是不生效的,必须写在 ~/.zshrc 里。

配置完后让其生效:

source ~/.zshrc

四、什么是 Shell?

Shell 就是你打开「终端」后,帮你执行命令的程序。可以理解为一个「翻译官」,把你输入的命令翻译给操作系统执行。

常见的 Shell 有 sh、bash、zsh、fish 等,它们功能类似但各有增强。


五、Bash 和 Zsh 是什么?

名称 全称 含义
sh Bourne Shell 最古老的 Shell,以作者 Stephen Bourne 命名
bash Bourne Again Shell sh 的增强版,"重生的 Bourne Shell"(双关语 born again = 重生)
zsh Z Shell bash 的增强版,名字来自普林斯顿助教邵中(Zhong Shao)的用户名

继承关系

sh(祖宗)
 └── bash(儿子,增强版)
      └── zsh(孙子,更强大)

六、macOS 默认用哪个 Shell?

  • macOS Catalina(10.15)之前:默认 bash
  • macOS Catalina(10.15)及之后:默认 zsh

查看当前 Shell:

echo $SHELL
# /bin/zsh → 用的 zsh
# /bin/bash → 用的 bash

为什么苹果要从 bash 换成 zsh?

bash 新版本改用了 GPLv3 许可证,苹果不愿接受。

GPLv3 的核心要求:如果你在产品中使用了 GPLv3 的软件,用户修改了这个软件后,你必须允许用户把修改版装回设备运行

这和苹果的封闭生态冲突——macOS/iOS 的系统文件都有代码签名,不允许用户随意替换。

通俗比喻:苹果卖你一辆车,车里装了一台 GPLv3 的发动机。GPLv3 说车主可以自己改造发动机并装回去,但苹果不愿意让你动它的车。所以苹果换了一台 MIT 许可的发动机(zsh),没有任何限制。

最终苹果的做法:

  • 系统自带的 bash 停留在 3.2 版本(2007 年的,最后一个 GPLv2 版本)
  • 默认 Shell 改为 zsh(MIT 许可证,没有"传染性"要求)

七、Oh-My-Zsh 是什么?

Oh-My-Zsh = zsh 的「插件和主题管理器」,它不改变 zsh 核心功能,而是让体验更好。

zsh = 引擎(自带 Tab 补全等核心功能)
oh-my-zsh = 改装套件(主题 + 插件)
功能 提供者
Tab 补全命令/路径 zsh 自带
Tab 补全时方向键选择 zsh 自带
终端主题/配色 oh-my-zsh
Git 分支显示在命令行 oh-my-zsh 主题
命令别名(如 gst = git status oh-my-zsh 的 git 插件
根据历史记录灰色提示 oh-my-zsh 的 autosuggestions 插件

八、Zsh 命名趣事

zsh 的作者是 Paul Falstad,1990 年在普林斯顿大学读书时开发。当时有个助教叫邵中(Zhong Shao),他的登录用户名是 zsh,Paul 觉得这名字结尾是 sh,很像一个 Shell 的名字,就直接拿来用了。

邵中本人和 zsh 的开发没有任何关系,他后来成为了耶鲁大学计算机科学系教授,研究编程语言和编译器。

❌
❌