view_model 0.14.0-dev.1
view_model: ^0.14.0-dev.1 copied to clipboard
Everything is ViewModel. Enjoy automatic lifecycle management, prevent memory leaks, and share state effortlessly. Simple, lightweight, and powerful.
✨ view_model: Lightweight Flutter State Management #
Ultra-lightweight (just
with) | Zero intrusion | Say goodbye to BuildContext hellOnly ~6K lines of code, yet transforms your architecture completely 🚀
| Package | Version |
|---|---|
| view_model | |
| view_model_annotation | |
| view_model_generator |
ChangeLog | 中文文档 | Architecture Guide
💡 Why view_model? #
✨ Three Core Strengths #
🪶 Ultra-Lightweight = Zero Overhead
- Minimal footprint: Only ~6K lines of code, 3 dependencies (flutter + meta + stack_trace)
- Zero setup: No root widget wrapping, no mandatory initialization
- On-demand creation: ViewModels are created lazily and disposed automatically
🎯 Minimal Intrusion = Maximum Compatibility
- Just
with: Addwith ViewModelStateMixinto your State—that's it - Drop-in ready: Works with any existing Flutter code, integrate anytime
- Pure Dart mixins: Leverages Dart 3 mixin capabilities, zero inheritance pollution
🌈 Complete Flexibility
- Access anywhere: Use ViewModels in Widgets, Repositories, Services—no
BuildContextneeded - Automatic memory management: Reference counting + auto-disposal means no memory leaks
- Share or isolate: Need a singleton? Add a
key. Need isolation? Don't. It's that simple.
📦 Installation #
dependencies:
view_model: ^latest_version
dev_dependencies:
build_runner: ^latest_version
view_model_generator: ^latest_version # Highly recommended for less boilerplate!
🚀 Get Started in 3 Steps #
Step 1️⃣: Write Your Business Logic #
Just use with ViewModel (yes, it's that simple):
class CounterViewModel with ViewModel {
int count = 0;
void increment() {
update(() => count++); // Automatically notifies UI
}
}
Why with instead of extends?
Dart mixins enable composition over inheritance—more flexible and keeps your class hierarchy clean!
Step 2️⃣: Register a Provider #
final counterProvider = ViewModelProvider<CounterViewModel>(
builder: () => CounterViewModel(),
);
Pro tip: Skip this step entirely by using view_model_generator—just add an annotation and it's auto-generated! 🎉
Step 3️⃣: Use in Your Widget #
Add one mixin, unlock superpowers:
class CounterPage extends StatefulWidget {
@override
State<CounterPage> createState() => _CounterPageState();
}
class _CounterPageState extends State<CounterPage>
with ViewModelStateMixin { // 👈 Just this one line!
@override
Widget build(BuildContext context) {
final vm = vef.watch(counterProvider); // Automatically listens
return Scaffold(
body: Center(child: Text('${vm.count}')),
floatingActionButton: FloatingActionButton(
onPressed: vm.increment,
child: Icon(Icons.add),
),
);
}
}
Intrusion comparison:
| Solution | Changes Required | Root Wrapping | BuildContext Dependency |
|---|---|---|---|
| view_model | ✅ Just add mixin | ❌ No | ❌ No |
| Provider | ⚠️ InheritedWidget | ✅ Yes | ✅ Yes |
| Riverpod | ⚠️ ConsumerWidget | ✅ Yes | ❌ No |
| GetX | ⚠️ Often global state | ❌ No | ❌ No |
🛠️ Core Features #
1️⃣ Universal Access with Vef (Custom Ref) #
What is vef?
Vef = ViewModel Execution Framework. It's a mixin you can add to any class, giving it the power to access ViewModels anywhere!
💡 Fun fact:
ViewModelStateMixinis actually powered byWidgetVefunder the hood—a Flutter-optimized variant ofVef. This ensures a consistent API whether you're in Widgets, ViewModels, or pure Dart classes!
class StartupTaskRunner with Vef {
Future<void> run() async {
final authVM = vef.read(authProvider);
await authVM.checkAuth();
final configVM = vef.read(configProvider);
await configVM.fetchRemoteConfig();
}
@override
void dispose() {
super.dispose();
}
}
#### 📱 In Widgets (Built-in)
```dart
class _MyPageState extends State<MyPage> with ViewModelStateMixin {
@override
Widget build(BuildContext context) {
final vm = vef.watch(myProvider); // Auto-reactive
return Text(vm.data);
}
}
🧠 In ViewModels (Built-in)
ViewModels can coordinate with each other:
class CartViewModel with ViewModel {
void checkout() {
final userVM = vef.read(userProvider); // Direct access to other VMs
processOrder(userVM.user);
}
}
🏗️ In Any Class (Custom Ref)
Need a pure logic manager? Just with Vef:
class StartupTaskRunner with Vef {
Future<void> run() async {
final authVM = vef.read(authProvider);
await authVM.checkAuth();
final configVM = vef.read(configProvider);
await configVM.fetchRemoteConfig();
}
@override
void dispose() {
super.dispose(); // Auto-cleans all dependencies
}
}
🎯 Quick Reference: Vef Methods
| Method | Behavior | Best For |
|---|---|---|
vef.watch(provider) |
Reactive | Inside build()—rebuilds on change |
vef.read(provider) |
Direct access | Callbacks, event handlers, or other ViewModels |
vef.listen(provider) |
Side effects | Navigation, snackbars, etc. |
vef.watchCached(key:) |
Targeted | Access specific shared instance by key |
vef.readCached(key:) |
Targeted | Read specific shared instance without listening |
vef.listenState(provider) |
State Listener | Listen to state changes (previous, current) |
vef.listenStateSelect(provider) |
Selector | Listen to specific state property changes |
Legacy API support: Prefer the classic watchViewModel syntax? Go ahead! It's fully supported and powered by the high-performance vef engine under the hood:
| Legacy Method | Modern Equivalent | Description |
|---|---|---|
watchViewModel |
vef.watch |
Watch + auto-rebuild |
readViewModel |
vef.read |
Direct read, zero overhead |
listenViewModel |
vef.listen |
Listen without rebuild |
watchCachedViewModel |
vef.watchCached |
Watch cached instance |
readCachedViewModel |
vef.readCached |
Read cached instance |
listenViewModelState |
vef.listenState |
Listen to state changes |
listenViewModelStateSelect |
vef.listenStateSelect |
Listen to selected state changes |
2️⃣ Immutable State (StateViewModel) #
For developers who love clean, immutable state! Pairs beautifully with Freezed ✨
class UserViewModel extends StateViewModel<UserState> {
UserViewModel() : super(state: UserState());
void loadUser() async {
setState(state.copyWith(isLoading: true));
// ... fetch data ...
setState(state.copyWith(isLoading: false, name: 'Alice'));
}
}
3️⃣ Dependency Injection (Arguments) #
Real talk: Many Flutter "DI" libraries are actually Service Locators in disguise. True DI requires reflection or powerful meta-programming, but Flutter disables reflection.
We chose to embrace reality—use a clean, explicit argument system:
final userProvider = ViewModelProvider.arg<UserViewModel, int>(
builder: (int id) => UserViewModel(id),
);
// Usage:
final vm = vef.watch(userProvider(42));
Simple, direct, debuggable. No magic tricks.
4️⃣ Instance Sharing (Keys) #
- Isolated by default: Each widget gets its own ViewModel instance
- Shared instances: Add a
key, and widgets with the same key share the same instance
final productProvider = ViewModelProvider.arg<ProductViewModel, String>(
builder: (id) => ProductViewModel(id),
key: (id) => 'prod_$id', // Same ID = shared instance
);
5️⃣ Automatic Lifecycle ♻️ #
Set it and forget it—memory management handled automatically:
- Creation: Auto-created on first
watchorread - Alive: Stays alive as long as any widget is using it
- Disposal: Auto-cleaned when the last user unmounts
Need a global singleton? Add aliveForever: true, perfect for Auth, App Config, etc:
final authProvider = ViewModelProvider(
builder: () => AuthViewModel(),
aliveForever: true, // Never disposed
);
🏗️ Architecture Patterns #
In real-world apps, Repositories and Services can use with ViewModel to coordinate with other ViewModels—no BuildContext passing needed:
class UserRepository with ViewModel {
Future<User> fetchUser() async {
final token = vef.read(authProvider).token; // Direct access
return api.getUser(token);
}
}
For detailed patterns, check out our Architecture Guide
🧪 Testing Made Easy #
Mocking is straightforward—no simulator required:
testWidgets('Displays correct user data', (tester) async {
final mockVM = MockUserViewModel();
userProvider.setProxy(ViewModelProvider(builder: () => mockVM));
await tester.pumpWidget(MyApp());
expect(find.text('Alice'), findsOneWidget);
});
⚙️ Global Configuration #
Configure in main():
void main() {
ViewModel.initialize(
config: ViewModelConfig(
isLoggingEnabled: true,
onListenerError: (error, stack, context) {
// Report to Crashlytics
},
),
);
runApp(MyApp());
}
📊 The Numbers Don't Lie #
| Metric | Value |
|---|---|
| Core codebase | ~6K lines (with comments) |
| Required dependencies | 3 (flutter, meta, stack_trace) |
| Mixins needed | 1 (ViewModelStateMixin) |
| Root widget wrapping | ❌ None |
| Mandatory initialization | ❌ Optional |
| Performance overhead | Minimal (reference counting + Zone) |
📜 License #
MIT License—use it freely! 💖
🎉 Bottom Line #
Tired of:
- ❌ Passing
BuildContexteverywhere - ❌ Complex global state management
- ❌ Memory leaks
- ❌ Invasive code changes
Try view_model! Lightweight, clean, elegant—it'll transform how you build Flutter apps ✨
Remember: Just with, and everything becomes simple!
Built with ❤️ for the Flutter community.