1. 为什么需要ViewModel管理状态
在Flutter开发中,状态管理一直是个绕不开的话题。我刚开始接触Flutter时,经常把业务逻辑和UI代码混在一起写,结果就是代码越来越臃肿,维护起来特别痛苦。后来尝试了各种状态管理方案,发现ChangeNotifier配合ViewModel的模式特别适合中小型项目。
ChangeNotifier是Flutter提供的一个简单但强大的工具类,它实现了观察者模式。当数据变化时,它会通知所有监听者更新UI。ViewModel则是MVVM架构中的关键角色,它负责处理业务逻辑和状态管理,与UI层解耦。
这种组合有几个明显优势:
- 代码分层清晰:UI只负责展示,业务逻辑都在ViewModel中
- 测试方便:可以单独测试ViewModel而不需要构建UI
- 复用性强:同一个ViewModel可以被多个Widget使用
- 生命周期明确:ViewModel可以绑定到Widget生命周期
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. ChangeNotifier的核心机制
2.1 基本工作原理
ChangeNotifier的核心就是两个方法:addListener()和notifyListeners()。当数据变化时调用notifyListeners(),所有通过addListener()注册的回调都会被触发。
dart复制class Counter with ChangeNotifier {
int _count = 0;
int get count => _count;
void increment() {
_count++;
notifyListeners();
}
}
这个简单的计数器例子展示了基本用法。每次increment()被调用时,都会通知监听者更新。
2.2 与Provider的配合
在实际项目中,我们通常会用Provider包来管理ChangeNotifier的生命周期和依赖关系:
dart复制void main() {
runApp(
ChangeNotifierProvider(
create: (context) => Counter(),
child: MyApp(),
),
);
}
Provider会自动处理ChangeNotifier的创建和销毁,还能方便地在Widget树中获取实例:
dart复制final counter = Provider.of<Counter>(context);
3. ViewModel的实践模式
3.1 典型ViewModel结构
一个完整的ViewModel通常包含以下几个部分:
dart复制class UserViewModel with ChangeNotifier {
// 状态数据
User _user;
bool _isLoading = false;
String _errorMessage;
// 业务方法
Future<void> fetchUser() async {
_isLoading = true;
notifyListeners();
try {
_user = await UserRepository.getUser();
_errorMessage = null;
} catch (e) {
_errorMessage = e.toString();
} finally {
_isLoading = false;
notifyListeners();
}
}
// 计算属性
String get displayName => _user?.name ?? 'Guest';
// 状态getter
bool get isLoading => _isLoading;
String get errorMessage => _errorMessage;
}
3.2 生命周期管理
ViewModel通常绑定到页面生命周期。使用Provider时,可以通过ChangeNotifierProvider.value来复用ViewModel:
dart复制class UserPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider.value(
value: UserViewModel(),
child: Scaffold(
// ...
),
);
}
}
对于需要持久化的ViewModel,可以考虑使用GetIt等依赖注入工具。
4. 常见问题与优化技巧
4.1 性能优化
过度调用notifyListeners()会导致不必要的重建。我总结了几个优化点:
- 使用Equatable或重写==操作符来避免相同值的重复通知
- 对频繁更新的状态考虑使用ValueNotifier
- 在列表更新时使用ListView.builder的itemBuilder优化
dart复制class OptimizedListViewModel with ChangeNotifier {
List<Item> _items = [];
void updateItem(int index, Item newItem) {
if (_items[index] != newItem) {
_items[index] = newItem;
notifyListeners();
}
}
}
4.2 测试策略
ViewModel的测试非常直观,因为不依赖UI:
dart复制void main() {
test('counter increments', () {
final counter = Counter();
expect(counter.count, 0);
counter.increment();
expect(counter.count, 1);
});
test('user loading state', () async {
final mockRepo = MockUserRepository();
final viewModel = UserViewModel(repository: mockRepo);
expect(viewModel.isLoading, false);
final future = viewModel.fetchUser();
expect(viewModel.isLoading, true);
await future;
expect(viewModel.isLoading, false);
});
}
4.3 与BLoC的对比
很多开发者会纠结用ChangeNotifier还是BLoC。根据我的经验:
-
ChangeNotifier更适合:
- 简单到中等复杂度的状态管理
- 需要快速上手的项目
- 团队对Stream/RxDart不熟悉的情况
-
BLoC更适合:
- 复杂的状态转换和组合
- 需要持久化的事件流
- 已有RxDart经验的团队
5. 实战案例:购物车实现
让我们通过一个购物车案例来看看完整的实现:
5.1 ViewModel定义
dart复制class CartViewModel with ChangeNotifier {
final List<CartItem> _items = [];
Coupon _coupon;
List<CartItem> get items => List.unmodifiable(_items);
double get total => _items.fold(0, (sum, item) => sum + item.price * item.quantity);
double get discount => _coupon?.discount ?? 0;
double get finalTotal => total - discount;
void addItem(Product product) {
final existing = _items.firstWhere(
(item) => item.productId == product.id,
orElse: () => null,
);
if (existing != null) {
existing.quantity++;
} else {
_items.add(CartItem.fromProduct(product));
}
notifyListeners();
}
void applyCoupon(Coupon coupon) {
_coupon = coupon;
notifyListeners();
}
}
5.2 UI集成
dart复制class CartPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('购物车')),
body: Consumer<CartViewModel>(
builder: (context, cart, child) {
if (cart.items.isEmpty) {
return Center(child: Text('购物车为空'));
}
return Column(
children: [
Expanded(
child: ListView.builder(
itemCount: cart.items.length,
itemBuilder: (context, index) {
final item = cart.items[index];
return CartItemTile(
item: item,
onRemove: () => cart.removeItem(item),
);
},
),
),
CartSummary(
total: cart.total,
discount: cart.discount,
finalTotal: cart.finalTotal,
),
],
);
},
),
);
}
}
5.3 状态持久化
如果需要持久化购物车状态,可以结合shared_preferences:
dart复制class PersistentCartViewModel extends CartViewModel {
final SharedPreferences prefs;
PersistentCartViewModel(this.prefs) {
_loadCart();
}
Future<void> _loadCart() async {
final json = prefs.getString('cart');
if (json != null) {
_items = (jsonDecode(json) as List)
.map((e) => CartItem.fromJson(e))
.toList();
notifyListeners();
}
}
@override
void addItem(Product product) {
super.addItem(product);
_saveCart();
}
Future<void> _saveCart() async {
final json = jsonEncode(_items.map((e) => e.toJson()).toList());
await prefs.setString('cart', json);
}
}
6. 进阶技巧与最佳实践
6.1 状态分组
当ViewModel变得庞大时,可以考虑按功能拆分状态:
dart复制class AppViewModel with ChangeNotifier {
final UserViewModel user;
final CartViewModel cart;
final SettingsViewModel settings;
AppViewModel({
this.user,
this.cart,
this.settings,
}) {
user.addListener(notifyListeners);
cart.addListener(notifyListeners);
settings.addListener(notifyListeners);
}
@override
void dispose() {
user.removeListener(notifyListeners);
cart.removeListener(notifyListeners);
settings.removeListener(notifyListeners);
super.dispose();
}
}
6.2 异步操作处理
对于复杂的异步操作,可以使用AsyncNotifier:
dart复制class DataViewModel extends ChangeNotifier {
AsyncValue<Data> _data = AsyncValue.loading();
Future<void> fetchData() async {
_data = AsyncValue.loading();
notifyListeners();
try {
final result = await DataService.fetch();
_data = AsyncValue.data(result);
} catch (e) {
_data = AsyncValue.error(e);
} finally {
notifyListeners();
}
}
}
6.3 与导航集成
ViewModel可以处理页面导航逻辑:
dart复制class AuthViewModel with ChangeNotifier {
final AuthService auth;
AuthState _state = AuthState.initial;
Future<void> login(String email, String password) async {
_state = AuthState.loading;
notifyListeners();
try {
await auth.login(email, password);
_state = AuthState.success;
} catch (e) {
_state = AuthState.failure(e.toString());
}
notifyListeners();
}
void navigateBasedOnState(BuildContext context) {
switch (_state) {
case AuthState.success:
Navigator.pushReplacementNamed(context, '/home');
break;
case AuthState.failure:
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('登录失败')),
);
break;
default:
break;
}
}
}
在实际项目中,我发现这种模式特别适合需要频繁交互的页面。它保持了代码的整洁性,同时提供了足够的灵活性来处理各种业务场景。
