Flutter 状态管理极速版:view_model

1,397 阅读6分钟

ViewModel Logo

view_model

Flutter 缺失的 ViewModel 方案 — 万物皆 ViewModel ✨

PackageVersion
view_modelPub Version
view_model_annotationPub Version
view_model_generatorPub Version

Codecov

更新日志 | English Doc

目录

为什么要用 view_model?

万物皆 ViewModel,任何类都能处处访问。

其他方案让你二选一:

  • 全局状态(到处共享,容易混乱)
  • 手动 Provider(样板代码多 + BuildContext 地狱)

view_model 两者兼得,绝绝子!

  • 万物皆 ViewModel - Repository、Service、任何类都能是 ViewModel
  • 处处可访问 - 彻底告别 BuildContext 传参
  • 默认隔离 - 每个 Widget 独享实例,互不干扰
  • 按需共享 - 加个 key 就能随处复用
  • 零样板 - 没有复杂的胶水代码
  • 自动生命周期 - 自动创建,自动销毁,省心!

📦 安装搞起

dependencies:
  view_model: ^latest_version

dev_dependencies:
  build_runner: ^latest_version
  view_model_generator: ^latest_version # 强烈推荐,用了就回不去!

⚡️ 三步快速上手

1. 定义 ViewModel

继承 ViewModel,用 update() 通知界面刷新。

class CounterViewModel extends ViewModel {
  int count = 0;

  void increment() {
    update(() => count++);
  }
}

2. 创建 Provider

定义一个全局 Provider,Widget 就靠它找到你的 ViewModel。

final counterProvider = ViewModelProvider<CounterViewModel>(
  builder: () => CounterViewModel(),
);

(小贴士:用 view_model_generator 可以跳过这一步哦!) 😉

3. 在 Widget 中使用

StatefulWidget 中混入 ViewModelStateMixin

class CounterPage extends StatefulWidget {
  @override
  State<CounterPage> createState() => _CounterPageState();
}

class _CounterPageState extends State<CounterPage> with ViewModelStateMixin {
  @override
  Widget build(BuildContext context) {
    // 监听 provider。ViewModel 更新时,Widget 会自动重建。
    final vm = vef.watch(counterProvider);

    return Scaffold(
      body: Center(
        child: Text('${vm.count}'),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: vm.increment,
        child: Icon(Icons.add),
      ),
    );
  }
}

🌈 核心功能详解

1. 万能访问 (Vef) 🗝️

核心概念Vef 是一个 Mixin,可以混入到 任何类 中,不只是 Widget!

vef (ViewModel Execution Framework) 是你即取即用 ViewModel 的神器。

在 Widget 中 (内置支持)

用了 ViewModelStateMixinvef 自动到手:

class _MyPageState extends State<MyPage> with ViewModelStateMixin {
  @override
  Widget build(BuildContext context) {
    final vm = vef.watch(myProvider); // vef 直接用
    return Text(vm.data);
  }
}
在 ViewModel 中 (内置支持)

ViewModel 已经内置了 vef 你可以直接调用其他 ViewModel,套娃也没问题:

// ✅ ViewModel 调用其他 ViewModel
class CartViewModel extends ViewModel {
  void checkout() {
    // 直接用 vef,不需要 extra mixin
    final userVM = vef.read(userProvider);
    final paymentVM = vef.read(paymentProvider);

    processOrder(userVM.user, paymentVM.method);
  }
}

class UserViewModel extends StateViewModel<UserState> {
  void updateTheme() {
    // 访问全局设置 VM
    final settingsVM = vef.read(settingsProvider);
    applyTheme(settingsVM.state.theme);
  }
}
在任何类中 - "万物皆 ViewModel"

设计哲学:Repository、Service、Helper... 它们都可以是 ViewModel!只需继承 ViewModel,马上拥有访问能力,告别 BuildContext

// ✅ Repository 也是 ViewModel
class UserRepository extends ViewModel {
  Future<User> fetch() async {
    // 轻松拿到 AuthViewModel 的 token
    final authVM = vef.read(authProvider);
    return api.get('/user', token: authVM.token);
  }
}

// ✅ Service 也是 ViewModel
class AnalyticsService extends ViewModel {
  void trackEvent(String event) {
    final userVM = vef.read(userProvider);
    analytics.log(event, userId: userVM.userId);
  }
}

// ✅ 测试 Helper 也是 ViewModel
class TestHelper extends ViewModel {
  void setupTestData() {
    final authVM = vef.read(authProvider);
    authVM.loginAsTestUser();
  }
}

ViewModel 之间协同工作

class UserProfileViewModel extends ViewModel {
  final UserRepository _repo;
  UserProfileViewModel(this._repo);

  Future<void> loadUser() async {
    // 通过 vef 访问其他 ViewModel
    final authVM = vef.read(authProvider);
    final user = await _repo.fetch(); // Repo 内部也用 vef

    // 通知其他 ViewModel
    vef.read(cacheProvider).updateCache(user);
  }
}

为什么说 "万物皆 ViewModel" 真香?

  • 无需 Context - 哪里都能拿数据,不再透传 Context
  • 统一 DI - 全层级统一的依赖注入模式
  • 自动引用计数 - 内存管理全自动
  • 可测试 - 这里的所有依赖都能 Mock
  • 灵活 - 怎么舒服怎么写
Vef 方法查询表
方法用法
vef.watch(provider)获取 + 监听。返回实例并订阅更新(触发重建)。在 build()initState() 里放心用!
vef.read(provider)仅获取。返回实例但不订阅。不会触发重建。在回调(如 onPressed)里用它!
vef.listen(provider)仅监听。订阅变化来处理副作用(比如弹窗),不重建 UI。会自动释放哦。
vef.watchCached(key:)通过 key 获取已存在的实例(只能拿现成的,不创建)。
vef.readCached(key:)通过 key 读取已存在的实例(不监听)。

2. 不可变状态 (StateViewModel) 🔒

对于复杂状态,不可变对象 (Immutable) 才是 YYDS!StateViewModel 专门为此设计。

// 1. 状态类 (带上 copyWith)
class UserState {
  final String name;
  final bool isLoading;

  UserState({this.name = '', this.isLoading = false});

  // 必须:copyWith 方法用于不可变更新
  UserState copyWith({String? name, bool? isLoading}) {
    return UserState(
      name: name ?? this.name,
      isLoading: isLoading ?? this.isLoading,
    );
  }
}

// 2. ViewModel
class UserViewModel extends StateViewModel<UserState> {
  UserViewModel() : super(state: UserState());

  void loadUser() async {
    setState(state.copyWith(isLoading: true)); // 更新状态
    // ... 请求接口 ...
    setState(state.copyWith(isLoading: false, name: 'Alice'));
  }
}

墙裂推荐:用 freezedbuilt_value 自动生成 copyWith,省时省力!

监听变化

只想在特定状态变化时搞事情(比如弹窗、跳转)?完全没问题!

// 监听特定属性
vef.listenStateSelect(
  userProvider,
  selector: (state) => state.isLoading,
  onChanged: (prev, isLoading) {
    if (isLoading) {
      showLoadingDialog();
    } else {
      dismissLoadingDialog();
    }
  },
);

// 监听整个状态
vef.listenState(userProvider, onChanged: (prev, state) {
  print('状态变啦:$prev -> $state');
});

3. 依赖注入 (参数传递) 💉

ViewModel 需要外部参数(比如 ID 或 Repository)?必须支持!

// 定义需要参数 (int id) 的 provider
final userProvider = ViewModelProvider.arg<UserViewModel, int>(
  builder: (int id) => UserViewModel(id),
);

// 在 Widget 中使用
final vm = vef.watch(userProvider(123)); // 传参只需一步,太方便了

4. 实例共享 (Keys) 🔗

默认行为:隔离 当你调用 vef.watch(provider),你拿到的是这个 Widget 独享的全新实例。别的 Widget 用同一个 provider,拿到的是另一个实例。(再也不用担心状态污染!)

共享行为:Keys 想在不同 Widget 间(比如商品详情页和它的 Header)共享同一个 ViewModel?加个 key 就行!

场景ProductPage 和子组件 ProductHeader 需要共享数据。

// 1. 定义 provider,key 基于参数生成
final productProvider = ViewModelProvider.arg<ProductViewModel, String>(
  builder: (id) => ProductViewModel(id),
  key: (id) => 'product_$id', // Key 相同,实例就相同
);

// 2. 父组件 (Page)
class ProductPage extends StatefulWidget {
  final String productId;
  // ...
  build(context) {
    // 创建或查找 key 为 'product_123' 的实例
    final vm = vef.watch(productProvider(productId));
    // ...
  }
}

// 3. 子组件 (Header)
class ProductHeader extends StatefulWidget {
  final String productId;
  // ...
  build(context) {
    // Key 一样,拿到的就是同一个实例!完美同步!
    final vm = vef.watch(productProvider(productId)); 
    return Text(vm.title);
  }
}

5. 自动生命周期 ♻️

view_model 使用严格的引用计数来管理内存,不用操心!

  1. 创建:第一次 watchreadlisten 时,ViewModel 被创建(引用 +1)。
  2. 存活:只要 Widget 还在,引用就在。
    • watch:持有引用 + 监听。
    • read:持有引用(不监听)。
    • listen:内部持有引用。
  3. 销毁:Widget 销毁时,引用 -1。当引用归零,ViewModel 自动 dispose()。👋 拜拜了您嘞!

例外 (Keep Alive):如果你在 provider 里设置了 aliveForever: true,那它就永远不会自动销毁,哪怕引用归零。这就变成全局单例啦!

6. 长生不老 (全局状态)

默认情况下,ViewModel 无人使用时会自动销毁。但有些 ViewModel 需要“长生不老”(比如用户会话、应用设置)。

你可以通过设置 aliveForever: true 来实现。强烈建议同时指定 key,以便在全局范围内唯一标识和查找该实例。

手动定义
final appSettingsProvider = ViewModelProvider<AppSettingsViewModel>(
  builder: () => AppSettingsViewModel(),
  key: 'app_settings', // 指定一个全局 key
  aliveForever: true, // 这个实例永远不会被销毁
);
使用生成器 (推荐)
@GenProvider(key: 'app_settings', aliveForever: true)
class AppSettingsViewModel extends ViewModel {}

注意:即使 aliveForever 为 true,ViewModel 依然是 懒加载 的。只有第一次访问时才会创建。

7. 架构模式 (Clean Architecture) 🏗️

怎么用 view_model 搭建 Clean Architecture?抄作业时间到!📝

// ============================================
// 1️⃣ 数据层 - Repository 也是 ViewModel
// ============================================
@GenProvider()
class UserRepository extends ViewModel {
  final ApiClient _api;

  UserRepository(this._api);

  // ✅ Repository 是 ViewModel - 可以访问其他 VM
  Future<User> fetchUser(int id) async {
    final authVM = vef.read(authProvider);
    return _api.get('/users/$id',
      headers: {'Authorization': 'Bearer ${authVM.token}'}
    );
  }

  Future<void> updateUser(User user) async {
    final authVM = vef.read(authProvider);
    await _api.put('/users/${user.id}', user.toJson(),
      headers: {'Authorization': 'Bearer ${authVM.token}'}
    );
  }
}

// ============================================
// 2️⃣ 领域层 - 全局 & 业务 ViewModel
// ============================================
@GenProvider(key: 'auth', aliveForever: true)
class AuthViewModel extends StateViewModel<AuthState> {
  AuthViewModel() : super(state: AuthState.unauthenticated());

  String? get token => state.token;
  bool get isAuthenticated => state.isAuthenticated;

  Future<void> login(String email, String password) async {
    setState(state.copyWith(isLoading: true));
    try {
      final result = await authService.login(email, password);
      setState(AuthState.authenticated(result.token, result.user));
    } catch (e) {
      setState(state.copyWith(error: e.toString(), isLoading: false));
    }
  }

  void logout() {
    setState(AuthState.unauthenticated());
  }
}

@GenProvider()
class UserViewModel extends StateViewModel<UserState> {
  final UserRepository _repository;

  UserViewModel(this._repository) : super(state: UserState.initial());

  Future<void> loadUser(int id) async {
    setState(state.copyWith(isLoading: true));
    try {
      // ✅ Repository 内部自己搞定 Auth,这里只管业务
      final user = await _repository.fetchUser(id);
      setState(state.copyWith(user: user, isLoading: false));
    } catch (e) {
      setState(state.copyWith(error: e.toString(), isLoading: false));
    }
  }

  Future<void> updateProfile(String name) async {
    final updated = state.user!.copyWith(name: name);
    await _repository.updateUser(updated);
    setState(state.copyWith(user: updated));

    // 通知其他 ViewModel
    vef.read(profileCacheProvider).invalidate();
  }
}

// ============================================
// 3️⃣ 表现层 - Widgets
// ============================================
class UserProfilePage extends StatefulWidget {
  final int userId;
  const UserProfilePage({required this.userId});

  @override
  State<UserProfilePage> createState() => _UserProfilePageState();
}

class _UserProfilePageState extends State<UserProfilePage>
    with ViewModelStateMixin {

  @override
  void initState() {
    super.initState();
    // 页面打开加载数据
    vef.read(userProvider).loadUser(widget.userId);

    // 监听 Auth 变化 (比如退出登录)
    vef.listenStateSelect(
      authProvider,
      selector: (state) => state.isAuthenticated,
      onChanged: (prev, isAuth) {
        if (!isAuth) {
          Navigator.of(context).pushReplacementNamed('/login');
        }
      },
    );
  }

  @override
  Widget build(BuildContext context) {
    final userVM = vef.watch(userProvider);
    final authVM = vef.watch(authProvider);

    if (userVM.state.isLoading) {
      return Center(child: CircularProgressIndicator());
    }

    return Scaffold(
      appBar: AppBar(
        title: Text(userVM.state.user?.name ?? 'Profile'),
        actions: [
          IconButton(
            icon: Icon(Icons.logout),
            onPressed: authVM.logout,
          ),
        ],
      ),
      body: Column(
        children: [
          Text('Name: ${userVM.state.user?.name}'),
          Text('Email: ${userVM.state.user?.email}'),
          ElevatedButton(
            onPressed: () => _showEditDialog(userVM),
            child: Text('Edit Profile'),
          ),
        ],
      ),
    );
  }
  
  // ... helpers
}

划重点

  • 🔹 "万物皆 ViewModel" - 既然都是 ViewModel,那一套逻辑通吃!
  • 🔹 无需 Context - 组件间通信靠 vef,清爽!
  • 🔹 Repository 是 ViewModel - 它可以自己管理依赖(比如 Auth)。
  • 🔹 各司其职 - 业务 ViewModel 专注逻辑,Repository 专注数据。
  • 🔹 全局状态 (Auth) 用 aliveForever: true + key 搞定。
  • 🔹 统一的 DI - 全层级一样的写法,强迫症福音!

8. 代码生成 (强烈推荐) 🤖

手写 ViewModelProvider 太麻烦?用 @genProvider 解放双手!

@genProvider
class MyViewModel extends ViewModel {}

运行 dart run build_runner build,Provider 自动生成! 详情看这里 👉 view_model_generator

🧪 测试指南

Widget 测试

setProxy 就能轻松 Mock 任何 ViewModel!

testWidgets('显示用户数据', (tester) async {
  final mockVM = MockUserViewModel();
  when(mockVM.state).thenReturn(UserState(user: testUser));

  // 用 Mock 替换真实实现
  userProvider.setProxy(
    ViewModelProvider(builder: () => mockVM)
  );

  await tester.pumpWidget(MyApp());

  expect(find.text(testUser.name), findsOneWidget);
});

单元测试 (Repository ViewModel)

ViewModel 依赖其他 ViewModel 怎么测?Mock 这个依赖!

void main() {
  late UserRepository repository;
  late MockAuthViewModel mockAuthVM;

  setUp(() {
    mockAuthVM = MockAuthViewModel();
    when(mockAuthVM.token).thenReturn('test-token');

    // Mock auth 模块
    authProvider.setProxy(
      ViewModelProvider(builder: () => mockAuthVM)
    );

    repository = UserRepository(mockApiClient);
  });

  tearDown(() {
    authProvider.clearProxy();
  });

  test('请求带上了 token', () async {
    // 仓库内部会 vef.read(authProvider)
    await repository.fetchUser(123);

    // 验证 token 是否被使用
    verify(mockApiClient.get(
      '/users/123',
      headers: {'Authorization': 'Bearer test-token'}
    ));
  });
}

单元测试 (依赖其他 VM 的 VM)

当你的 ViewModel 内部也用了 vef,测试时需要手动创建一个 Vef 环境:

// 测试辅助类
class TestVef with Vef {}

void main() {
  test('CartViewModel 访问 UserViewModel', () {
    // 创建测试用的 Vef 环境
    final testVef = TestVef();

    final mockUserVM = MockUserViewModel();
    when(mockUserVM.user).thenReturn(testUser);

    userProvider.setProxy(
      ViewModelProvider(builder: () => mockUserVM)
    );

    // ✅ 通过 Vef 创建 CartViewModel,这样它才能拿到 Mock 的依赖
    final cartVM = testVef.read(cartProvider);
    cartVM.checkout();

    // 验证调用
    verify(mockUserVM.user).called(1);

    testVef.dispose();
  });
}

� 避坑指南 (必看)

aliveForever 忘了加 key

如果只加 aliveForever 不加 key,每个页面还是会创建新实例(这就尴尬了)。

// ❌ 错!还是多实例
@GenProvider(aliveForever: true)
class AuthViewModel extends ViewModel {}

// ✅ 对!全局单例
@GenProvider(key: 'auth', aliveForever: true)
class AuthViewModel extends ViewModel {}

❌ State 类忘了 copyWith

StateViewModel 默认比较内存地址,想更新状态,必须给个新对象

copyWith 是标准做法:

// ❌ 错 - 没有 copyWith
class MyState {
  final int count;
  MyState(this.count);
}

// ✅ 对 - 有 copyWith
class MyState {
  final int count;
  MyState(this.count);

  MyState copyWith({int? count}) => MyState(count ?? this.count);
}

// ✅ 完美 - 用 freezed/built_value 自动生成
@freezed
class MyState with _$MyState {
  factory MyState({required int count}) = _MyState;
}

�🔧 全局配置

main() 里可以配置全局行为。

void main() {
  ViewModel.initialize(
    config: ViewModelConfig(
      // 开启日志大法 (Debug 必备)
      isLoggingEnabled: true,

      // 自定义状态比较逻辑 (可选)
      equals: (prev, current) {
        // 如果你像用 Equatable...
        if (prev is Equatable && current is Equatable) {
          return prev == current;
        }
        return identical(prev, current);
      },

      // 监听器报错处理 (v0.13.0 新增)
      onListenerError: (error, stackTrace, context) {
        // 上报给 Crashlytics
        FirebaseCrashlytics.instance.recordError(error, stackTrace);

        if (kDebugMode) {
          print('❌ $context 报错了: $error');
        }
      },

      // 销毁时报错处理 (v0.13.0 新增)
      onDisposeError: (error, stackTrace) {
        print('⚠️ 销毁时出错了: $error');
      },
    ),

    // 添加全局导航/生命周期观察者
    lifecycles: [
      MyViewModelObserver(),
    ],
  );

  runApp(MyApp());
}

// 自定义观察者示例
class MyViewModelObserver extends ViewModelLifecycle {
  @override
  void onCreate(ViewModel viewModel, InstanceArg arg) {
    print('✅ 创建了: ${viewModel.runtimeType}');
  }

  @override
  void onDispose(ViewModel viewModel, InstanceArg arg) {
    print('🗑️ 销毁了: ${viewModel.runtimeType}');
  }
}

v0.13.0 新特性

  • onListenerError: 捕获 notifyListeners() 和状态监听里的异常,防止一处崩全盘崩。
  • onDisposeError: 捕获资源清理时的异常。
  • 🎯 对于崩溃上报和调试非常有帮助!

📄 License

MIT License - 详见 LICENSE 文件。