跨平台状态管理完全指南:React / Next.js / Flutter / Swift / RxSwift / SwiftUI / ArkTS / RxJava / Agera / Jetpack Compose

由浅入深,从基本概念到源码原理,再到实战案例,系统梳理 Web、移动、原生各平台的状态管理方案


一、什么是状态管理?

1.1 状态(State)的本质

状态是应用在某一时刻的数据快照,它决定了 UI 的展示内容和行为。任何会随时间变化的数据——用户输入、网络请求结果、权限、主题——都可视为状态。

1
2
3
4
5
6
7
8
9
+------------+   +------------+   +------------+   +------------+
| User Input | | Biz Data | | UI State | | Server |
| Form/Search| | List/Detail| |Modal/Load | | Cache |
+------+-----+ +------+-----+ +------+-----+ +------+-----+
| | | |
+-----------------+--------+--------+-----------------+
|
v
UI 渲染 / 副作用执行

用户输入·业务数据·UI状态·服务端 → 汇聚为统一的 UI 渲染与副作用执行

1.2 为什么需要状态管理?

痛点 说明 状态管理的价值
散落 状态分散在各处,难以追踪 集中、可预测的数据流
同步 多处 UI 依赖同一数据,容易不一致 单一数据源(Single Source of Truth)
生命周期 状态何时创建、何时销毁、何时持久化 与组件/页面生命周期绑定
跨层级 深层级组件需要访问顶层状态 提供 Context / Provider / 依赖注入
可测试 业务逻辑与 UI 耦合,难以单测 状态逻辑可独立测试

1.3 各平台状态管理方案概览

平台/框架 主要方案 核心机制 特点
React Hooks (useState/useReducer) 虚拟 DOM diff + 依赖收集 函数式、声明式、生态丰富
Next.js Server Components + Client State 服务端/客户端分离 减少客户端 JS、流式渲染
Flutter Provider / Riverpod / Bloc InheritedWidget / Listenable Dart 异步流、可组合
Swift Combine / 手动 KVO 发布-订阅 系统级、与 SwiftUI 整合
RxSwift Observable / Subject 响应式流 操作符丰富、事件驱动
SwiftUI @State / @Binding / @Observable 声明式 + 属性包装器 数据驱动视图、自动重绘
ArkTS @State / @Prop / @Link 装饰器 + 响应式 鸿蒙声明式 UI
RxJava Observable / Subject 响应式流 异步编排、背压支持
Agera Observable / Updatable 推事件-拉数据 Google 早期方案,已归档
Jetpack Compose remember / mutableStateOf 重组(Recomposition) 声明式、与 Kotlin 协程整合

二、状态管理的核心原则与模式

2.1 单向数据流(Unidirectional Data Flow)

大多数现代框架采用「单向数据流」:状态向下流动,事件向上反馈。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
┌─────────────┐
│ State │ ← 单一数据源
└──────┬──────┘
│ 只读传递

┌─────────────┐
│ View │ → 用户操作
└──────┬──────┘
│ 事件 / Action

┌─────────────┐
│ Reducer / │ → 计算新状态
│ setState │
└──────┬──────┘

└──────────► 更新 State → 重新渲染

React、Redux、Flutter Bloc、Jetpack Compose 的 ViewModel 均遵循此模式。

2.2 观察者模式:统一的底层基石

状态管理大多基于观察者模式:被观察对象变化时,通知所有订阅者。

平台 被观察者 观察者 通知方式
React useState 返回值 组件函数 调度重渲染
SwiftUI @State / @Published 视图 body 自动重算 body
Jetpack Compose mutableStateOf Composable 重组(Recomposition)
Flutter ChangeNotifier addListener notifyListeners
RxJava Observable Observer onNext
ArkTS @State 变量 组件 build 框架触发重绘

2.3 局部状态 vs 全局状态

类型 范围 典型方案 场景
局部状态 单组件/单页面 useState / @State / remember 输入框、折叠面板、动画
共享状态 多组件/跨页面 Context / Provider / ViewModel 主题、用户信息、购物车
服务端状态 与后端同步 React Query / SWR / 自定义 列表、详情、缓存

三、Web 前端:React 与 Next.js

3.1 React 状态管理

3.1.1 基本概念:useState

useState 是 React 最基础的状态 Hook,返回当前值和更新函数:

1
2
3
4
5
6
7
8
function Counter() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(c => c + 1)}>
Count: {count}
</button>
);
}

原理简述:React 在内部维护 Fiber 树,每个组件对应一个 Fiber 节点,useState 将状态存储在 Fiber 的 memoizedState 链表中。更新时调度 setState,标记组件需要重渲染,之后执行 diff 并提交 DOM 变更。

3.1.2 派生状态与 useReducer

当状态逻辑复杂时,用 useReducer 集中处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function reducer(state, action) {
switch (action.type) {
case 'increment': return { count: state.count + 1 };
case 'decrement': return { count: state.count - 1 };
default: return state;
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, { count: 0 });
return (
<>
<span>{state.count}</span>
<button onClick={() => dispatch({ type: 'increment' })}>+</button>
</>
);
}

3.1.3 跨组件共享:Context

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const ThemeContext = createContext('light');

function App() {
const [theme, setTheme] = useState('light');
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
<Header />
<Main />
</ThemeContext.Provider>
);
}

function Header() {
const { theme } = useContext(ThemeContext);
return <div className={theme}>...</div>;
}

注意:Context 变化会导致所有消费该 Context 的组件重渲染,可配合 useMemo / 拆分 Provider 优化。

3.1.4 副作用与依赖:useEffect

1
2
3
4
5
6
7
8
9
10
const [keyword, setKeyword] = useState('');
const [users, setUsers] = useState([]);

useEffect(() => {
if (keyword.length < 2) return;
const timer = setTimeout(() => {
fetchUsers(keyword).then(setUsers);
}, 300);
return () => clearTimeout(timer); // 清理函数
}, [keyword]);

3.2 Next.js 状态管理

Next.js 引入 Server ComponentsClient Components 的区分,状态管理策略随之变化。

3.2.1 服务端 vs 客户端状态

类型 组件 可用能力 典型场景
Server Component 默认 直接 fetch、访问 DB、无 hooks 静态内容、SEO、首屏数据
Client Component 'use client' useState、useEffect、事件处理 交互、表单、实时更新

3.2.2 组合模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// app/page.tsx - Server Component(默认)
async function Page() {
const data = await fetchFromDB(); // 服务端直接拉取
return (
<Layout>
<StaticContent data={data} />
<InteractiveCart /> {/* Client Component,内部用 useState */}
</Layout>
);
}

// components/InteractiveCart.tsx
'use client';
export function InteractiveCart() {
const [items, setItems] = useState([]);
return <CartUI items={items} onAdd={...} />;
}

3.2.3 Context 在 Next.js 中的使用

Context Provider 必须放在 Client Component 中:

1
2
3
4
5
6
7
8
9
10
// providers/ThemeProvider.tsx
'use client';
export function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
{children}
</ThemeContext.Provider>
);
}

四、跨平台移动:Flutter

4.1 基本概念

Flutter 的 UI 是声明式的:给定状态,构建对应 Widget 树。状态变化触发 setStatenotifyListeners,进而重建 Widget。

4.2 内置方案

4.2.1 StatefulWidget + setState

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Counter extends StatefulWidget {
@override
_CounterState createState() => _CounterState();
}

class _CounterState extends State<Counter> {
int _count = 0;
@override
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: () => setState(() => _count++),
child: Text('$_count'),
);
}
}

4.2.2 ValueNotifier + ValueListenableBuilder(局部重建)

1
2
3
4
5
6
7
final counter = ValueNotifier<int>(0);

ValueListenableBuilder<int>(
valueListenable: counter,
builder: (context, value, child) => Text('$value'),
)
// 只有 ValueListenableBuilder 会重建,而非整棵子树

4.2.3 ChangeNotifier + Provider

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class CartModel extends ChangeNotifier {
final List<Item> _items = [];
void add(Item item) {
_items.add(item);
notifyListeners();
}
}

// 根节点
ChangeNotifierProvider(create: (_) => CartModel(), child: MyApp())

// 子节点消费
context.watch<CartModel>(); // 监听变化并重建
context.read<CartModel>(); // 仅读取,不监听

4.3 进阶:Riverpod / Bloc

  • Riverpod:编译期安全、可测试、不依赖 BuildContext
  • Bloc:事件 → 状态 的显式映射,适合复杂业务流

五、iOS 原生:Swift、RxSwift、SwiftUI

5.1 Swift 传统方式

  • 属性观察器willSet / didSet
  • KVOobserve(_:options:changeHandler:)
  • 通知NotificationCenter
  • Delegate / 闭包回调

5.2 RxSwift 响应式状态

RxSwift 将状态抽象为,通过 Observable / Subject 管理:

1
2
3
4
5
6
7
8
9
10
11
// 文本框输入 → 搜索
let searchResults = searchTextField.rx.text.orEmpty
.debounce(.milliseconds(300), scheduler: MainScheduler.instance)
.flatMapLatest { api.search($0) }
.observe(on: MainScheduler.instance)

searchResults
.bind(to: tableView.rx.items(cellIdentifier: "Cell")) { _, model, cell in
cell.textLabel?.text = model.name
}
.disposed(by: disposeBag)

Subject 可同时作为观察者和被观察者,常用于「桥接」命令式代码与响应式流:

1
2
3
4
5
6
7
8
let buttonTaps = PublishSubject<Void>()
buttonTaps
.flatMapLatest { api.fetchData() }
.subscribe(onNext: { updateUI($0) })
.disposed(by: disposeBag)

// 命令式触发
button.rx.tap.bind(to: buttonTaps).disposed(by: disposeBag)

5.3 SwiftUI 声明式状态

5.3.1 @State:组件内部值类型状态

1
2
3
4
5
6
struct CounterView: View {
@State private var count = 0
var body: some View {
Button("Count: \(count)") { count += 1 }
}
}

5.3.2 @Binding:父子双向绑定

1
2
3
4
5
6
7
8
9
10
11
12
13
struct ParentView: View {
@State private var isOn = false
var body: some View {
ToggleView(isOn: $isOn) // 传递 Binding
}
}

struct ToggleView: View {
@Binding var isOn: Bool
var body: some View {
Toggle("", isOn: $isOn)
}
}

5.3.3 @ObservedObject / @StateObject:引用类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class UserViewModel: ObservableObject {
@Published var name = ""
@Published var isLoading = false
}

struct ProfileView: View {
@StateObject private var viewModel = UserViewModel() // 创建并持有
var body: some View {
Text(viewModel.name)
}
}

struct ChildView: View {
@ObservedObject var viewModel: UserViewModel // 从父组件传入
var body: some View { ... }
}

5.3.4 @Observable(iOS 17+)

新宏 @Observable 可替代 ObservableObject,更简洁:

1
2
3
4
5
6
7
8
9
10
11
12
@Observable
class User {
var name: String = ""
var age: Int = 0
}

struct UserView: View {
@State private var user = User()
var body: some View {
Text(user.name) // 自动追踪依赖
}
}

六、Android 原生:RxJava、Agera、Jetpack Compose

6.1 RxJava 响应式状态

1
2
3
4
5
6
7
8
9
val querySubject = PublishSubject.create<String>()
val results = querySubject
.debounce(300, TimeUnit.MILLISECONDS)
.filter { it.length >= 2 }
.switchMap { api.search(it).toObservable() }
.observeOn(AndroidSchedulers.mainThread())

results.subscribe { updateUI(it) }.addTo(compositeDisposable)
querySubject.onNext(editText.text.toString())

StateFlow / SharedFlow(Kotlin Flow)是官方推荐替代 LiveData 的方案:

1
2
3
4
5
6
7
8
9
10
class ViewModel : ViewModel() {
private val _uiState = MutableStateFlow<UiState>(UiState.Loading)
val uiState: StateFlow<UiState> = _uiState.asStateFlow()

fun loadData() {
viewModelScope.launch {
_uiState.value = UiState.Success(repository.fetch())
}
}
}

6.2 Agera 简介(已归档)

Agera 是 Google 早期的轻量级响应式库,采用推事件、拉数据模型:

  • Observable:广播事件
  • Updatable:监听事件,从 Repository 拉取数据
1
2
3
4
5
6
7
8
9
// 概念示例(Agera 已归档,仅作了解)
val repository = Repositories.repositoryWithInitialValue(initialData)
.observe()
.onUpdatesPerLoop()
.getFrom { fetchFromNetwork() }
.compile()

repository.addUpdatable(updatable)
// 事件触发时,Updatable 从 Repository 拉取最新数据

注意:Agera 已于 2023 年 3 月归档,新项目建议使用 Kotlin Flow / StateFlow。

6.3 Jetpack Compose 状态管理

6.3.1 remember + mutableStateOf

1
2
3
4
5
6
7
@Composable
fun Counter() {
var count by remember { mutableStateOf(0) }
Button(onClick = { count++ }) {
Text("Count: $count")
}
}
  • remember:在重组间保持值,避免每次重组重新创建
  • mutableStateOf:创建可观察状态,读该状态的 Composable 会在值变化时重组

6.3.2 状态提升(State Hoisting)

1
2
3
4
5
6
7
8
9
10
11
12
@Composable
fun Parent() {
var count by remember { mutableStateOf(0) }
Child(count = count, onCountChange = { count = it })
}

@Composable
fun Child(count: Int, onCountChange: (Int) -> Unit) {
Button(onClick = { onCountChange(count + 1) }) {
Text("$count")
}
}

6.3.3 ViewModel + StateFlow

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class MyViewModel : ViewModel() {
private val _state = MutableStateFlow(MyState())
val state: StateFlow<MyState> = _state.asStateFlow()

fun update() {
_state.update { it.copy(...) }
}
}

@Composable
fun Screen(viewModel: MyViewModel = viewModel()) {
val state by viewModel.state.collectAsStateWithLifecycle()
// 使用 state
}

6.3.4 配置变更保留:rememberSaveable

1
2
var count by rememberSaveable { mutableStateOf(0) }
// 屏幕旋转等配置变更后,count 会保留

七、鸿蒙:ArkTS

7.1 装饰器驱动的状态

ArkTS 通过装饰器声明「可观察」状态,状态变化自动触发 UI 更新。

7.1.1 @State:组件内部状态

1
2
3
4
5
6
7
8
9
10
11
12
@Entry
@Component
struct Counter {
@State count: number = 0
build() {
Column() {
Text(`Count: ${this.count}`)
Button('+1')
.onClick(() => { this.count++ })
}
}
}

7.1.2 @Prop:父 → 子单向

1
2
3
4
5
6
7
8
9
10
@Component
struct Child {
@Prop value: number // 父组件传入,子组件只读
build() {
Text(`${this.value}`)
}
}

// 父组件
Child({ value: this.count })

7.1.3 @Link:父子双向

1
2
3
4
5
6
7
8
9
10
@Component
struct Child {
@Link value: number // 子组件修改会同步回父组件
build() {
Button('+1').onClick(() => { this.value++ })
}
}

// 父组件
Child({ value: $count }) // $ 语法传递引用

7.1.4 @Provide / @Consume:跨层级

1
2
3
4
5
// 祖先
@Provide('theme') theme: string = 'light'

// 任意后代
@Consume('theme') theme: string

7.1.5 @Observed + @ObjectLink:嵌套对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Observed
class User {
name: string
constructor(name: string) { this.name = name }
}

@Component
struct UserView {
@ObjectLink user: User
build() {
Text(this.user.name)
.onClick(() => { this.user.name = 'New' }) // 触发更新
}
}

7.2 状态管理 V2(@ObservedV2 + @Trace)

V2 支持深层属性观察,解决嵌套对象内部属性不可观察的问题。


八、跨平台对比与选型

8.1 概念映射表

概念 React Flutter SwiftUI Jetpack Compose ArkTS
组件内状态 useState State @State remember + mutableStateOf @State
父子单向 props 构造函数参数 普通参数 参数 @Prop
父子双向 回调 + props 回调 @Binding 回调 + 参数 @Link
跨层级 Context Provider/InheritedWidget EnvironmentObject CompositionLocal @Provide/@Consume
引用类型 useRef/Context ChangeNotifier @ObservableObject ViewModel @Observed+@ObjectLink

8.2 选型建议

场景 推荐方案
简单 UI 状态 各平台内置(useState / @State / remember)
跨组件共享 Context / Provider / ViewModel / @Provide
复杂异步流 RxSwift / RxJava / Kotlin Flow
服务端数据 React Query / SWR / ViewModel + Repository
Next.js 全栈 Server Components 拉数据 + Client 管理交互态
新 Android 项目 Jetpack Compose + ViewModel + StateFlow
新 iOS 项目 SwiftUI + @Observable
鸿蒙应用 ArkUI 装饰器体系

九、源码原理浅析

9.1 React:Fiber 与 Hooks 链表

React 在 Fiber 节点上维护 memoizedState 链表,每个 Hook 对应链表中的一个节点:

1
2
3
4
5
Fiber.memoizedState → useState → useEffect → useContext → ...

├─ baseState
├─ baseQueue
└─ next (下一个 Hook)

setState 会将更新放入队列,调度器在合适时机执行重渲染,按顺序应用更新,保证 Hooks 调用顺序稳定。

9.2 Jetpack Compose:快照与重组

Compose 使用 Snapshot 系统追踪状态读取:

  1. mutableStateOf 创建 SnapshotMutableState
  2. 读取 state.value 时,当前 Composable 的「重组作用域」会记录对该 state 的依赖
  3. 写入 state.value = x 时,Compose 标记依赖该 state 的作用域需要重组
  4. 重组时重新执行对应 Composable,得到新 UI
1
读取 state → 记录依赖 → 写入 state → 标记无效 → 调度重组 → 重新执行 Composable

9.3 SwiftUI:@State 与依赖追踪

SwiftUI 在编译期和运行期结合,追踪 body 中对 @State 等属性的访问。当 @State 变化时,视图的 body 会重新求值,生成新的 View 描述,再与旧描述 diff 后更新真实视图。

9.4 ArkTS:装饰器与更新调度

@State 等装饰器在编译期生成观察逻辑,运行时状态变化会标记组件为脏,下一帧统一执行 build 更新 UI,类似 Flutter 的 setState + 帧调度。


十、实战案例

10.1 登录表单校验(多字段组合)

需求:用户名 ≥3 字符、密码 ≥6 字符时,登录按钮才可点击。

React

1
2
3
4
const [user, setUser] = useState('');
const [pwd, setPwd] = useState('');
const canLogin = user.length >= 3 && pwd.length >= 6;
return <Button disabled={!canLogin}>登录</Button>;

SwiftUI

1
2
3
4
@State private var user = ""
@State private var pwd = ""
var canLogin: Bool { user.count >= 3 && pwd.count >= 6 }
Button("登录") { }.disabled(!canLogin)

Jetpack Compose

1
2
3
4
var user by remember { mutableStateOf("") }
var pwd by remember { mutableStateOf("") }
val canLogin = user.length >= 3 && pwd.length >= 6
Button(onClick = {}, enabled = canLogin) { Text("登录") }

ArkTS

1
2
3
4
5
6
@State user: string = ''
@State pwd: string = ''
private get canLogin(): boolean {
return this.user.length >= 3 && this.pwd.length >= 6
}
Button('登录').enabled(this.canLogin)

10.2 搜索防抖 + 取消旧请求

React

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const [keyword, setKeyword] = useState('');
const [results, setResults] = useState([]);

useEffect(() => {
if (keyword.length < 2) return;
const timer = setTimeout(() => {
let cancelled = false;
search(keyword).then(data => {
if (!cancelled) setResults(data);
});
return () => { cancelled = true; };
}, 300);
return () => clearTimeout(timer);
}, [keyword]);

RxSwift

1
2
3
4
5
6
7
searchTextField.rx.text.orEmpty
.filter { $0.count >= 2 }
.debounce(.milliseconds(300), scheduler: MainScheduler.instance)
.flatMapLatest { api.search($0) }
.observe(on: MainScheduler.instance)
.bind(to: resultsRelay)
.disposed(by: disposeBag)

Jetpack Compose + ViewModel

1
2
3
4
5
6
val query = MutableStateFlow("")
val results = query
.debounce(300)
.filter { it.length >= 2 }
.flatMapLatest { repository.search(it) }
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList())

10.3 购物车总价实时计算

Flutter (Provider)

1
2
3
4
5
class CartModel extends ChangeNotifier {
final List<CartItem> _items = [];
double get total => _items.fold(0, (sum, item) => sum + item.price * item.qty);
void add(CartItem item) { _items.add(item); notifyListeners(); }
}

React

1
2
3
4
5
const [items, setItems] = useState([]);
const total = useMemo(
() => items.reduce((s, i) => s + i.price * i.qty, 0),
[items]
);

10.4 Next.js 服务端数据 + 客户端状态

1
2
3
4
5
6
7
8
9
10
// app/product/[id]/page.tsx
export default async function Page({ params }: { params: { id: string } }) {
const product = await fetchProduct(params.id); // 服务端拉取
return (
<div>
<ProductInfo product={product} />
<AddToCartButton productId={product.id} /> {/* 内部用 useState 管理数量 */}
</div>
);
}

十一、总结与最佳实践

11.1 核心要点

维度 共性
本质 状态驱动 UI,变化触发更新
模式 单向数据流、观察者、单一数据源
局部 vs 全局 按范围选择合适的共享机制
生命周期 状态与组件/页面生命周期绑定,避免泄漏

11.2 最佳实践

  1. 最小化状态:能推导的不要存储,用 useMemo / computed / getter
  2. 状态提升:当多组件需要共享时,提升到共同祖先
  3. 不可变更新:避免直接修改,使用 setState/copy/扩展运算符
  4. 副作用清理useEffect 清理、Disposable.disposeviewModelScope
  5. 服务端状态分离:与 UI 状态区分,用专门库(React Query 等)管理
  6. 类型安全:TypeScript、Swift、Kotlin 充分利用类型约束状态结构

11.3 各平台快速对照

平台 局部状态 共享状态 异步/流式
React useState Context / Redux useEffect / React Query
Next.js useState (Client) Context (Client) Server Components + fetch
Flutter State / ValueNotifier Provider / Riverpod Stream / Future
SwiftUI @State @ObservableObject / Environment async/await
RxSwift Observable / Subject 同上 + Subject Observable 链
Compose remember + mutableStateOf ViewModel + StateFlow Flow / LaunchedEffect
ArkTS @State @Provide / @Consume Promise / async

参考资源

跨平台与原生交互完全指南:Flutter × Electron

由浅入深,从基本概念到源码解析,全面掌握 Flutter 与 Android/iOS/鸿蒙、Electron 与 Mac/Windows 的原生交互能力


一、为什么需要原生交互?

1.1 跨平台框架的局限

跨平台框架(Flutter、Electron、React Native 等)为开发效率带来巨大提升,但受限于自身运行时,无法直接访问所有原生能力:

场景 Flutter 移动端 Electron 桌面端
硬件访问 相机、蓝牙、NFC、传感器 串口、USB、显卡驱动
系统 API 推送、定位、生物识别 剪贴板、系统托盘、原生菜单
第三方 SDK 微信/支付宝支付、地图 企业认证、硬件加密狗
性能敏感 视频编解码、图像处理 大文件加解密、实时音视频
平台特性 iOS Live Activities、Android WorkManager macOS Touch Bar、Windows 通知中心

核心矛盾:跨平台代码运行在「沙箱」中,必须通过桥接层与原生世界通信。

1.2 两种典型架构对比

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
┌─────────────────────────────────────────────────────────────────────────┐
│ Flutter 移动端(Android / iOS / 鸿蒙) │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ Dart (UI/逻辑) ──► Platform Channel ──► Native (Kotlin/Swift/ArkTS) │
│ MethodChannel EventChannel Android/iOS/HarmonyOS API │
│ BasicMessageChannel │
│ │
└─────────────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────────────┐
│ Electron 桌面端(Mac / Windows / Linux) │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ Renderer (HTML/JS) ──► IPC ──► Main Process ──► Native Addon │
│ contextBridge Node-API / FFI │
│ .node / .dll / .dylib │
│ │
└─────────────────────────────────────────────────────────────────────────┘

二、Flutter 与原生交互

2.1 基本概念

2.1.1 Platform Channel 架构

Flutter 的 Platform Channel 是 Dart 与原生代码之间的消息传递机制,底层基于 BinaryMessenger 进行异步二进制通信。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
┌──────────────────┐         channel name          ┌──────────────────┐
│ │ ◄────────────────────────► │ │
│ Dart 端 │ ByteData (序列化消息) │ Native 端 │
│ (Client) │ Future/Stream 响应 │ (Host) │
│ │ │ │
│ MethodChannel │ │ MethodChannel │
│ EventChannel │ │ EventChannel │
│ BasicMessageChannel │ BasicMessageChannel │
└──────────────────┘ └──────────────────┘
│ │
│ StandardMessageCodec (JSON-like 序列化) │
│ 支持: null, bool, int, double, String │
│ Uint8List, List, Map │
▼ ▼
Flutter Engine (C++) ──────────────────── Platform Embedder

2.1.2 三种 Channel 类型辨析

Channel 类型 通信模式 典型场景 特点
MethodChannel 请求-响应(RPC) 获取电池电量、调用支付 SDK、打开相机 最常用,一对一调用,返回 Future
EventChannel 流式数据(Stream) 监听传感器、位置更新、蓝牙扫描 单向流,Native → Dart,支持 cancel
BasicMessageChannel 简单消息传递 低层级自定义协议 无方法语义,纯消息收发,较少直接使用

2.1.3 数据类型映射(Dart ↔ Native)

Dart Kotlin/Java Swift/ObjC ArkTS (鸿蒙)
null null nil null
bool Boolean Bool / NSNumber boolean
int (≤32位) Int Int32 number
int (>32位) Long Int64 number
double Double Double number
String String String string
Uint8List ByteArray FlutterStandardTypedData Uint8Array
List List Array Array
Map HashMap Dictionary Object

2.2 MethodChannel 实战:获取电池电量

2.2.1 Dart 端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import 'package:flutter/services.dart';

class _MyHomePageState extends State<MyHomePage> {
static const platform = MethodChannel('samples.flutter.dev/battery');
String _batteryLevel = 'Unknown';

Future<void> _getBatteryLevel() async {
try {
final int level = await platform.invokeMethod<int>('getBatteryLevel');
setState(() => _batteryLevel = 'Battery level: $level%');
} on PlatformException catch (e) {
setState(() => _batteryLevel = "Failed: '${e.message}'");
}
}

@override
Widget build(BuildContext context) {
return Column(
children: [
ElevatedButton(onPressed: _getBatteryLevel, child: Text('Get Battery')),
Text(_batteryLevel),
],
);
}
}

要点

  • Channel 名必须与原生端完全一致
  • invokeMethod 返回 Future,支持泛型
  • 捕获 PlatformException 处理原生端 result.error()

2.2.2 Android (Kotlin)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// MainActivity.kt
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel

class MainActivity : FlutterActivity() {
private val CHANNEL = "samples.flutter.dev/battery"

override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
MethodChannel(
flutterEngine.dartExecutor.binaryMessenger,
CHANNEL
).setMethodCallHandler { call, result ->
when (call.method) {
"getBatteryLevel" -> {
val level = getBatteryLevel()
if (level != -1) {
result.success(level)
} else {
result.error("UNAVAILABLE", "Battery level not available.", null)
}
}
else -> result.notImplemented()
}
}
}

private fun getBatteryLevel(): Int {
val batteryManager = getSystemService(Context.BATTERY_SERVICE) as BatteryManager
return batteryManager.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY)
}
}

2.2.3 iOS (Swift)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// AppDelegate.swift
import Flutter
import UIKit

@main
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
let controller = window?.rootViewController as! FlutterViewController
let channel = FlutterMethodChannel(
name: "samples.flutter.dev/battery",
binaryMessenger: controller.binaryMessenger
)
channel.setMethodCallHandler { [weak self] (call, result) in
guard call.method == "getBatteryLevel" else {
result(FlutterMethodNotImplemented)
return
}
self?.receiveBatteryLevel(result: result)
}
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}

private func receiveBatteryLevel(result: FlutterResult) {
let device = UIDevice.current
device.isBatteryMonitoringEnabled = true
if device.batteryState == .unknown {
result(FlutterError(code: "UNAVAILABLE", message: "Battery level not available.", details: nil))
} else {
result(Int(device.batteryLevel * 100))
}
}
}

2.2.4 鸿蒙 (ArkTS)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// EntryAbility.ets
import type { FlutterPlugin } from '@ohos/flutter';

export default class EntryAbility extends UIAbility {
onWindowStageCreate(windowStage: window.WindowStage) {
const flutterEngine = getFlutterEngine();
const messenger = flutterEngine.getBinaryMessenger();

const channel = new MethodChannel(messenger, 'samples.flutter.dev/battery');
channel.setMethodCallHandler(async (call) => {
if (call.method === 'getBatteryLevel') {
const level = await this.getBatteryLevel();
return level;
}
throw new Error('NotImplemented');
});
}

private async getBatteryLevel(): Promise<number> {
const batteryInfo = await battery.getBatteryInfo();
return batteryInfo.batterySoc; // 0-100
}
}

2.3 EventChannel 实战:监听位置更新

适用于持续从 Native 向 Dart 推送数据的场景。

2.3.1 Dart 端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import 'dart:async';
import 'package:flutter/services.dart';

class LocationService {
static const _channel = EventChannel('samples.flutter.dev/location');

Stream<Location> get locationStream => _channel
.receiveBroadcastStream()
.map((dynamic event) => Location.fromMap(Map<String, dynamic>.from(event as Map)));
}

// 使用
StreamSubscription? _subscription;

void _startListening() {
_subscription = LocationService().locationStream.listen(
(loc) => print('Lat: ${loc.lat}, Lng: ${loc.lng}'),
onError: (e) => print('Error: $e'),
);
}

void _stopListening() {
_subscription?.cancel();
}

2.3.2 Android (Kotlin) - EventChannel.StreamHandler

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class LocationStreamHandler : EventChannel.StreamHandler {
private var eventSink: EventChannel.EventSink? = null
private var locationCallback: LocationCallback? = null

override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
eventSink = events
locationCallback = object : LocationCallback() {
override fun onLocationResult(result: LocationResult) {
for (loc in result.locations) {
eventSink?.success(mapOf(
"lat" to loc.latitude,
"lng" to loc.longitude
))
}
}
}
fusedLocationClient.requestLocationUpdates(request, locationCallback!!, Looper.getMainLooper())
}

override fun onCancel(arguments: Any?) {
locationCallback?.let { fusedLocationClient.removeLocationUpdates(it) }
eventSink = null
}
}

// 注册
EventChannel(flutterEngine.dartExecutor.binaryMessenger, "samples.flutter.dev/location")
.setStreamHandler(LocationStreamHandler())

2.4 底层原理:BinaryMessenger

所有 Channel 的底层都依赖 BinaryMessenger

1
2
3
4
5
6
7
8
9
Dart 侧 (binary_messenger.dart)
├── send(channel, message) → Future<ByteData?>
├── setMessageHandler(channel, handler)
└── 消息以 ByteData 形式序列化传输

Native 侧 (Android: DartExecutor, iOS: FlutterBinaryMessenger)
├── 实现 BinaryMessenger 接口
├── 通过 Flutter Engine 与 Dart 隔离
└── 线程:Android 主线程,iOS 主线程

关键点

  • 消息是异步的,保证 UI 不阻塞
  • 序列化由 StandardMessageCodec 完成(或自定义 MessageCodec
  • Channel 名称是路由键,用于区分不同功能

2.5 Pigeon:类型安全的 Platform Channel

手动维护 MethodChannel 容易出错(字符串拼写、类型不匹配)。Pigeon 是 Flutter 官方代码生成工具,从接口定义自动生成各端代码。

2.5.1 定义接口 (pigeon/api.dart)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import 'package:pigeon/pigeon.dart';

@HostApi()
abstract class BatteryApi {
int getBatteryLevel();
}

@HostApi()
abstract class LocationApi {
void startLocationUpdates();
void stopLocationUpdates();
}

@FlutterApi()
abstract class LocationCallback {
void onLocation(double lat, double lng);
}

2.5.2 生成代码

1
2
3
# pubspec.yaml
dev_dependencies:
pigeon: ^26.0.0
1
flutter pub run pigeon --input pigeon/api.dart

会生成:

  • dart/*.dart:Dart 端接口实现
  • kotlin/*.kt:Android Kotlin 实现骨架
  • swift/*.swift:iOS Swift 实现骨架

2.5.3 优势

对比 手动 MethodChannel Pigeon
类型安全 运行时检查 编译期保证
维护成本 三端同步改 改接口定义即可
代码量 重复样板 自动生成

2.6 实际项目应用案例

案例 1:支付 SDK 接入

1
2
3
4
5
6
7
业务需求:Flutter 调起微信/支付宝支付,并接收支付结果回调

实现要点:
1. MethodChannel 调用 Native 调起支付
2. 支付结果通过 EventChannel 或 MethodChannel.invokeMethod 反向回调
3. Android: Activity Result API,iOS: URL Scheme / Universal Links
4. 注意:支付必须在主线程/主 Activity 完成

案例 2:相机自定义预览 + 拍照

1
2
3
4
5
6
7
业务需求:自定义相机 UI,支持滤镜、闪光灯、变焦

实现要点:
1. 使用 Platform View (AndroidView / UiKitView) 嵌入原生相机 View
2. MethodChannel 控制:开始预览、拍照、切换滤镜
3. EventChannel 或 Texture 传递预览帧(如需实时处理)
4. 注意线程:相机回调可能在子线程,需 Post 到主线程再通过 Channel 回传

案例 3:蓝牙设备扫描与连接

1
2
3
4
5
6
7
业务需求:扫描 BLE 设备,连接并读写特征值

实现要点:
1. EventChannel 持续推送扫描到的设备列表
2. MethodChannel 执行:连接、断开、读/写特征值
3. 连接状态、数据回调通过 EventChannel 流式回传
4. 鸿蒙侧使用 @ohos.bluetooth 相关 API,需申请权限

三、Electron 与原生交互

3.1 基本概念

3.1.1 Electron 架构回顾

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
┌─────────────────────────────────────────────────────────────────────┐
│ Renderer Process (多个) │
│ HTML + CSS + JavaScript (前端界面) │
│ 受沙箱限制,无法直接访问 Node.js / 原生模块 │
└───────────────────────────────────┬─────────────────────────────────┘
│ IPC (ipcRenderer / contextBridge)
┌───────────────────────────────────▼─────────────────────────────────┐
│ Main Process (单个) │
│ Node.js 运行时,可 require('fs')、require('原生模块') │
│ 可加载 .node (Native Addon)、调用 FFI │
└───────────────────────────────────┬─────────────────────────────────┘
│ Node-API / N-API
┌───────────────────────────────────▼─────────────────────────────────┐
│ Native Addon / 系统 API │
│ C/C++ 编写,编译为 .node (Unix) / .dll (Windows) │
│ 或通过 FFI 调用已有 .dll / .dylib / .so │
└─────────────────────────────────────────────────────────────────────┘

核心原则:所有原生调用必须在 Main Process 完成,Renderer 通过 IPC 与 Main 通信。

3.1.2 两种原生交互方式

方式 适用场景 技术栈
Native Addon 自己用 C++ 写模块,编译成 .node Node-API / N-API、node-gyp、node-addon-api
FFI 调用已有的 C 库(.dll/.dylib/.so) node-ffi-napi、koffi

3.2 Native Addon 开发(Node-API)

3.2.1 环境准备

macOS

1
xcode-select --install  # 安装 Xcode Command Line Tools

Windows

  • 安装 Node.js 时勾选 “Tools for Native Modules”
  • 或通过 npm install -g windows-build-tools 安装 VS Build Tools

依赖

1
2
npm install node-addon-api bindings
npm install -g node-gyp

3.2.2 项目结构

1
2
3
4
5
6
native-addon/
├── binding.gyp # 构建配置
├── src/
│ └── addon.cc # C++ 源码
├── package.json
└── index.js # 对外导出

3.2.3 binding.gyp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{
"targets": [{
"target_name": "my_addon",
"sources": [ "src/addon.cc" ],
"include_dirs": [
"<!@(node -p \"require('node-addon-api').include\")"
],
"dependencies": [
"<!(node -p \"require('node-addon-api').gyp\")"
],
"cflags!": [ "-fno-exceptions" ],
"cflags_cc!": [ "-fno-exceptions" ],
"defines": [ "NAPI_DISABLE_CPP_EXCEPTIONS" ],
"conditions": [
["OS=='win'", { "msvs_settings": { "VCCLCompilerTool": { "ExceptionHandling": 1 } } }]
]
}]
}

3.2.4 C++ 源码 (src/addon.cc)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <napi.h>

Napi::Value Add(const Napi::CallbackInfo& info) {
Napi::Env env = info.Env();
if (info.Length() < 2 || !info[0].IsNumber() || !info[1].IsNumber()) {
Napi::TypeError::New(env, "Number expected").ThrowAsJavaScriptException();
return env.Null();
}
double a = info[0].As<Napi::Number>().DoubleValue();
double b = info[1].As<Napi::Number>().DoubleValue();
return Napi::Number::New(env, a + b);
}

Napi::Object Init(Napi::Env env, Napi::Object exports) {
exports.Set("add", Napi::Function::New(env, Add));
return exports;
}

NODE_API_MODULE(my_addon, Init)

3.2.5 调用方 (index.js)

1
2
const addon = require('bindings')('my_addon');
console.log(addon.add(1.5, 2.3)); // 3.8

3.2.6 在 Electron 中使用

关键:Electron 的 Node.js 版本与官方 Node 不同,需要重新编译 Native Addon:

1
2
npm install @electron/rebuild --save-dev
npx electron-rebuild

或在 package.json 中配置:

1
2
3
4
5
{
"scripts": {
"rebuild": "electron-rebuild"
}
}

Main Process 中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// main.js
const { app, BrowserWindow, ipcMain } = require('electron');
const addon = require('bindings')('my_addon');

let win;

function createWindow() {
win = new BrowserWindow({
webPreferences: { nodeIntegration: false, contextIsolation: true }
});
win.loadFile('index.html');
}

ipcMain.handle('add-numbers', (event, a, b) => {
return addon.add(a, b);
});

app.whenReady().then(createWindow);

Preload (preload.js)

1
2
3
4
5
const { contextBridge, ipcRenderer } = require('electron');

contextBridge.exposeInMainWorld('native', {
add: (a, b) => ipcRenderer.invoke('add-numbers', a, b)
});

Renderer

1
2
const result = await window.native.add(1.5, 2.3);
console.log(result); // 3.8

3.3 FFI:调用已有 C 库

当需要调用已有的 .dll (Windows)、.dylib (macOS)、.so (Linux) 时,使用 FFI 无需重写 C++ 代码。

3.3.1 使用 koffi(推荐,支持 N-API)

1
npm install koffi
1
2
3
4
5
6
7
8
9
10
11
12
13
const koffi = require('koffi');

// 加载系统库
const libc = koffi.load('libc.so.6'); // Linux
// const libc = koffi.load('msvcrt.dll'); // Windows
// const libc = koffi.load('libc.dylib'); // macOS

const ceil = libc.func('double ceil(double)', 'ceil');
console.log(ceil(3.2)); // 4

// 加载自定义 DLL
const myLib = koffi.load('./my_lib.dll');
const myFunc = myLib.func('int my_func(const char* str, int len)', 'my_func');

3.3.2 使用 node-ffi-napi(注意 Electron 兼容性)

1
2
3
4
5
6
7
8
const ffi = require('ffi-napi');
const ref = require('ref-napi');

const msvcrt = ffi.Library('msvcrt', {
'ceil': ['double', ['double']]
});

console.log(msvcrt.ceil(3.2)); // 4

注意:Electron 20.3.8+ 的沙箱可能限制指针操作,导致 ffi-napi 崩溃,可考虑:

  • 使用 koffi(基于 N-API,兼容性更好)
  • 或通过 Main Process 调用,避免在 Renderer 使用 FFI

3.4 实际项目应用案例

案例 1:调用 Windows 系统 API(获取 CPU 使用率)

1
2
3
4
5
6
7
需求:在 Electron 应用中显示实时 CPU 使用率

实现:
1. 使用 node-ffi-napi 或 koffi 加载 kernel32.dll / pdh.dll
2. 调用 GetSystemTimes、PdhCollectQueryData 等 API
3. Main Process 定时采样,通过 IPC 推送到 Renderer 展示
4. 跨平台:macOS 使用 sysctl,Linux 使用 /proc/stat

案例 2:硬件加密狗认证

1
2
3
4
5
6
7
需求:桌面端软件需插入 USB 加密狗才能使用

实现:
1. 厂商提供 .dll / .so SDK
2. 通过 FFI 加载 SDK,调用 CheckDongle()、GetLicense() 等
3. Main Process 启动时校验,失败则禁止启动
4. 注意:.dll 路径需随应用打包正确配置(如 resources/ 目录)

案例 3:大文件加解密(性能敏感)

1
2
3
4
5
6
7
需求:对 GB 级文件进行 AES 加解密,纯 JS 太慢

实现:
1. 用 C++ 编写加解密模块,基于 OpenSSL 或 crypto 库
2. 编译为 Native Addon,暴露 encryptFile(path)、decryptFile(path)
3. Main Process 调用,通过 Stream 或进度回调推送到 Renderer
4. 可考虑 Worker 线程 + 原生模块,避免阻塞主进程

四、对比与选型

4.1 Flutter vs Electron 原生交互对比

维度 Flutter (移动端) Electron (桌面端)
通信机制 Platform Channel(异步消息) IPC + Native Addon / FFI
数据格式 StandardMessageCodec(JSON-like) 任意(JS 对象 ↔ C 结构体需手动处理)
类型安全 可配合 Pigeon 生成 需自行保证
多端一致性 同一套 Channel 名,各端实现不同 同一套 Addon/FFI 调用,各平台编译不同
性能 消息序列化有开销,适合中低频调用 Native Addon 直接调用,适合高性能场景
调试 可通过日志追踪 Channel 消息 需 gdb/lldb 调试 C++

4.2 何时用 MethodChannel / EventChannel / FFI / Native Addon

需求 推荐方案
单次调用原生 API(如获取电池) Flutter MethodChannel / Electron IPC + Addon
持续接收原生数据流(如传感器) Flutter EventChannel
调用已有 C 库,不想重写 Electron FFI
高性能计算、自定义算法 Native Addon(C++)
需要类型安全、少写样板 Flutter Pigeon

五、最佳实践与注意事项

5.1 Flutter

  1. Channel 命名:使用反向域名,如 com.yourapp.service/battery,避免冲突
  2. 主线程:Android/iOS 的 Channel 回调一般在主线程,耗时操作应异步处理
  3. 错误处理:原生端务必调用 result.error()result.notImplemented(),避免 Dart 侧 Future 一直挂起
  4. 内存泄漏:EventChannel 的 onCancel 必须移除监听器、释放资源
  5. 插件化:可复用逻辑封装为 Flutter Plugin,发布到 pub.dev

5.2 Electron

  1. 安全:禁用 Renderer 的 nodeIntegration,仅通过 contextBridge 暴露必要 API
  2. 重建:每次升级 Electron 版本后执行 electron-rebuild
  3. 路径:Native Addon 的 .node 文件路径在打包后可能变化,使用 __dirnameapp.getPath('userData') 等正确处理
  4. 崩溃:C++ 模块崩溃会导致整个进程退出,需做好 try-catch 和日志

5.3 鸿蒙特别说明

  • Flutter 对 OpenHarmony/HarmonyOS 的支持在快速演进,需关注官方文档和 flutter_harmony 等生态
  • ArkTS 与 Dart 的 MethodChannel 接口类似,但需使用鸿蒙提供的 FlutterPluginMethodChannel 等 API
  • 权限、包名、签名等需符合鸿蒙应用规范

六、总结

技术栈 核心机制 典型用途
Flutter + Android/iOS/鸿蒙 Platform Channel(MethodChannel / EventChannel) 移动端调用相机、支付、蓝牙等
Electron + Mac/Windows IPC + Native Addon / FFI 桌面端调用系统 API、硬件、高性能计算

掌握原生交互能力,是跨平台开发从「能写」到「写好」的关键一步。建议先跑通官方示例,再结合实际业务逐步封装和优化。


附录:参考资源

Android / iOS / 鸿蒙 / React / Flutter 响应式编程共同点分析

由浅入深,从基本概念到源码原理,再到实战案例,系统梳理五大主流开发框架中的响应式编程共性与差异


一、什么是响应式编程?

1.1 从命令式到声明式

无论使用哪个平台,传统命令式编程的本质都是「我告诉程序每一步该做什么」:

1
2
3
4
5
6
7
8
9
// Android 命令式:手动监听 + 条件判断
editText.addTextChangedListener(object : TextWatcher {
override fun onTextChanged(s: CharSequence?, p1: Int, p2: Int, p3: Int) {
val text = text.toString()
if (text.length >= 3) {
searchUsers(text)
}
}
})
1
2
3
4
5
6
// iOS 命令式:Target-Action
textField.addTarget(self, action: #selector(textDidChange), for: .editingChanged)
func textDidChange() {
let text = textField.text ?? ""
if text.count >= 3 { searchUsers(text) }
}

响应式编程则将数据流(Data Stream)视为核心,用声明式方式描述「当数据变化时该做什么」:

1
2
命令式:监听 → 判断 → 执行
响应式:数据流 → 转换/过滤 → 订阅并响应

1.2 响应式编程的三大特征

特征 说明 跨平台体现
数据流 事件、状态、异步结果统一抽象为「流」 Observable / Signal / Stream / State
声明式 描述「是什么」而非「怎么做」 链式操作符、装饰器、Hooks
自动传播 依赖变化自动触发更新 订阅机制、依赖收集、重新渲染

1.3 五大平台的响应式方案概览

平台 主要方案 核心类型 特点
Android RxJava / Kotlin Flow Observable / Flow 操作符丰富,协程整合
iOS RAC / RxSwift Signal / Observable 热/冷信号、Cocoa 扩展
鸿蒙 ArkUI @State / @Observed 声明式 UI,装饰器驱动
React Hooks useState / useEffect 函数式、虚拟 DOM 驱动
Flutter Stream / ValueNotifier Stream / ChangeNotifier Dart 异步流、Listenable

二、核心概念共通点

2.1 观察者模式:统一的底层基石

所有响应式实现都以观察者模式为基础:有「被观察对象」和「观察者」,数据变化时通知观察者。

1
2
3
4
5
6
7
┌─────────────────┐         ┌──────────────────┐
│ 数据源/生产者 │ ──────► │ 观察者/消费者 │
│ (Observable等) │ 订阅 │ (Observer等) │
└─────────────────┘ └──────────────────┘
│ ▲
│ onNext / send / emit │
└────────────────────────────┘
平台 被观察者 观察者 订阅方式
Android RxJava Observable Observer subscribe()
Android Flow Flow Collector collect {}
iOS RAC Signal / SignalProducer Observer observe() / start()
React useState 返回值 组件 隐式(依赖 React 调度)
Flutter Stream / ValueNotifier StreamSubscription / addListener listen() / addListener()
鸿蒙 @State 变量 UI 组件 隐式(框架自动重绘)

2.2 可取消性(Disposable / 生命周期)

订阅通常会产生「可取消」的句柄,用于在合适时机释放资源,避免内存泄漏:

平台 取消句柄 典型用法
RxJava Disposable compositeDisposable.add()
Kotlin Flow Job / CoroutineScope viewModelScope.launch {}
RAC Disposable disposable.dispose()
React useEffect 清理函数 return () => cleanup()
Flutter StreamSubscription subscription.cancel()
鸿蒙 框架管理 组件销毁自动解绑

2.3 热流 vs 冷流(部分平台)

在 RxJava、RAC、Kotlin Flow 中,流有「热」「冷」之分:

类型 含义 典型场景
冷流 每次订阅才执行,每个订阅者独立收到完整数据 网络请求、文件读取
热流 无论是否订阅都会持续发送,多订阅共享同一流 按钮点击、通知、UI 事件

React / Flutter / 鸿蒙 的「状态」更接近热流:始终有一个当前值,变化时通知依赖方。


三、各平台实现原理浅析

3.1 Android:RxJava 与 Kotlin Flow

RxJava:基于 ReactiveX 规范,Observable 链式操作符,每次操作返回新 Observable,订阅时从上游向下游传递事件。

1
2
3
4
5
6
7
8
9
10
11
// RxJava 链式调用
Observable.create<String> { emitter ->
emitter.onNext("Hello")
emitter.onComplete()
}
.map { it.uppercase() }
.filter { it.length > 0 }
.subscribe(
{ println(it) },
{ it.printStackTrace() }
)

Kotlin Flow:冷流,基于协程,背压天然支持,与 StateFlow/SharedFlow 配合做 UI 状态和事件。

1
2
3
4
5
// Flow 冷流 + 操作符
flowOf(1, 2, 3)
.map { it * 2 }
.filter { it > 2 }
.collect { println(it) }

LiveData:轻量级、生命周期感知,主线程回调,适合简单 UI 状态,官方推荐新项目用 Flow 替代。

3.2 iOS:ReactiveCocoa / ReactiveSwift

Signal:热信号,由 pipe() 创建,observer 控制发送。

1
2
3
let (signal, observer) = Signal<String, Never>.pipe()
signal.observeValues { print($0) }
observer.send(value: "Hi")

SignalProducer:冷信号,每次 start 执行一次,适合异步任务。

1
2
3
4
5
let producer = SignalProducer<String, Never> { obs, _ in
obs.send(value: "Hello")
obs.sendCompleted()
}
producer.startWithValues { print($0) }

3.3 鸿蒙 ArkUI:装饰器驱动的状态

ArkUI 用装饰器声明「可观察」的状态,状态变化自动触发 UI 更新:

1
2
3
4
5
6
7
8
9
10
11
12
// @State:组件内部状态
@State count: number = 0

// @Prop:父→子单向
// @Link:双向
// @Provide / @Consume:跨层级

// @Observed + @ObjectLink:嵌套对象
@Observed
class User {
name: string
}

@ObservedV2 + @Trace(V2):支持深层属性观察,解决嵌套对象不可观察的问题。

3.4 React:状态与副作用

React 的响应式体现在「状态驱动 UI」和「副作用与依赖同步」:

1
2
3
4
5
6
7
8
9
10
11
const [keyword, setKeyword] = useState('');
const [users, setUsers] = useState([]);

// 依赖 keyword 变化,自动重新执行
useEffect(() => {
if (keyword.length < 3) return;
const timer = setTimeout(() => {
searchUsers(keyword).then(setUsers);
}, 300);
return () => clearTimeout(timer); // 清理
}, [keyword]);

状态变化 → 重新渲染 → 虚拟 DOM diff → 最小化 DOM 更新。

3.5 Flutter:Stream 与 Listenable

Stream:Dart 异步流,类似冷流,支持 map、where、asyncMap 等。

1
2
3
4
Stream.periodic(Duration(seconds: 1))
.map((i) => i * 2)
.take(5)
.listen((value) => print(value));

ValueNotifier / ChangeNotifier:同步、轻量,配合 ValueListenableBuilder 做局部重建:

1
2
3
4
5
final counter = ValueNotifier<int>(0);
ValueListenableBuilder<int>(
valueListenable: counter,
builder: (_, value, __) => Text('$value'),
)

四、操作符与组合逻辑的共性

4.1 转换类:map / filter

操作符 含义 Android iOS RAC Flutter React
map 值转换 map {} .map {} .map() 派生 state
filter 过滤 filter {} .filter {} .where() 条件 + early return

4.2 组合类:combineLatest / merge

多流组合在各平台均有对应能力:

场景 RxJava RAC Flutter React
多源最新值 combineLatest combineLatest RxDart combineLatest2 多个 useState + useEffect
多流合并 merge merge StreamGroup.merge 自定义逻辑

4.3 扁平化:flatMap / switchMap

将「每个值 → 新流」展开,常用于搜索联想、取消旧请求:

平台 操作符 行为
RxJava flatMap / switchMap switchMap 只保留最新内层流
RAC flatMap(.latest) 新关键词取消上次请求
Flutter asyncExpand 类似 flatMap
React useEffect + 清理 依赖变化时清理上次 effect

4.4 时间控制:debounce / throttle

防抖、节流在搜索、滚动等场景通用:

平台 防抖 节流
RxJava debounce() throttleFirst/throttleLatest
RAC debounce() throttle()
Flutter debounce from rxdart throttle from rxdart
React 自定义 setTimeout + cleanup useThrottle / lodash

五、源码层面的共通原理

5.1 链式结构与包装

操作符通常不修改原流,而是返回新的「包装流」,内部订阅上游并转换后传给下游:

1
2
3
4
5
6
7
8
9
Observable.map(f)

├─ 创建 MapObservable
│ │
│ ├─ source = 上游 Observable
│ └─ mapper = 转换函数 f

└─ subscribe 时:上游.subscribe(下游的包装 Observer)
下游 Observer 收到值时:onNext(mapper(value))

RAC 的 map 也是类似结构:Signal { observer in self.observe { ... observer.send(value: transform($0)) } }

5.2 订阅传播

订阅是「从下游往上游」建立,事件是「从上游往下游」传递:

1
2
3
4
5
6
subscribe(observer)
→ 下游 Observable 订阅其 source
→ 再往上游订阅
→ … 直到最顶层
→ 顶层开始 onNext/onComplete
→ 层层传递到最下游

5.3 取消传播

Disposable / Job 形成链:取消下游会向上传递,终止整条链的执行。


六、跨平台示例:搜索防抖 + 取消旧请求

6.1 Android (RxJava)

1
2
3
4
5
6
7
8
RxTextView.textChanges(searchEditText)
.map { it.toString() }
.filter { it.length >= 2 }
.debounce(300, TimeUnit.MILLISECONDS)
.switchMap { keyword -> api.searchUsers(keyword).toObservable() }
.observeOn(AndroidSchedulers.mainThread())
.subscribe({ users -> updateUI(users) }, { it.printStackTrace() })
.addTo(compositeDisposable)

6.2 iOS (RAC)

1
2
3
4
5
6
7
8
9
10
searchTextField.reactive.continuousTextValues
.filter { ($0 ?? "").count >= 2 }
.debounce(0.3, on: QueueScheduler.main)
.flatMap(.latest) { keyword in
searchAPI(keyword ?? "")
}
.observe(on: UIScheduler())
.observeValues { [weak self] users in
self?.updateSearchResults(users)
}

6.3 鸿蒙 (ArkUI)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@State keyword: string = ''
@State users: User[] = []
private timer: number = -1

onInputChange(value: string) {
this.keyword = value
clearTimeout(this.timer)
if (value.length < 2) return
this.timer = setTimeout(() => {
SearchAPI.searchUsers(value).then((users: User[]) => {
this.users = users
})
}, 300)
}

6.4 React

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const [keyword, setKeyword] = useState('');
const [users, setUsers] = useState([]);

useEffect(() => {
if (keyword.length < 2) return;
const timer = setTimeout(() => {
let cancelled = false;
searchUsers(keyword).then(data => {
if (!cancelled) setUsers(data);
});
return () => { cancelled = true; };
}, 300);
return () => clearTimeout(timer);
}, [keyword]);

6.5 Flutter

1
2
3
4
5
6
7
8
9
10
11
12
13
final _keyword = StreamController<String>.broadcast();
final _users = ValueNotifier<List<User>>([]);

_keyword.stream
.where((s) => s.length >= 2)
.transform(StreamTransformer.fromHandlers(
handleData: (data, sink) => Timer(Duration(milliseconds: 300), () => sink.add(data)),
))
.asyncMap((k) => searchAPI(k))
.listen((users) => _users.value = users);

// 输入时
_keyword.add(controller.text);

七、实际项目应用案例

7.1 登录表单校验(多字段组合)

需求:用户名 ≥3 字符、密码 ≥6 字符时,登录按钮才可点击。

平台 实现要点
RxJava combineLatest(username, password).map { u, p -> u.length>=3 && p.length>=6 }
RAC Signal.combineLatest(username.signal, password.signal).map { … }
React useMemo 派生 canLogin = user.length>=3 && pwd.length>=6
Flutter ValueNotifier 或 Stream,combineLatest2 派生
鸿蒙 @State user/pwd,computed 或 @Computed 派生 canLogin

7.2 多数据源合并展示

需求:本地缓存 + 网络接口合并去重后展示。

各平台通用思路:

  1. 本地流 + 远程流
  2. combineLatest / zip / merge 合并
  3. map 去重、排序
  4. 订阅结果更新 UI

7.3 列表下拉刷新 + 分页加载

需求:下拉刷新、上拉加载更多,防重复请求。

  • Subject / PublishSubject 表示下拉、触底事件
  • debounce 防抖
  • flatMap/switchMap 发起请求,合并到单一列表流
  • 错误重试、loading 状态管理

7.4 电商 App 购物车总价

需求:商品数量、价格变化时,实时计算总价。

  • 每个商品的数量、单价作为流/状态
  • combineLatest 合并所有项
  • map/reduce 计算总价
  • 单一订阅更新总价 UI

八、各平台选型建议

场景 Android iOS 鸿蒙 React Flutter
新项目 优先 Kotlin Flow RAC/RxSwift ArkUI 原生 Hooks Stream + ValueNotifier
复杂异步流 RxJava / Flow RAC 自定义 + Promise useEffect + 自定义 Hook rxdart
简单 UI 状态 StateFlow / LiveData @Published @State useState ValueNotifier
跨层级状态 ViewModel + StateFlow 单例 + Property @Provide/@Consume Context/Redux Provider/Riverpod

九、共同的最佳实践

  1. 生命周期绑定:订阅要在页面/组件销毁时取消,避免泄漏。
  2. 线程/调度:UI 更新必须在主线程/主 Isolate,注意 observeOn / UIScheduler
  3. 防抖与节流:输入、滚动等高频事件务必做时间控制。
  4. 取消旧请求:搜索、联想场景用 switchMap / flatMap(.latest) 或 useEffect 清理。
  5. 错误处理:onError / catch / retry 统一处理,避免吞掉异常。
  6. 弱引用:闭包中 [weak self] / WeakReference,防止循环引用。

十、总结

维度 共性
本质 观察者模式 + 数据流抽象
思想 声明式、数据驱动、自动传播
操作符 map、filter、combine、flatMap、debounce 等在各平台都有对应
取消 Disposable / 清理函数 / 生命周期绑定
热/冷 部分平台区分热流(共享)与冷流(按需执行)
应用 表单校验、搜索联想、多源合并、列表分页、实时计算

掌握这些共性后,从一个平台迁移到另一个平台时,可以快速找到对应概念和 API,写出一致、可维护的响应式代码。


参考资源

移动端与桌面端跨平台开发

由浅入深,从基本概念、原理与源码,到示例与实际项目应用,系统梳理移动端与桌面端跨平台技术体系


一、基本概念

1.1 什么是跨平台开发?

跨平台开发指用一套(或高度共享的)代码,同时支持多个操作系统或设备形态(如 iOS、Android、macOS、Windows、Linux、Web),从而降低开发与维护成本、加快迭代速度。

1
2
3
4
5
6
7
8
9
10
11
┌─────────────────────────────────────────────────────────────────────────┐
│ 跨平台开发的核心目标 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ 一套 / 共享代码 ──► 多端运行 ──► 降低人力、统一体验、快速发布 │
│ │
│ 移动端:iOS + Android(+ 鸿蒙 / 其他) │
│ 桌面端:macOS + Windows + Linux │
│ 大前端:Web + 移动 + 桌面(部分方案可三端复用) │
│ │
└─────────────────────────────────────────────────────────────────────────┘

1.2 移动端跨平台 vs 桌面端跨平台

维度 移动端跨平台 桌面端跨平台
目标平台 iOS、Android、鸿蒙等 Windows、macOS、Linux
交互特点 触屏、手势、传感器、多分辨率 键鼠、窗口、菜单、多显示器
分发方式 App Store、应用市场、企业内部分发 安装包、商店、便携版、包管理器
性能关注 电量、内存、启动速度、流畅度 内存、CPU、GPU、安装体积
典型方案 Flutter、React Native、Kotlin Multiplatform Electron、Tauri、Flutter Desktop、Qt

1.3 为什么需要跨平台?

痛点 说明 跨平台带来的价值
多套原生实现 iOS (Swift/ObjC)、Android (Kotlin/Java) 各写一套 一套业务逻辑 + UI,多端复用
人力与节奏 双端/三端并行,需求同步、发版协调成本高 统一技术栈,一次开发多端发布
体验一致性 各端实现细节不同,交互与视觉易分裂 可控的 UI 与交互一致性(尤其自绘方案)
桌面端同样分裂 Win/Mac/Linux 三套原生或 Web 各搞一套 一套代码覆盖主流桌面系统

二、跨平台的核心原理

2.1 三种主流渲染思路

跨平台方案按「谁来画 UI」可分为三类:

1
2
3
4
5
6
7
8
9
10
11
12
┌────────────────────────────────────────────────────────────────────────────┐
│ 方式 │ 谁负责渲染? │ 代表技术 │
├────────────────────────────────────────────────────────────────────────────┤
│ 原生控件映射 │ 使用系统原生控件 │ React Native、Xamarin、Compose │
│ (Native Mapping) │ 通过桥接更新属性 │ Multiplatform │
├────────────────────────────────────────────────────────────────────────────┤
│ Web 容器 │ 内嵌 WebView/浏览器 │ Cordova、Capacitor、Electron │
│ (WebView/Chromium)│ 用 HTML/CSS/JS 画 │ (Electron 用 Chromium 做桌面壳) │
├────────────────────────────────────────────────────────────────────────────┤
│ 自绘 (Skia/Impeller)│ 自己画像素/矢量 │ Flutter、Qt、.NET MAUI 部分 │
│ (Self-draw) │ 不依赖系统控件 │ │
└────────────────────────────────────────────────────────────────────────────┘
  • 原生控件映射:体验最接近系统原生,但受限于各端控件能力与差异,需要处理平台差异。
  • Web 容器:开发效率高、生态成熟,桌面端 Electron 安装体积与内存占用较大。
  • 自绘:UI 一致性强、不依赖系统控件,可精细控制渲染;需要自己实现可访问性、输入法等。

2.2 架构共性:桥接与运行时

无论哪种方式,跨平台层都要和「宿主平台」通信:

1
2
3
4
5
6
7
8
9
10
11
12
13
┌─────────────────────────────────────────────────────────────────────────┐
│ 跨平台层(业务 + UI 描述) │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ Dart / JavaScript / C# / Kotlin 等 │ │
│ │ 组件树 / 虚拟 DOM / 声明式 UI │ │
│ └────────────────────────────┬────────────────────────────────────┘ │
│ │ 桥接层 (Bridge / Channel / FFI) │
├───────────────────────────────┼─────────────────────────────────────────┤
│ 宿主 / 原生层 ▼ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ Native (Swift/Kotlin/C++/Rust) + 系统 API + 原生控件/自绘引擎 │ │
│ └─────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────┘
  • 移动端:通常通过 Bridge / Channel(如 React Native 的 Bridge、Flutter 的 Platform Channel)做异步通信。
  • 桌面端:Electron 是 主进程 + 渲染进程 的 IPC;Tauri 是 Web 前端 + Rust 核心 的 IPC/FFI,体积更小。

2.3 技术选型简表

方案 类型 移动端 桌面端 语言/技术 特点摘要
Flutter 自绘 ✅ iOS/Android/鸿蒙 ✅ Win/Mac/Linux Dart + Skia/Impeller 一致性强、性能好、桌面逐渐成熟
React Native 原生映射 ✅ iOS/Android ❌ 需另选桌面方案 JS/TS + 原生桥 生态大、热更新友好
Electron Web 容器 ✅ Win/Mac/Linux HTML/CSS/JS + Node + Chromium 桌面占主导、包体大
Tauri Web + 原生壳 实验性 ✅ Win/Mac/Linux 前端任意 + Rust 轻量、安全、系统集成好
Kotlin Multiplatform 原生映射/共享逻辑 ✅ 主打 可共享逻辑 Kotlin 逻辑共享、UI 仍多端各自实现
Capacitor / Cordova WebView 可做桌面壳 Web 技术 用 Web 开发,打包成 App

三、代表性框架原理与源码要点

3.1 Flutter:自绘引擎与 Dart 层

3.1.1 整体架构

Flutter 的 UI 不依赖系统控件,由 Skia(或 Impeller on iOS)在画布上自绘,因此各端视觉与行为高度一致。

1
2
3
4
5
6
7
8
9
10
11
12
13
┌──────────────────────────────────────────────────────────────────┐
│ Dart 层 │
│ Widget 树 → Element 树 → RenderObject 树(布局与绘制) │
└────────────────────────────┬─────────────────────────────────────┘
│ Dart VM / AOT 编译
┌────────────────────────────▼─────────────────────────────────────┐
│ Flutter Engine (C++) │
│ Layer 树 → Scene → Skia/Impeller 绘制 → 显示 │
└────────────────────────────┬─────────────────────────────────────┘
│ 平台嵌入层 (Embedder)
┌────────────────────────────▼─────────────────────────────────────┐
│ Android (SurfaceView) / iOS (Metal) / Windows (ANGLE) / ... │
└──────────────────────────────────────────────────────────────────┘

3.1.2 关键源码位置(概念性)

  • Widget → Element → RenderObjectflutter/lib/src/widgets/framework.dartrendering/object.dart
    Element 持有 RenderObjectRenderObject 负责 layoutpaint,最终生成 Layer 提交给引擎。
  • 绘制入口RenderViewcompositeFrame()Layer 树提交给 Window.render(),引擎再交给 Skia/Impeller。
  • 平台通道PlatformChannelflutter/lib/src/services/platform_channel.dart,Dart 与原生通过 BinaryMessenger 收发序列化消息。

理解「Widget 不可变 → Element 挂载/更新 → RenderObject 布局绘制」这条链路,就抓住了 Flutter UI 的核心。

3.2 React Native:桥接与原生组件

3.2.1 新架构(Fabric + TurboModules)与旧桥

  • 旧架构:JS 与原生通过 Bridge 异步传 JSON,原生侧用 UIManager 把「虚拟节点」转成原生 View。
  • 新架构
    • Fabric:C++ 的渲染器,Shadow 树在 C++ 中,减少跨桥次数,支持同步布局与优先级。
    • TurboModules:按需加载的 JSI 原生模块,可同步调用,不再全部在启动时塞进 Bridge。
1
2
3
4
5
6
7
8
9
┌─────────────┐     JSI (JavaScript Interface)      ┌─────────────────┐
│ JavaScript │ ◄──── 同步/异步调用 ─────────────► │ C++ Fabric / │
│ React 组件 │ (新架构) │ TurboModules │
└─────────────┘ └────────┬────────┘

┌─────────────┐ Bridge (旧) / 序列化消息 ┌───────▼───────┐
│ Metro 打包 │ ◄──────────────────────────────► │ Native Views │
│ JS Bundle │ │ (Android/iOS)│
└─────────────┘ └───────────────┘

3.2.2 源码可读入口(概念性)

  • React 组件到原生:新架构下 ReactFabricFabricUIManager 等(C++),旧架构下 UIManagerModule(Java/ObjC)把「节点树」转成原生 View。
  • 通信NativeModulesNativeEventEmitter 对应到原生模块与事件发送,新架构下通过 JSI 直接调 C++ 再调原生。

3.3 Electron:多进程与 IPC

3.3.1 主进程 + 渲染进程

Electron 基于 Chromium,每个窗口是一个渲染进程,主进程负责生命周期、系统 API、原生菜单等。

1
2
3
4
5
6
7
8
9
10
11
┌─────────────────────────────────────────────────────────────────────────┐
│ Main Process (Node.js) │
│ BrowserWindow、app、Menu、系统 API、原生模块 (.node) │
└────────────────────────────┬────────────────────────────────────────────┘
│ IPC (ipcMain / ipcRenderer)
│ contextBridge.exposeInMainWorld 安全暴露 API
└────────────────────────────┬────────────────────────────────────────────┘
┌────────────────────────────▼─────────────────────────────────────────────┐
│ Renderer Process (Chromium) │
│ HTML / CSS / JS,类似前端 SPA;可禁用 Node 仅用 preload 暴露能力 │
└─────────────────────────────────────────────────────────────────────────┘

3.3.2 安全与 contextBridge

  • 不要在渲染进程直接开 nodeIntegration: true 并把整个 Node 暴露给页面。
  • 推荐:用 contextBridge.exposeInMainWorld 在 preload 里暴露有限 API,主进程通过 ipcMain 处理,渲染进程只调这些 API。

源码层面:lib/renderer/api/context-bridge.tslib/main/api/ipc-main.ts 等,理解「preload 注入 → contextBridge → ipcRenderer.invoke」这条链路即可。

3.4 Tauri:Rust 核心 + 前端

3.4.1 轻量从何而来

Tauri 不内嵌完整 Chromium,而是用 系统 WebView(Windows: WebView2,macOS: WKWebView,Linux: WebKitGTK),所以安装包小、内存占用低。

1
2
3
4
5
6
7
8
9
┌─────────────────────────────────────────────────────────────────────────┐
│ Frontend (任意: React / Vue / Svelte / 纯 HTML) │
│ 运行在系统 WebView 中 │
└────────────────────────────┬────────────────────────────────────────────┘
│ Tauri API (invoke / event)
┌────────────────────────────▼─────────────────────────────────────────────┐
│ Tauri Core (Rust) │
│ 窗口、菜单、系统托盘、文件与 shell、插件;可调用任意 Rust 与系统 API │
└─────────────────────────────────────────────────────────────────────────┘

3.4.2 命令与安全

  • 前端通过 invoke('command_name', { ... }) 调用 Rust 端在 #[tauri::command] 里定义的函数。
  • 权限与能力在 tauri.conf.jsoncapabilities 中声明,避免前端随意调敏感 API。
    源码可看 tauri/src/manager.rstauri/src/app.rs 以及命令派发与 IPC 的实现。

四、示例代码

4.1 Flutter:一个跨移动端 + 桌面的页面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
// 同一套代码可跑在 iOS、Android、Windows、macOS、Linux
import 'package:flutter/material.dart';

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

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

@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Cross-Platform Demo',
theme: ThemeData(primarySwatch: Colors.blue),
home: const HomePage(),
);
}
}

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

@override
State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
int _counter = 0;

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Flutter 跨平台')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('点击次数: $_counter', style: Theme.of(context).textTheme.headlineMedium),
const SizedBox(height: 24),
FilledButton.icon(
onPressed: () => setState(() => _counter++),
icon: const Icon(Icons.add),
label: const Text('增加'),
),
],
),
),
);
}
}
  • 移动端:flutter run 选 iOS/Android 设备即可。
  • 桌面端:flutter run -d windows / macos / linux(需先 flutter config --enable-*desktop)。

4.2 React Native:一个带原生能力的组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 跨 iOS + Android,调用原生模块示例
import { NativeModules, Platform, StyleSheet, Text, View, Button } from 'react-native';

const { MyNativeModule } = NativeModules; // 需在原生侧实现 MyNativeModule

export default function App() {
const [result, setResult] = React.useState('');

const callNative = async () => {
try {
const value = await MyNativeModule?.getDeviceId?.() ?? '未实现';
setResult(value);
} catch (e) {
setResult(String(e));
}
};

return (
<View style={styles.container}>
<Text style={styles.text}>平台: {Platform.OS}</Text>
<Button title="调用原生 getDeviceId" onPress={callNative} />
<Text style={styles.result}>{result}</Text>
</View>
);
}

const styles = StyleSheet.create({
container: { flex: 1, justifyContent: 'center', alignItems: 'center', padding: 20 },
text: { fontSize: 16, marginBottom: 12 },
result: { marginTop: 12, color: '#333' },
});

4.3 Electron:主进程与渲染进程通信

1
2
3
4
5
6
7
// preload.js(运行在隔离上下文中,通过 contextBridge 暴露)
const { contextBridge, ipcRenderer } = require('electron');

contextBridge.exposeInMainWorld('electronAPI', {
getAppVersion: () => ipcRenderer.invoke('get-app-version'),
openFolder: (path) => ipcRenderer.invoke('dialog:openFolder', path),
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// main.js(主进程)
const { app, BrowserWindow, ipcMain, dialog } = require('electron');
const path = require('path');

function createWindow() {
const win = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
nodeIntegration: false,
contextIsolation: true,
},
});
win.loadFile('index.html');
}

app.whenReady().then(() => {
ipcMain.handle('get-app-version', () => app.getVersion());
ipcMain.handle('dialog:openFolder', (_, dir) =>
dialog.showOpenDialog({ properties: ['openDirectory'] })
);
createWindow();
});
1
2
3
4
5
6
7
8
9
<!-- 渲染进程 (index.html 里的脚本) -->
<button id="version">获取版本</button>
<div id="out"></div>
<script>
document.getElementById('version').onclick = async () => {
const v = await window.electronAPI.getAppVersion();
document.getElementById('out').textContent = 'Version: ' + v;
};
</script>

4.4 Tauri:前端调用 Rust 命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// src-tauri/src/main.rs
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]

#[tauri::command]
fn greet(name: &str) -> String {
format!("Hello, {}! (from Rust)", name)
}

fn main() {
tauri::Builder::default()
.invoke_handler(tauri::generate_handler![greet])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 前端 (如 React)
import { invoke } from '@tauri-apps/api/core';

function App() {
const [msg, setMsg] = useState('');

const sayHello = async () => {
const result = await invoke('greet', { name: 'Tauri' });
setMsg(result);
};

return (
<div>
<button onClick={sayHello}>Say Hello</button>
<p>{msg}</p>
</div>
);
}

五、实际项目中的应用案例

5.1 移动端跨平台

产品/项目 方案 说明
阿里巴巴闲鱼 Flutter 部分核心页面用 Flutter,与原生混编,统一商品与互动体验。
腾讯微信 多端自研 + 部分 RN 核心为原生,部分业务用自研或 RN 类方案做动态化与跨端。
字节抖音 / 飞书 Flutter / 自研 部分模块 Flutter,桌面端 Electron(如飞书)。
美团 React Native 部分 App 内页用 RN,热更新与双端复用。
Reflectly Flutter 日记/习惯类 App,全 Flutter,iOS/Android 一套 UI。
BMW 车载 Flutter 车载中控 UI 使用 Flutter 做多车型统一界面。

5.2 桌面端跨平台

产品/项目 方案 说明
VS Code Electron 编辑器 UI + 扩展用 Web 技术,主进程 Node,跨 Win/Mac/Linux。
Slack / Discord Electron 桌面客户端统一用 Web 栈,一套代码多端。
Figma Desktop Electron 设计工具桌面版,与 Web 共享核心逻辑与渲染。
1Password 8 Electron 密码管理桌面端,跨平台一致体验。
Clash Verge / 部分工具 Tauri 需要小体积、低内存的桌面工具,用 Tauri 替代 Electron。
Appflowy Flutter 桌面端用 Flutter,与移动端共享部分 UI 与逻辑。

5.3 移动 + 桌面 统一

产品/项目 移动端 桌面端 说明
Flutter 官方 Flutter (iOS/Android) Flutter (Win/Mac/Linux) 同一套 Dart 代码,多端运行,适合重 UI 一致性的产品。
飞书 原生 + 混合 Electron 移动端以原生为主,桌面端 Electron,部分逻辑与 UI 复用。
Notion 原生 / WebView Electron 桌面 Electron,移动端混合,内容与逻辑复用。

5.4 选型时可参考的维度

  • 团队技能:前端强可选 RN/Electron/Tauri;能接受 Dart 可选 Flutter 全平台。
  • 体验要求:要极致接近系统原生 → 原生映射(RN)或原生开发;要强一致性、可控渲染 → Flutter/自绘。
  • 桌面包体与内存:对体积和内存敏感 → Tauri 或 Flutter Desktop;优先生态与成熟度 → Electron。
  • 热更新与合规:移动端需热更新且符合商店政策时,需评估各方案的热更与审核风险。
  • 已有资产:已有 Web 或 React 技术栈,可优先 RN/Electron/Capacitor;已有 Rust/系统开发,可考虑 Tauri。

六、小结

  • 概念:跨平台开发用一套(或共享)代码覆盖多端,降低成本、统一体验;移动端与桌面端在平台特性、交互、分发上不同,但「桥接 + 运行时」的架构思想相通。
  • 原理:三种主要思路——原生控件映射、Web 容器、自绘;理解各方案的渲染模型与桥接方式,有助于选型和排坑。
  • 源码:Flutter 的 Widget/Element/RenderObject 与引擎、RN 的 Fabric/TurboModules、Electron 的 IPC 与 contextBridge、Tauri 的 Rust 命令与系统 WebView,是深入时的好入口。
  • 示例:同一套 Flutter 可跑移动+桌面;RN 通过 NativeModules 调原生;Electron 用 preload + contextBridge + ipcMain;Tauri 用 invoke 调 Rust 命令。
  • 实战:大量商业产品已在移动端(Flutter/RN)和桌面端(Electron/Tauri/Flutter)落地,选型时结合团队、体验、体积、热更新与既有技术栈综合权衡。

掌握「概念 → 原理 → 源码入口 → 示例 → 案例」这条线,就能在移动端与桌面端跨平台开发中建立清晰的技术图景,并做出更合适的架构与选型决策。

React / Flutter 状态管理

由浅入深,从基本概念到原理与源码,再到示例与实际项目应用案例,系统梳理两大主流框架中的状态管理方案


一、状态管理基础概念

1.1 什么是状态(State)?

状态是驱动 UI 变化的数据。当状态改变时,界面随之更新,形成「数据驱动视图」的声明式模式。

1
2
3
4
状态 ──► 视图
▲ │
│ ▼
└── 用户交互 / 网络请求 / 定时器等

1.2 状态的分类

类型 作用域 典型场景 生命周期
本地状态 单组件 输入框内容、展开/折叠、选中项 跟随组件
共享状态 多组件 用户信息、主题、购物车 需要提升或全局管理
服务端状态 与后端同步 API 数据、缓存 异步、需缓存策略

1.3 为什么需要状态管理?

随着应用复杂度上升,会出现:

  • 状态提升导致 props 层层传递(prop drilling)
  • 状态分散导致难以追踪和调试
  • 重复请求缓存失效等数据一致性问题

状态管理方案的目标:集中、可预测、易维护


二、React 状态管理

2.1 内置方案概览

方案 适用场景 特点
useState 本地状态 简单、轻量
useReducer 复杂本地状态 可预测、易测试
Context API 跨层级共享 官方内置、易造成不必要的重渲染

2.2 useState:最简单的本地状态

1
2
3
4
5
6
7
8
9
10
11
12
function Counter() {
const [count, setCount] = useState(0);

return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>+1</button>
{/* 函数式更新,避免闭包陷阱 */}
<button onClick={() => setCount(prev => prev + 1)}>+1 (安全)</button>
</div>
);
}

惰性初始化:初始值可以是函数,仅在首次渲染执行。

1
const [state, setState] = useState(() => expensiveComputation());

2.3 useReducer:复杂状态的 reducer 模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function reducer(state, action) {
switch (action.type) {
case 'increment':
return { count: state.count + 1 };
case 'decrement':
return { count: state.count - 1 };
default:
return state;
}
}

function Counter() {
const [state, dispatch] = useReducer(reducer, { count: 0 });

return (
<div>
<span>{state.count}</span>
<button onClick={() => dispatch({ type: 'increment' })}>+</button>
<button onClick={() => dispatch({ type: 'decrement' })}>-</button>
</div>
);
}

2.4 Context API:跨层级共享状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const ThemeContext = createContext('light');

function App() {
const [theme, setTheme] = useState('light');
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
<Page />
</ThemeContext.Provider>
);
}

function Page() {
const { theme } = useContext(ThemeContext);
return <div className={theme}>...</div>;
}

注意:Provider 的 value 变化会导致所有 useContext 的消费者重渲染,需配合 useMemo 或拆分 Context 优化。

2.5 Redux / Redux Toolkit:全局状态管理

Redux 采用单向数据流View → Action → Reducer → Store → View

1
2
3
4
5
┌─────────┐   dispatch    ┌─────────┐   reduce    ┌────────┐
│ View │ ───────────► │ Action │ ─────────► │ Store │
└─────────┘ └─────────┘ └────────┘
▲ │
└──────────────── subscribe ─────────────────────┘

Redux Toolkit 示例(官方推荐写法):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// store/counterSlice.js
import { createSlice } from '@reduxjs/toolkit';

const counterSlice = createSlice({
name: 'counter',
initialState: { value: 0 },
reducers: {
increment: state => { state.value += 1; },
decrement: state => { state.value -= 1; },
incrementByAmount: (state, action) => {
state.value += action.payload;
},
},
});

export const { increment, decrement, incrementByAmount } = counterSlice.actions;
export default counterSlice.reducer;
1
2
3
4
5
6
7
8
9
10
// 组件中使用
import { useDispatch, useSelector } from 'react-redux';
import { increment } from './store/counterSlice';

function Counter() {
const count = useSelector(state => state.counter.value);
const dispatch = useDispatch();

return <button onClick={() => dispatch(increment())}>{count}</button>;
}

2.6 Zustand:轻量级全局状态

Zustand 基于 Hooks,API 简洁,无 Provider 包裹。

1
2
3
4
5
6
7
8
9
10
11
12
import { create } from 'zustand';

const useStore = create((set) => ({
count: 0,
increment: () => set(state => ({ count: state.count + 1 })),
decrement: () => set(state => ({ count: state.count - 1 })),
}));

function Counter() {
const { count, increment } = useStore();
return <button onClick={increment}>{count}</button>;
}

选择器优化:只订阅需要的字段,避免无关更新。

1
const count = useStore(state => state.count); // 仅 count 变化时重渲染

2.7 React 状态管理原理浅析

useState 的链表结构

React 内部用链表存储 Hooks。每个 Hook 对应链表中的一个节点,通过 FibermemoizedState 串联。

1
2
3
Fiber.memoizedState → Hook1 → Hook2 → Hook3 → ...

[state, setState]

这就是为什么 Hooks 必须在顶层调用、不能放在条件/循环中:链表顺序必须稳定。

setState 的批处理(Batching)

React 18 默认对所有更新进行自动批处理,多次 setState 会合并为一次渲染。

1
2
3
4
5
function handleClick() {
setCount(c => c + 1);
setFlag(f => !f);
// 仅触发一次重渲染
}

三、Flutter 状态管理

3.1 方案概览

方案 官方/社区 适用场景 特点
setState 内置 本地状态 简单,整组件重建
InheritedWidget 内置 跨层级共享 底层基础,一般不直接使用
Provider 官方推荐 中小型应用 基于 InheritedWidget,易上手
Riverpod 社区主流 中大型应用 编译期安全、可测试、无 context
Bloc 社区 复杂业务逻辑 事件驱动、可预测、适合团队
GetX 社区 快速开发 全能型,状态+路由+依赖注入

3.2 setState:最简单的本地状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Counter extends StatefulWidget {
@override
_CounterState createState() => _CounterState();
}

class _CounterState extends State<Counter> {
int _count = 0;

void _increment() {
setState(() {
_count++;
});
}

@override
Widget build(BuildContext context) {
return Column(
children: [
Text('$_count'),
ElevatedButton(onPressed: _increment, child: Text('+1')),
],
);
}
}

原理setState 会标记当前 Element 为脏,在下一帧触发 build 重建子树。

3.3 Provider:官方推荐方案

Provider 基于 InheritedWidget,通过 context.watch<T>() 监听变化并重建。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 1. 定义 Model(继承 ChangeNotifier)
class CounterModel extends ChangeNotifier {
int _count = 0;
int get count => _count;

void increment() {
_count++;
notifyListeners(); // 通知监听者
}
}

// 2. 在根节点提供
void main() {
runApp(
ChangeNotifierProvider(
create: (_) => CounterModel(),
child: MyApp(),
),
);
}

// 3. 在子组件使用
class CounterPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
final counter = context.watch<CounterModel>();
return Text('${counter.count}');
}
}

多种 Provider 类型

类型 用途
Provider 不可变值
ChangeNotifierProvider 可变、需 notifyListeners
FutureProvider 异步数据
StreamProvider 流数据
MultiProvider 组合多个 Provider

3.4 Riverpod:下一代状态管理

Riverpod 无 BuildContext 依赖,支持编译期类型安全、易于测试和复用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 1. 定义 Provider
final counterProvider = StateNotifierProvider<CounterNotifier, int>((ref) {
return CounterNotifier();
});

class CounterNotifier extends StateNotifier<int> {
CounterNotifier() : super(0);
void increment() => state++;
}

// 2. 在 runApp 外包一层 ProviderScope
void main() {
runApp(ProviderScope(child: MyApp()));
}

// 3. 在组件中使用(无需 context)
class CounterPage extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final count = ref.watch(counterProvider);
return ElevatedButton(
onPressed: () => ref.read(counterProvider.notifier).increment(),
child: Text('$count'),
);
}
}

ref 的三大方法

方法 作用
ref.watch() 监听变化,值变化时重建
ref.read() 一次性读取,不监听
ref.listen() 监听变化并执行副作用,不重建

3.5 Bloc:事件驱动架构

Bloc 将 UI 与业务逻辑解耦,通过 Event → Bloc → State 的流程管理状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 1. 定义 Event 和 State
abstract class CounterEvent {}
class Increment extends CounterEvent {}
class Decrement extends CounterEvent {}

class CounterState {
final int count;
CounterState(this.count);
}

// 2. 实现 Bloc
class CounterBloc extends Bloc<CounterEvent, CounterState> {
CounterBloc() : super(CounterState(0)) {
on<Increment>((event, emit) => emit(CounterState(state.count + 1)));
on<Decrement>((event, emit) => emit(CounterState(state.count - 1)));
}
}

// 3. 在 UI 中使用
BlocProvider(
create: (_) => CounterBloc(),
child: BlocBuilder<CounterBloc, CounterState>(
builder: (context, state) {
return Text('${state.count}');
},
),
)

3.6 Flutter 状态管理原理浅析

setState 与 Element 树

1
2
3
4
5
6
7
8
9
10
setState() 被调用


标记 Element 为 dirty


下一帧 SchedulerBinding 触发 build


Element.rebuild() → State.build()

InheritedWidget 与依赖收集

InheritedWidget 通过 context.dependOnInheritedWidgetOfExactType<T>() 建立「依赖关系」。当 InheritedWidget 更新时,依赖它的 Element 会被标记为脏并重建。

Provider 的 notifyListeners() 会触发 InheritedWidget 的更新,从而通知所有 context.watch 的消费者。


四、React vs Flutter 状态管理对比

4.1 概念映射

概念 React Flutter
本地状态 useState setState
复杂本地状态 useReducer 自建 StatefulWidget + 内部逻辑
跨层级共享 Context InheritedWidget / Provider
全局 Store Redux / Zustand Provider / Riverpod / Bloc
选择器/按需订阅 useSelector / useStore(selector) context.select / ref.watch(provider.select())

4.2 设计哲学差异

维度 React Flutter
更新粒度 组件级,虚拟 DOM diff Widget 树重建,Element 复用
数据流 单向(Redux)或自由(Zustand) 多为单向,Bloc 强调事件流
依赖注入 通过 props / Context 通过 context / ref(Riverpod)
服务端状态 React Query / SWR 等 Riverpod 的 FutureProvider、flutter_bloc 等

五、源码层面的理解

5.1 React useState 的调度

React 的 setState 会调用 dispatchSetState,将更新放入 updateQueue,由调度器(Scheduler)在合适的时机批量处理,触发 rendercommit

1
2
3
4
5
6
// 简化流程
setState(newState)
enqueueUpdate(fiber, update)
scheduleUpdateOnFiber(fiber)
→ performConcurrentWorkOnRoot / performSyncWorkOnRoot
→ commitRoot

5.2 Flutter ChangeNotifier 与 Listenable

ChangeNotifier 继承 Listenable,内部维护 _listeners 列表。notifyListeners() 遍历并调用所有监听者。

1
2
3
4
5
6
// 简化逻辑
void notifyListeners() {
for (final listener in _listeners) {
listener(); // 触发 Consumer 等重建
}
}

ProviderInheritedProvideraddListenerChangeNotifier,当 notifyListeners 被调用时,触发自身 updateShouldNotify 并重建子树。


六、实际项目应用案例

6.1 案例一:电商购物车(React + Zustand)

需求:跨页面购物车数量、增删改、持久化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// store/cartStore.js
import { create } from 'zustand';
import { persist } from 'zustand/middleware';

export const useCartStore = create(
persist(
(set) => ({
items: [],
addItem: (product, qty = 1) =>
set((state) => ({
items: state.items.some((i) => i.id === product.id)
? state.items.map((i) =>
i.id === product.id ? { ...i, qty: i.qty + qty } : i
)
: [...state.items, { ...product, qty }],
})),
removeItem: (id) =>
set((state) => ({ items: state.items.filter((i) => i.id !== id) })),
totalCount: (state) => state.items.reduce((sum, i) => sum + i.qty, 0),
}),
{ name: 'cart-storage' }
)
);

// Header 中只订阅 totalCount,避免整 store 变化导致重渲染
const totalCount = useCartStore((s) =>
s.items.reduce((sum, i) => sum + i.qty, 0)
);

6.2 案例二:用户认证流(Flutter + Riverpod)

需求:登录态、token 刷新、路由守卫。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// providers/auth_provider.dart
final authStateProvider = StateNotifierProvider<AuthNotifier, AsyncValue<User?>>((ref) {
return AuthNotifier(ref);
});

class AuthNotifier extends StateNotifier<AsyncValue<User?>> {
AuthNotifier(this.ref) : super(const AsyncValue.loading()) {
_init();
}
final Ref ref;

Future<void> _init() async {
final token = await storage.getToken();
if (token == null) {
state = const AsyncValue.data(null);
return;
}
state = const AsyncValue.loading();
state = await AsyncValue.guard(() => api.getCurrentUser());
}

Future<void> login(String email, String pwd) async {
state = const AsyncValue.loading();
state = await AsyncValue.guard(() => api.login(email, pwd));
}

void logout() {
storage.clearToken();
state = const AsyncValue.data(null);
}
}

// 路由守卫:根据 authState 跳转登录页或首页
ref.listen(authStateProvider, (prev, next) {
next.whenData((user) {
if (user == null) navigator.pushReplacement(LoginRoute());
});
});

6.3 案例三:列表筛选与分页(React + Redux Toolkit + RTK Query)

需求:筛选条件、分页、缓存、乐观更新。

1
2
3
4
5
6
7
8
// 使用 RTK Query 管理服务端状态
const { data, isLoading, refetch } = useGetProductsQuery({
page: currentPage,
category: selectedCategory,
});

// 本地筛选状态用 Redux 或 useState 均可
const [filters, setFilters] = useState({ category: '', sort: 'default' });

6.4 案例四:主题与多语言(Flutter + Provider)

需求:亮/暗主题、中英文切换,全局生效。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 使用 MultiProvider 组合
runApp(
MultiProvider(
providers: [
ChangeNotifierProvider(create: (_) => ThemeModel()),
ChangeNotifierProvider(create: (_) => LocaleModel()),
],
child: MyApp(),
),
);

// 任意子组件
final theme = context.watch<ThemeModel>();
final locale = context.watch<LocaleModel>();

七、选型建议

场景 React 推荐 Flutter 推荐
小项目/原型 useState + Context setState + Provider
中大型项目 Redux Toolkit / Zustand Riverpod / Bloc
强类型、可测试 Redux + TypeScript / Zustand Riverpod
复杂业务流、事件驱动 Redux / XState Bloc
服务端状态 React Query / SWR Riverpod FutureProvider / dio + 自封装

八、总结

  • React:从 useState 起步,全局状态优先考虑 Redux ToolkitZustand,服务端状态用 React Query 等。
  • Flutter:从 setState 起步,共享状态用 Provider 入门,进阶用 RiverpodBloc
  • 选型时关注:团队熟悉度项目规模可测试性与框架生态的契合度

由浅入深掌握上述方案后,可以根据具体业务灵活组合,构建可维护、可扩展的状态管理体系。