view_model 1.1.0 copy "view_model: ^1.1.0" to clipboard
view_model: ^1.1.0 copied to clipboard

Everything is ViewModel. Enjoy automatic lifecycle management, prevent memory leaks, and share state effortlessly. Simple, lightweight, and powerful.

1.1.0 #

  • Give every ViewModel generation a stable dependency binding. Nested children now live for at least as long as their parent, retain unkeyed identity across root-owner handoffs, and receive the parent's root binding additions/removals in real time with source-aware reference counting.
  • Add construction-lineage and runtime dependency-cycle checks, failure-atomic dependency-scope rollback, and transaction-level notification deduplication for diamond graphs. Every aliveForever ViewModel now requires an explicit key at both root and nested resolution sites so retained entries remain globally reachable; the Store also rejects lower-level retained factories without a key before invoking their builder.
  • Extend DevTools with an explicit lifecycle registry for every observed root and dependency binding, including empty initialized roots. The graph now renders parent-generation ownership as parent VM → virtual binding → child VM and uses depth-aware layout while replacing the legacy edge payload with typed ownership relationships.
  • Apply parent ownership consistently to watch/read, cached lookup, and tag-batch lookup. Read variants still ignore child notifyListeners() but observe handle disposal/recycle.
  • Remove the relationship-preserving recreate API from bindings and the instance runtime. Use a new explicit key for an independent instance, or force recycle and re-resolve through a watch/read getter when replacing the shared generation globally is intentional.
  • Remove the deprecated ObservableValue, ObserverBuilder, ObserverBuilder2, and ObserverBuilder3 APIs. Use Flutter's ValueNotifier with ValueListenableBuilder for widget-local values, or a StateViewModel with ViewModelSpec for managed state.

1.0.7 #

  • Restore ViewModelConfig.equals as the global equality fallback. Full state uses local equals → global equalsidentical(), while selected values use explicit equals → global equals==.
  • Fold the selected-value comparator into the optional equals argument on listenStateSelect and remove the redundant listenStateSelectWithEquals API and binding capability.
  • Remove the redundant ViewModel.resetForTesting() alias. ViewModel.reset() directly performs the complete test runtime reset.

1.0.6 #

  • Make ViewModel.update notify synchronously for synchronous blocks and notify after successful asynchronous completion; failed blocks no longer notify.
  • Add relationship-preserving recreate. Clarify that recycle is a dangerous global operation that removes all owners and force-disposes the shared instance, including aliveForever instances. Consumers must re-resolve it through getters that call watch/read on every access after owner updates, instead of retaining the disposed object in a field. The optional recreate capability keeps existing direct ViewModelBindingInterface implementations source-compatible.
  • Warn in debug builds when different specs resolve the same ViewModel type and effective key in one binding, because the later builder is not part of instance identity and will not run.
  • Add ViewModel.resetForTesting() for complete runtime isolation, including force-disposal of cached retained instances and reset of configuration, lifecycle observers, and DevTools tracking state.
  • Keep listenStateSelect source-compatible with its == default and add listenStateSelectWithEquals for strongly typed, per-listener equality. The binding-side comparator is also an optional capability for direct custom interface implementations. notifyListeners() continues to refresh only broad listeners without replaying state diffs.
  • Keep StateViewModel callbacks synchronous while preserving state-stream transition order during reentrant updates; a synchronous callback may also dispose the state store safely.
  • Add StateViewModelSelector<T, R> as a strongly typed, single-selector widget while retaining StateViewModelValueWatcher for compatibility.
  • Add scoped spec overrides with idempotent overrideWith restore callbacks and async-Zone-isolated runWithOverride, including safe concurrent, nested, and out-of-order restore.
  • Extend DevTools diagnostics with ordered active owners, the current primary owner used for nested dependency resolution, and primary-owner handoff metadata.

1.0.5 #

  • Document getter-based ViewModel dependencies and shared-parent lifecycle boundaries.
  • Show all package versions and Codecov coverage in the README status table.

1.0.4 #

  • Refactor AutoDisposeInstanceController.getInstancesByTag to drop the unused listen parameter; recreate-listener attachment is now unconditional, matching the documented semantics of watchCachesByTag / readCachesByTag.

1.0.3 #

  • Extend ErrorType enum with lifecycle and pauseResume for finer-grained error classification.
  • Unify all error handling through reportViewModelError with secondary try-catch to prevent onError callback failures from breaking dispose chains.
  • Fix silent error swallowing when isLoggingEnabled is false — errors are now always reported via debugPrint.
  • Fix AutoDisposeInstanceController._attachRecreateListener not using unified error path.
  • Add handlerStack capture in reportViewModelError secondary catch for better diagnostics.

1.0.2 #

  • Fix listener mutation safety during notification dispatch.
  • Fix recreate failure handling to keep previous instance valid.
  • Fix disposal cleanup in instance manager and pause providers.
  • Improve cached access error handling and DevTools active state reporting.
  • Internal cleanup: remove duplicated binding extension implementations.

1.0.1 #

  • Fix: DevToolsService not clearing singleton _instance on dispose
  • Fix: typo "suucess" → "success" in DevToolsService log
  • Fix: replace deprecated TickerMode.getNotifier with TickerMode.of in ViewModelStateMixin
  • Docs: add "view_model vs riverpod" comparison section (EN & ZH)
  • Style: code formatting for 80-char line width compliance
  • Fix: add const constructors in devtools extension widgets

1.0.0 #

  • Breaking: API Standardization:
    • Renamed Vef to ViewModelBinding for clarity. (Deprecated typedef Vef = ViewModelBinding remains for compatibility).
    • Renamed ViewModelProvider to ViewModelSpec. (Deprecated ViewModelProvider wrapper remains).
    • Renamed vef getter to viewModelBinding. (Deprecated vef getter remains).
    • Renamed annotations @GenProvider / @genProvider to @GenSpec / @genSpec.
  • Breaking: Notification System Refactoring:
    • Standardized all notifications to be synchronous.
    • Performance optimization: notifications are now delivered in two phases (direct state listeners first, then binding updates).
  • Feat: DevTools Evolution:
    • Deep redesign of the UI with a professional layout and high-performance scrolling.
    • Added Dependency Graph Visualization to see real-time connections between widgets and ViewModels.
    • Enhanced data loading resilience and refined object serialization for the dev console.
  • Feat: Advanced State Management:
    • Added support for per-ViewModel state equality configuration via instance-level equals function.
    • Priority logic: instance equals > global ViewModelConfig.equals > identical().
  • Compatibility:
    • Restored support for deprecated ViewModelFactory.singleton() callback to ease migration of legacy sharing logic to the key system.
    • Full backward compatibility for vef variable mapping to viewModelBinding across all mixins.
  • Generator:
    • Prioritizes the spec factory method (e.g., MyViewModel.spec()) over unnamed constructors for more flexible DI.
  • Docs:
    • New localized Chinese README (README_ZH.md) and comprehensive architecture guides.

0.14.2 #

  • Docs: Polish README and README_ZH for better clarity and conciseness
  • Docs: Remove emojis and simplify feature descriptions for a more professional look
  • Docs: Streamline configuration and usage examples

0.14.1 #

  • Docs: Refine "Why view_model?" section with "The 'Pain-Free' Alternative to Riverpod" focus
  • Docs: Add team background and hybrid team MVVM story to clarify design philosophy
  • Docs: Explicitly contrast with Riverpod's boilerplate (No Root Wrapping, No Forced Inheritance, etc.)

0.14.0 #

  • Docs: Add design philosophy section emphasizing Flutter-native, class-oriented approach
  • Docs: Update README tagline to highlight Flutter-native style

0.14.0-dev.1 #

  • Revert: remove _dependencyListeners and associated dependency tracking methods

0.14.0-dev.0 #

  • Simplify Architecture Guide structure in ARCHITECTURE_GUIDE.md and ARCHITECTURE_GUIDE_ZH.md
  • Update ViewModelStateMixin docs
  • Fix bugs

0.13.0 #

  • Support aliveForever
  • Fix: resolve unbindVef race condition
  • Update lints

0.12.0 #

  • update docs

0.11.0 #

  • Add: test-time proxy overrides for specs
    • ViewModelSpecWithArg/Arg2/Arg3/Arg4 now support setProxy and clearProxy to override builder, key, tag during tests.
    • Mirrors existing no-arg ViewModelSpec proxy behavior for consistency.

0.10.0 #

  • Feat: Introduce methods to retrieve and watch multiple ViewModel instances by tag (readCachesByTag, watchCachesByTag)

0.9.2 #

  • add more tests

0.9.1 #

  • update dependencies

0.9.0 #

🎉 Major Update: Introducing ViewModelSpec & Code Generator

🚀 ViewModelSpec: Simpler, Cleaner, Better Replaces the verbose Factory pattern with a declarative, type-safe provider system.

Before (Factory pattern):

class CounterViewModelFactory extends ViewModelFactory<CounterViewModel> {
  @override
  CounterViewModel build() => CounterViewModel();
}

final vm = watchViewModel(factory: CounterViewModelFactory());

After (Spec pattern):

/// auto generated provider for CounterViewModel
final counterSpec = ViewModelSpec<CounterViewModel>(
  builder: () => CounterViewModel(),
);

final vm = viewModelBinding.watch(counterSpec);

With Arguments:

/// auto generated provider for UserViewModel
final userSpec = ViewModelSpec.arg<UserViewModel, String>(
  builder: (userId) => UserViewModel(userId),
  key: (userId) => 'user-$userId',
);

final vm = viewModelBinding.watch(userSpec('user-123'));

Migration Guide


🤖 Code Generator: Zero Boilerplate #

Introducing view_model_generator - automatically generate ViewModelSpec definitions from annotations.

Installation:

dependencies:
  view_model: ^latest
dev_dependencies:
  build_runner: ^latest
  view_model_generator: ^latest

Usage:

import 'package:view_model_generator/view_model_generator.dart';

part 'counter_view_model.vm.dart';

@genProvider
class CounterViewModel extends ViewModel {
  int count = 0;
  void increment() => update(() => count++);
}

Run generator:

dart run build_runner build

Generated code:

// counter_view_model.vm.dart
final counterSpec = ViewModelSpec<CounterViewModel>(
  builder: () => CounterViewModel(),
);

The generator supports ViewModels with up to 4 constructor parameters and automatically generates the appropriate ViewModelSpec.argX variant.

Package: https://pub.dev/packages/view_model_generator


🔄 New Unified API: viewModelBinding.watch & viewModelBinding.read #

Everything is ViewModelBinding - A unified, consistent API for accessing ViewModels.

New Recommended API:

// Watch (reactive)
final vm = viewModelBinding.watch(counterSpec);

// Read (non-reactive)
final vm = viewModelBinding.read(counterSpec);

// Watch cached by key/tag
final vm = viewModelBinding.watchCached<UserViewModel>(key: 'user-123');
final vm = viewModelBinding.readCached<UserViewModel>(tag: 'current-user');

Legacy API (still supported):

// Old API still works for backward compatibility
final vm = watchViewModel(factory: CounterViewModelFactory());
final vm = readViewModel(factory: CounterViewModelFactory());

Note: While watchViewModel and readViewModel are still supported, we recommend migrating to the new viewModelBinding.watch and viewModelBinding.read API with ViewModelSpec for better type safety and less boilerplate.


🌟 Everything is ViewModelBinding #

ViewModelBinding is the core abstraction of the view_model library, responsible for managing ViewModel lifecycle and dependency injection. WidgetMixin is essentially just a wrapper around WidgetViewModelBinding.

This means you can use ViewModel in any Dart class, independent of Widgets.

Custom ViewModelBinding Example:

class StartTaskBinding with ViewModelBinding {
  Future<void> runStartupTasks() async {
    final authVM = read(authSpec);
    await authVM.checkLoginStatus();
    
    final configVM = read(configSpec);
    await configVM.loadRemoteConfig();
  }
  
  @override
  void dispose() {
    super.dispose();
    // Clean up all watched ViewModels
  }
}

Use cases:

  • Pure Dart Tests: Test ViewModel interactions without testWidgets
  • Startup Tasks: Execute initialization logic before any Widget is rendered

See Custom ViewModelBinding Documentation for details.


🔧 Other Changes #

  • Renamed ViewModelPauseProvider to ViewModelBindingPauseProvider for consistency
  • Improved type inference for ViewModelSpec.argX variants

0.8.4 #

  • Update docs about design philosophy

Everything is ViewModel #

We redefine the "ViewModel" not as a specific MVVM component, but as a Specialized Manager Container equipped with lifecycle awareness.

1. Widget-Centric Architecture In a Flutter App, every action revolves around Pages and Widgets. No matter how complex the logic is, the ultimate consumer is always a Widget. Therefore, binding the Manager's lifecycle directly to the Widget tree is the most logical and natural approach.

2. One Concept, Flexible Scopes You don't need to distinguish between "Services", "Controllers", or "Stores". It's all just a ViewModel. The difference is only where you attach it:

  • Global: Attach to the top-level AppMain. It lives as long as the App (Singleton).
  • Local: Attach to a Page. It follows the page's lifecycle automatically.
  • Shared: Use a unique key (e.g., ProductID) to share the exact same instance across different Widgets.

3. Seamless Composition & Decoupling ViewModels can directly depend on and read other ViewModels internally (e.g., a UserVM reading a NetworkVM). However, the ViewModel itself remains Widget-Agnostic—it holds state and logic but does not know about the Widget's existence or hold a BuildContext.

4. Out-of-the-Box Simplicity Compared to GetIt (which requires manual binding glue code) or Riverpod (which involves complex graph concepts), this approach is strictly pragmatic. It provides automated lifecycle management and dependency injection immediately, with zero boilerplate.

0.8.3 #

  • Fix docs

0.8.2 #

  • Update docs

0.8.1 #

  • Fix: Custom VefPauseProvider was not working properly when added late, causing pause to fail.

0.8.0 #

  • BREAKING CHANGE: Reworked the ViewModelBinding pause/resume lifecycle to a more robust and extensible provider-based architecture.
    • Default providers PageRoutePauseProvider, TickerModePauseProvider and AppPauseProvider handle automatic pausing for route and app app lifecycle events and tickMode.
    • Added ManualVefPauseProvider for easy manual control in custom UI scenarios (e.g., TabBarView).
    • For details on the new API and migration, see the Pause/Resume Lifecycle Documentation.
// register [ViewModel.routeObserver] to navigatorObservers.
class App {
  Widget build(context) {
    return MaterialApp(
      navigatorObservers: [ViewModel.routeObserver],
      // ... other properties
    );
  }
}


  • Fix Devtool
  • Added support for ViewModelStatelessMixin on StatelessWidget. but prefer using ViewModelStateMixin on StatefulWidget.
class MyWidget extends StatelessWidget with ViewModelStatelessMixin {
  late final viewModel = watchViewModel<MyViewModel>(
      factory: MyViewModelFactory(),
    );
  const MyWidget({super.key});

  @override
  Widget build(BuildContext context) {
    return Text('Hello World ${viewModel}');
  }
}
  • Add StateViewModelValueWatcher to watch value changes on StateViewModel.
class MyWidget extends State with ViewModelStateMixin {
  const MyWidget({super.key});

  late final MyViewModel stateViewModel;

  @override
  void initState() {
    super.initState();
    stateViewModel = readViewModel<MyViewModel>(
      factory: MyViewModelFactory(),
    );
  }

  @override
  Widget build(BuildContext context) {
    // Watch value changes on `stateViewModel` and rebuild only when `name` or `age` changes.
    return StateViewModelValueWatcher<MyViewModel>(
      stateViewModel: stateViewModel,
      selectors: [(state) => state.name, (state) => state.age],
      builder: (state) {
        return Text('Name: \${state.name}, Age: \${state.age}');
      },
    );
  }
}

0.7.0 #

RouteAware Auto Pause (delay rebuilds when page is paused) #

  • can manually control pause/resume via viewModelVisibleListeners exposed by ViewModelStateMixin. Call viewModelVisibleListeners.onPause() when the page is covered, and viewModelVisibleListeners.onResume() when it becomes visible again. Wire these methods to your own RouteObserver or any visibility mechanism.

Example:

class _MyPageState extends State<MyPage> with ViewModelStateMixin<MyPage>, RouteAware {
  void didPushNext() {
    viewModelVisibleListeners.onPause();
  }

  void didPopNext() {
    viewModelVisibleListeners.onResume(); // triggers one refresh
  }
}

  • BreakingChange: Rename ViewModelWatcher to ViewModelBuilder
  • Add ViewModel#update, we often forget calling notifylistenr()
    await update(() async {
      await repository.save(data);
       _counter++;
    });
    
  • Add StateViewModel#listenStateSelect to listen value diff.

0.6.0 #

  • Add ViewModelBuilder and CachedViewModelBuilder widgets for binding and listening without mixing in ViewModelStateMixin; Naming: use shareKey in CachedViewModelBuilder to avoid confusion with widget Key.
// Example: Using ViewModelBuilder without mixing ViewModelStateMixin
ViewModelBuilder<MySimpleViewModel>
(
factory: MySimpleViewModelFactory(),
builder: (vm) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(vm.message),
const SizedBox(height: 8),
ElevatedButton(
onPressed: () => vm.updateMessage("Message Updated!"),
child: const Text('Update Message'),
),
],
);
},
)
// Example: Using CachedViewModelBuilder to bind to an existing instance
CachedViewModelBuilder<MySimpleViewModel>
(
shareKey: "shared-key", // or: tag: "shared-tag"
builder: (vm) {
return Row(
children: [
Expanded(child: Text(vm.message)),
IconButton(
onPressed: () => vm.incrementCounter(),
icon: const Icon(Icons.add),
),
],
);
},
)

0.5.0 #

  • Breaking Change & API Refinement: Major overhaul of ViewModel access methods to clarify responsibilities and improve predictability.
    • watchViewModel / readViewModel:
      • Now primarily responsible for creating new ViewModel instances.
      • The factory parameter is now mandatory.
      • Behavior depends on the provided factory:
        • If the factory includes a key, the instance is created and cached (or retrieved if already cached).
        • If the factory has no key, a new, non-shared instance is created every time.
    • New Methods for Cached Access:
      • Introduced watchCachedViewModel and readCachedViewModel to explicitly find existing, cached ViewModel instances by key or tag.
      • Introduced maybeWatchCachedViewModel and maybeReadCachedViewModel for safely accessing cached instances without throwing errors if not found.
    • Migration Guide:
      • To create/watch a new instance: Continue using watchViewModel but you must provide a factory.
      • To find an existing instance: Replace watchViewModel(key: ...) with watchCachedViewModel(key: ...) or readCachedViewModel(key: ...).
  • support ViewModel-to-ViewModel Access
  • Breaking change: The key parameter in watchViewModel, readViewModel, and ViewModel.read has been changed from String? to Object?. This allows for the use of custom objects as keys, but requires proper implementation of == and hashCode for custom key objects.

ViewModels can access other ViewModels using readViewModel and watchViewModel:

  • readViewModel: Access another ViewModel without reactive connection
  • watchViewModel: Create reactive dependency - automatically notifies when the watched ViewModel changes

When a ViewModel (the HostVM ) accesses another ViewModel (the SubVM ) via watchViewModel , the framework automatically binds the SubVM 's lifecycle to the HostVM 's UI observer (i.e., the State object of the StatefulWidget ).

This means both the SubVM and the HostVM are directly managed by the lifecycle of the same State object. When this State object is disposed, if neither the SubVM nor the HostVM has other observers, they will be disposed of together automatically.

This mechanism ensures clear dependency relationships between ViewModels and enables efficient, automatic resource management.

class UserProfileViewModel extends ViewModel {
  void loadData() {
    // One-time access without listening
    final authVM = watchCachedViewModel<AuthViewModel>();
    if (authVM?.isLoggedIn == true) {
      _fetchProfile(authVM!.userId);
    }
  }

  void setupReactiveAuth() {
    // Reactive access - auto-updates when auth changes
    final authVM = watchCachedViewModel<AuthViewModel>();
    // This ViewModel will be notified when authVM changes
  }


  void manualListening() {
    final authVM = watchCachedViewModel<AuthViewModel>();
    // You can also manually listen to any ViewModel
    authVM?.listen(() {
      // Custom listener logic
      _handleAuthChange(authVM);
    });
  }
}

0.4.7 #

  • fix ViewModel.read

0.4.6 #

  • The view_model package includes a powerful DevTools extension that provides real-time monitoring and debugging capabilities for your ViewModels during development.
  • create devtools_options.yaml in root directory of project.
description: This file stores settings for Dart & Flutter DevTools.
documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
extensions:
  - view_model: true

  • Breaking change: rename initConfig to initialize

0.4.5 #

  • Add ViewModelSpec for convenient and generic ViewModel factory creation.

0.4.4 #

  • Add ViewModel.maybeRead

0.4.3 #

  • Add maybeWatchViewModel and maybeReadViewModel
  • update watchViewModel find logic
VM watchViewModel<VM extends ViewModel>({
  ViewModelFactory<VM>? factory,
  Object? key,
  Object? tag,
});
Parameter Name Type Optional Description
factory ViewModelFactory<VM>? Provides the construction method for the ViewModel. Optional; if an
existing instance is not found in the cache, it will be used to create a new
one.
key String? Specifies a unique key to support sharing the same ViewModel instance.
First, it tries to find an instance with the same key in the cache.
tag Object? Add a tag for ViewModel instance. get tag by viewModel.tag. and it's
used by find ViewModel by watchViewModel(tag:tag).

🔍 Lookup Logic Priority (Important) The internal lookup and creation logic of watchViewModel is as follows (executed in priority order):

  1. If a key is passed in:
  • First, attempt to find an instance with the same key in the cache.
  • If a factory exists, use the factory to get a new instance.
  • If no factory is found and no instance is found, an error will be thrown.
  1. If a tag is passed in, attempt to find the latest created instance which has the same tag in the cache.
  2. If nothing passed in, attempt to find the latest created instance of this type in the cache.

⚠️ If no ViewModel instance of the specified type is found, an error will be thrown. Ensure that the ViewModel has been correctly created and registered before use.

0.4.2 #

  • Support find existing ViewModel by tag

set tag in ViewModelFactory.tag():

class MyViewModelFactory extends ViewModelFactory<MyViewModel> {

  @override
  Object? tag() {
    return 'tag';
  }
}

find existing ViewModel by tag:

late final MyViewModel viewModel;

@override
void initState() {
  super.initState();
  viewModel = watchViewModel<MyViewModel>(tag: 'tag');
}

0.4.1 #

Breaking change:

  • Use recycleViewModel instead of refreshViewModel.

0.4.0 #

Breaking change:

  • Use ViewModel instead of StatelessViewModel.

  • Use StateViewModel instead of ViewModel.

  • Use either watchViewModel or readViewModel instead of getViewModel/ requireExistingViewModel.

  • Use StateViewModel.listenState instead of ViewModel.listen.

  • Use ViewModel.listen instead of ViewModel.addListener.

  • Support ViewModel.read<T> to read existing view model globally.

0.3.0 #

12
likes
160
points
1.48k
downloads

Documentation

API reference

Publisher

verified publisherpub.lwjlol.com

Weekly Downloads

Everything is ViewModel. Enjoy automatic lifecycle management, prevent memory leaks, and share state effortlessly. Simple, lightweight, and powerful.

Repository (GitHub)
View/report issues

Topics

#state-management #view-model #caching #dependency-injection #vm

License

MIT (license)

Dependencies

flutter, meta, stack_trace, view_model_annotation

More

Packages that depend on view_model