📌 知识体系思维导图

一、Vue 到 Flutter:思维模式转换
1.1 核心概念对比表
| Vue 概念 | Flutter Provider 概念 | 关键差异 |
|---|---|---|
| Vuex Store | ChangeNotifier 模型类 |
Vuex 是单例全局存储,Provider 可以创建多个独立作用域 |
| State | 模型类中的数据字段 | Flutter 中状态是显式的类属性,需要手动通知更新 |
| Mutations | 模型类中的方法 + notifyListeners() |
Flutter 没有严格的 mutations 约束,任何方法都可修改状态 |
| Actions | 模型类中的异步方法 | 概念类似,但不需要特殊定义 |
| Getters | 模型类中的 getter 方法 | 完全相同的工作原理 |
| mapState | Consumer / Selector |
都是订阅状态变化的机制 |
| this.$store | Provider.of(context) |
Vue 通过 this 访问,Flutter 通过 BuildContext 访问 |
1.2 关键架构差异
Vue 响应式系统:
- 自动依赖追踪
- 虚拟 DOM diff 更新
- 组件内可直接修改状态(非严格模式)
Flutter Provider 系统:
- 手动声明依赖关系
- Widget 树重建(非虚拟 DOM)
- 不可变 Widget 树(核心差异!)
- 必须通过
notifyListeners()显式通知
二、Flutter Provider 核心机制详解
2.1 不可变 Widget 树与重建机制
这是 Vue 开发者最需要理解的 Flutter 核心理念:
dart
// Flutter Widget 是不可变的,不能这样写
class MyWidget extends StatelessWidget {
String text = "初始值"; // 这个值改变后,Widget 不会重建
@override
Widget build(BuildContext context) {
return Text(text);
}
}
// 正确做法:通过状态管理重建整个 Widget
class MyWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
// 每次 build 都从 Provider 获取最新值
final text = Provider.of<MyModel>(context).text;
return Text(text);
}
}
理解重点:
- 每次状态变化 →
notifyListeners()被调用 - 依赖该状态的 Widget 的
build()方法重新执行 - Flutter 比较新旧 Widget 树差异,只更新变化的部分
2.2 BuildContext:Flutter 的"依赖注入容器"
类比 Vue 中的 this,但功能更强大:
dart
// Vue 方式:通过 this 访问全局 store
methods: {
addItem() {
this.$store.commit('ADD_ITEM', newItem)
}
}
// Flutter 方式:通过 context 访问 Provider
void addItem(BuildContext context) {
// listen: false 表示"我不需要监听变化,只是读取/修改"
final model = Provider.of<ShoppingModel>(context, listen: false);
model.addItem(newItem);
// 注意:没有类似 Vuex 的 commit 约束,直接调用方法
}
三、完整开发流程示例(购物清单应用)
3.1 第一步:创建数据模型(类比 Vuex Module)
dart
// lib/models/shopping_item.dart
class ShoppingItem {
final String id;
final String name;
int quantity;
final double price;
bool isCompleted;
final String category;
final DateTime createdAt;
ShoppingItem({
required this.name,
this.quantity = 1,
required this.price,
this.isCompleted = false,
required this.category,
}) : id = DateTime.now().millisecondsSinceEpoch.toString(),
createdAt = DateTime.now();
// 类似 Vue 中的计算属性
double get totalPrice => quantity * price;
// 类似 Vue 中的方法,用于创建新实例(不可变性)
ShoppingItem copyWith({
String? name,
int? quantity,
double? price,
bool? isCompleted,
String? category,
}) {
return ShoppingItem(
name: name ?? this.name,
quantity: quantity ?? this.quantity,
price: price ?? this.price,
isCompleted: isCompleted ?? this.isCompleted,
category: category ?? this.category,
);
}
}
// lib/models/shopping_list_model.dart
import 'package:flutter/material.dart';
import 'shopping_item.dart';
// 类比 Vuex Module
class ShoppingListModel with ChangeNotifier {
// 状态(State)
List<ShoppingItem> _items = [];
double _budget = 500.0;
String _filter = 'all'; // 'all', 'active', 'completed'
// Getter(计算属性)
List<ShoppingItem> get items {
switch (_filter) {
case 'active':
return _items.where((item) => !item.isCompleted).toList();
case 'completed':
return _items.where((item) => item.isCompleted).toList();
default:
return _items;
}
}
double get totalSpent {
return _items
.where((item) => item.isCompleted)
.fold(0, (sum, item) => sum + item.totalPrice);
}
double get remainingBudget => _budget - totalSpent;
int get activeCount => _items.where((item) => !item.isCompleted).length;
// Mutations/Actions
void addItem(String name, double price, String category) {
final newItem = ShoppingItem(
name: name,
price: price,
category: category,
);
_items.add(newItem);
_sortItems(); // 内部方法,不对外暴露
notifyListeners(); // 相当于 Vuex 的 state 变化触发视图更新
}
void toggleItem(String id) {
final index = _items.indexWhere((item) => item.id == id);
if (index != -1) {
// 不可变性:创建新对象替换旧对象
final oldItem = _items[index];
final newItem = oldItem.copyWith(
isCompleted: !oldItem.isCompleted,
);
_items[index] = newItem;
notifyListeners();
}
}
void updateQuantity(String id, int newQuantity) {
final index = _items.indexWhere((item) => item.id == id);
if (index != -1 && newQuantity > 0) {
final oldItem = _items[index];
final newItem = oldItem.copyWith(quantity: newQuantity);
_items[index] = newItem;
notifyListeners();
}
}
void removeItem(String id) {
_items.removeWhere((item) => item.id == id);
notifyListeners();
}
void setFilter(String filter) {
_filter = filter;
notifyListeners(); // 过滤条件变化也需要通知
}
void updateBudget(double newBudget) {
_budget = newBudget;
notifyListeners();
}
// 私有方法,不需要通知外部
void _sortItems() {
_items.sort((a, b) => a.createdAt.compareTo(b.createdAt));
}
// 异步 Action 示例(如从 API 加载)
Future<void> loadInitialItems() async {
// 模拟 API 请求
await Future.delayed(Duration(seconds: 1));
_items = [
ShoppingItem(name: '牛奶', price: 12.5, category: '食品'),
ShoppingItem(name: '鸡蛋', price: 25.0, category: '食品', quantity: 2),
ShoppingItem(name: '纸巾', price: 15.0, category: '日用品', isCompleted: true),
];
notifyListeners();
}
}
3.2 第二步:Provider 注入(类比 Vue.use(Vuex))
dart
// lib/main.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'models/shopping_list_model.dart';
import 'screens/home_screen.dart';
void main() {
runApp(
// MultiProvider 类比 Vuex 的 modules
MultiProvider(
providers: [
// 每个 Provider 都是一个独立的"作用域"
ChangeNotifierProvider(
create: (context) => ShoppingListModel(),
// 可选:初始化时加载数据
lazy: false, // 立即创建,不延迟
),
// 可以添加更多 Provider(如用户配置、主题等)
],
child: MyApp(),
),
);
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
// 可以在这里访问 Provider,但通常不在这里消费数据
return MaterialApp(
title: '智能购物清单',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: HomeScreen(),
debugShowCheckedModeBanner: false,
);
}
}
3.3 第三步:UI 组件消费状态
3.3.1 方式一:Consumer(最常用)
dart
// lib/widgets/shopping_list.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../models/shopping_list_model.dart';
class ShoppingList extends StatelessWidget {
@override
Widget build(BuildContext context) {
// Consumer 类似于 Vue 的 mapState
return Consumer<ShoppingListModel>(
builder: (context, model, child) {
// model 是当前 ShoppingListModel 的实例
if (model.items.isEmpty) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.shopping_cart_outlined, size: 64, color: Colors.grey[300]),
SizedBox(height: 16),
Text(
'购物清单为空',
style: TextStyle(color: Colors.grey, fontSize: 18),
),
TextButton(
onPressed: () => model.loadInitialItems(),
child: Text('加载示例数据'),
),
],
),
);
}
return ListView.builder(
itemCount: model.items.length,
itemBuilder: (context, index) {
final item = model.items[index];
return _ShoppingItemCard(item: item);
},
);
},
);
}
}
class _ShoppingItemCard extends StatelessWidget {
final ShoppingItem item;
const _ShoppingItemCard({required this.item});
@override
Widget build(BuildContext context) {
// 在内部使用 Provider.of,类似 Vue 的 this.$store
final model = Provider.of<ShoppingListModel>(context, listen: false);
return Card(
margin: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
elevation: 2,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
child: ListTile(
leading: Checkbox(
value: item.isCompleted,
onChanged: (_) => model.toggleItem(item.id),
shape: CircleBorder(),
),
title: Text(
item.name,
style: TextStyle(
fontSize: 16,
decoration: item.isCompleted ? TextDecoration.lineThrough : null,
color: item.isCompleted ? Colors.grey : Colors.black87,
),
),
subtitle: Text(
'${item.quantity} × ¥${item.price.toStringAsFixed(2)} = ¥${item.totalPrice.toStringAsFixed(2)}',
style: TextStyle(
color: item.isCompleted ? Colors.grey : Colors.blueGrey,
),
),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
// 数量调整
Container(
decoration: BoxDecoration(
color: Colors.blue[50],
borderRadius: BorderRadius.circular(20),
),
child: Row(
children: [
IconButton(
icon: Icon(Icons.remove, size: 18),
onPressed: () => model.updateQuantity(item.id, item.quantity - 1),
),
Text('${item.quantity}'),
IconButton(
icon: Icon(Icons.add, size: 18),
onPressed: () => model.updateQuantity(item.id, item.quantity + 1),
),
],
),
),
SizedBox(width: 8),
// 删除按钮
IconButton(
icon: Icon(Icons.delete_outline, color: Colors.red[300]),
onPressed: () => model.removeItem(item.id),
),
],
),
),
);
}
}
3.3.2 方式二:Selector(性能优化)
dart
// lib/widgets/stats_card.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../models/shopping_list_model.dart';
class StatsCard extends StatelessWidget {
@override
Widget build(BuildContext context) {
// Selector 类似于 Vue 的计算属性 + 条件渲染
// 只监听特定数据的变化,避免不必要的重建
return Selector<ShoppingListModel, double>(
selector: (context, model) => model.totalSpent,
builder: (context, totalSpent, child) {
// child 是不会重建的部分(性能优化)
return Selector<ShoppingListModel, double>(
selector: (context, model) => model.remainingBudget,
builder: (context, remainingBudget, child) {
return Selector<ShoppingListModel, int>(
selector: (context, model) => model.activeCount,
builder: (context, activeCount, child) {
return Card(
margin: EdgeInsets.all(16),
color: Colors.blue[50],
child: Padding(
padding: EdgeInsets.all(16),
child: Column(
children: [
// 总花费
_StatRow(
icon: Icons.shopping_cart,
label: '已花费',
value: '¥${totalSpent.toStringAsFixed(2)}',
color: Colors.blue,
),
Divider(height: 20),
// 剩余预算
_StatRow(
icon: Icons.account_balance_wallet,
label: '剩余预算',
value: '¥${remainingBudget.toStringAsFixed(2)}',
color: remainingBudget > 0 ? Colors.green : Colors.red,
),
Divider(height: 20),
// 待购数量
_StatRow(
icon: Icons.checklist,
label: '待购项目',
value: '$activeCount 项',
color: Colors.orange,
),
// 预算进度条
SizedBox(height: 16),
LinearProgressIndicator(
value: totalSpent / 500, // 500是预算上限
backgroundColor: Colors.grey[200],
valueColor: AlwaysStoppedAnimation<Color>(
remainingBudget > 0 ? Colors.green : Colors.red,
),
),
],
),
),
);
},
);
},
);
},
);
}
}
class _StatRow extends StatelessWidget {
final IconData icon;
final String label;
final String value;
final Color color;
const _StatRow({
required this.icon,
required this.label,
required this.value,
required this.color,
});
@override
Widget build(BuildContext context) {
return Row(
children: [
Icon(icon, color: color),
SizedBox(width: 12),
Expanded(
child: Text(label, style: TextStyle(fontSize: 14, color: Colors.grey[700])),
),
Text(
value,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: color,
),
),
],
);
}
}
3.3.3 方式三:context.watch / context.read(Dart 扩展方法)
dart
// lib/screens/home_screen.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../models/shopping_list_model.dart';
import '../widgets/shopping_list.dart';
import '../widgets/stats_card.dart';
import '../widgets/add_item_dialog.dart';
class HomeScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
// context.watch 类似于 Vue 3 的 useStore() + computed
// 它会监听变化,数据更新时重建 Widget
final filter = context.watch<ShoppingListModel>().filter;
return Scaffold(
appBar: AppBar(
title: Text('智能购物清单'),
actions: [
// 过滤按钮
PopupMenuButton<String>(
onSelected: (value) {
// context.read 类似于 listen: false 的 Provider.of
// 只读取数据,不监听变化
context.read<ShoppingListModel>().setFilter(value);
},
itemBuilder: (context) => [
PopupMenuItem(value: 'all', child: Text('全部')),
PopupMenuItem(value: 'active', child: Text('待购')),
PopupMenuItem(value: 'completed', child: Text('已购')),
],
child: Row(
children: [
Icon(Icons.filter_list),
SizedBox(width: 4),
Text(_getFilterText(filter)),
],
),
),
SizedBox(width: 16),
],
),
body: Column(
children: [
// 统计卡片
StatsCard(),
SizedBox(height: 8),
// 购物清单
Expanded(child: ShoppingList()),
],
),
floatingActionButton: FloatingActionButton(
onPressed: () => _showAddDialog(context),
child: Icon(Icons.add),
backgroundColor: Colors.blue,
),
);
}
String _getFilterText(String filter) {
switch (filter) {
case 'active': return '待购';
case 'completed': return '已购';
default: return '全部';
}
}
void _showAddDialog(BuildContext context) {
showDialog(
context: context,
builder: (context) => AddItemDialog(),
);
}
}
四、Vue 开发者特别注意的陷阱
4.1 避免不必要的重建
错误示例:
dart
// 整个页面都在监听变化
@override
Widget build(BuildContext context) {
final model = Provider.of<ShoppingListModel>(context);
return Scaffold(
appBar: AppBar(title: Text('购物清单')),
body: ListView(
children: [
// 任何状态变化都会重建整个页面!
_buildStats(model),
_buildList(model),
_buildFooter(model),
],
),
);
}
正确做法:
dart
// 精细化监听
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('购物清单')),
body: Column(
children: [
// 只有统计部分监听统计数据变化
StatsCard(),
// 只有列表部分监听列表数据变化
Expanded(child: ShoppingList()),
// 页脚不需要监听
_buildFooter(),
],
),
);
}
4.2 处理异步操作
dart
class ShoppingListModel with ChangeNotifier {
bool _isLoading = false;
String? _error;
bool get isLoading => _isLoading;
String? get error => _error;
Future<void> fetchItems() async {
_isLoading = true;
_error = null;
notifyListeners(); // 通知 UI 显示加载状态
try {
// 模拟 API 调用
await Future.delayed(Duration(seconds: 2));
_items = await _apiService.getItems();
_error = null;
} catch (e) {
_error = '加载失败: $e';
} finally {
_isLoading = false;
notifyListeners(); // 通知 UI 更新状态
}
}
}
// UI 中使用
Consumer<ShoppingListModel>(
builder: (context, model, child) {
if (model.isLoading) {
return Center(child: CircularProgressIndicator());
}
if (model.error != null) {
return Center(child: Text('错误: ${model.error}'));
}
return ListView(...);
},
)
4.3 嵌套 Provider 作用域
dart
// 可以嵌套不同作用域的 Provider
ChangeNotifierProvider(
create: (_) => GlobalModel(),
child: Builder(
builder: (context) {
// 这里可以访问 GlobalModel
final globalModel = context.read<GlobalModel>();
return ChangeNotifierProvider(
create: (_) => LocalModel(globalModel.someValue),
child: LocalScreen(),
);
},
),
)
五、项目结构建议
bash
lib/
├── main.dart # 应用入口,Provider 初始化
├── models/ # 数据模型(相当于 Vuex modules)
│ ├── shopping_item.dart # 数据类(相当于 Vuex state 结构)
│ ├── shopping_list_model.dart # ChangeNotifier 类
│ └── user_preferences.dart # 其他模型
├── providers/ # Provider 配置(可选)
│ └── multi_providers.dart # MultiProvider 配置
├── screens/ # 页面级组件
│ ├── home_screen.dart
│ └── settings_screen.dart
├── widgets/ # 可复用 UI 组件
│ ├── shopping_list.dart
│ ├── stats_card.dart
│ └── add_item_dialog.dart
├── services/ # 业务逻辑(API 调用等)
│ └── api_service.dart
└── utils/ # 工具函数
└── formatters.dart
六、调试技巧
6.1 Provider 调试工具
dart
// 在 MaterialApp 外层包裹
void main() {
runApp(
MultiProvider(
providers: [...],
child: Builder(
builder: (context) {
// 添加调试层
return ProviderDebugger(
child: MyApp(),
showProviderData: true, // 显示当前可用的 Provider
showRebuildCount: true, // 显示 Widget 重建次数
);
},
),
),
);
}
6.2 性能分析
dart
// 使用 Flutter DevTools 的 Performance 面板
// 1. 运行应用时按 'P' 打开性能面板
// 2. 查看 Widget 重建次数
// 3. 检查是否有不必要的重建
// 在代码中添加调试信息
class ShoppingList extends StatelessWidget {
@override
Widget build(BuildContext context) {
print('ShoppingList 重建'); // 查看控制台输出
return Consumer<ShoppingListModel>(
builder: (context, model, child) {
print('Consumer 重建,项目数: ${model.items.length}');
return ...;
},
);
}
}
总结:Vue 到 Flutter 的关键转换
- 状态变更 :Vue 自动追踪 vs Flutter 手动
notifyListeners() - 组件更新:Vue 虚拟 DOM diff vs Flutter Widget 树重建
- 状态访问 :Vue
this.$storevs FlutterProvider.of(context) - 响应式:Vue 自动依赖收集 vs Flutter 显式声明依赖
- 性能优化:Vue 计算属性 vs Flutter Selector
通过这个详细的购物清单示例,你应该能够将 Vue 的状态管理经验顺利迁移到 Flutter Provider。记住核心原则:状态变化 → 通知监听者 → Widget 重建,其他都是围绕这个机制的优化和实践。