mvvm_core 1.1.1 copy "mvvm_core: ^1.1.1" to clipboard
mvvm_core: ^1.1.1 copied to clipboard

Simple yet powerful MVVM state management for Flutter. Reactive properties, async handling, collections, and DevTools integration.

example/example.dart

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:mvvm_core/mvvm_core.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'MVVM Core Example',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: const HomePage(),
    );
  }
}

// ============================================================================
// Home Page - Navigation to Examples
// ============================================================================

class HomePage extends StatelessWidget {
  const HomePage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('MVVM Core Examples'),
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
      ),
      body: ListView(
        children: [
          _ExampleTile(
            title: 'Counter',
            subtitle: 'Basic Reactive usage',
            icon: Icons.add_circle_outline,
            onTap: () => _navigate(context, const CounterView()),
          ),
          _ExampleTile(
            title: 'Todo List',
            subtitle: 'ReactiveList with CRUD operations',
            icon: Icons.checklist,
            onTap: () => _navigate(context, const TodoView()),
          ),
          _ExampleTile(
            title: 'Async Data',
            subtitle: 'ReactiveFuture with loading states',
            icon: Icons.cloud_download,
            onTap: () => _navigate(context, const UserView()),
          ),
        ],
      ),
    );
  }

  void _navigate(BuildContext context, Widget page) {
    Navigator.push(context, MaterialPageRoute(builder: (_) => page));
  }
}

class _ExampleTile extends StatelessWidget {
  const _ExampleTile({
    required this.title,
    required this.subtitle,
    required this.icon,
    required this.onTap,
  });

  final String title;
  final String subtitle;
  final IconData icon;
  final VoidCallback onTap;

  @override
  Widget build(BuildContext context) {
    return ListTile(
      leading: Icon(icon),
      title: Text(title),
      subtitle: Text(subtitle),
      trailing: const Icon(Icons.chevron_right),
      onTap: onTap,
    );
  }
}

// ============================================================================
// Example 1: Counter - Basic Reactive Usage
// ============================================================================

class CounterViewModel extends ViewModel {
  final Reactive<int> count = Reactive<int>(0);

  void increment() => count.value++;
  void decrement() => count.value--;
  void reset() => count.value = 0;

  @override
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
    properties.add(DiagnosticsProperty('count', count));
  }
}

class CounterView extends ViewHandler<CounterViewModel> {
  const CounterView({super.key});

  @override
  CounterViewModel viewModelFactory() => CounterViewModel();

  @override
  Widget build(
    BuildContext context,
    CounterViewModel viewModel,
    Widget? child,
  ) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Counter Example'),
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        actions: [
          IconButton(
            icon: const Icon(Icons.refresh),
            onPressed: viewModel.reset,
            tooltip: 'Reset',
          ),
        ],
      ),
      body: Center(
        child: viewModel.count.listen(
          builder: (context, count, _) => Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              const Text('You have pushed the button this many times:'),
              const SizedBox(height: 16),
              Text('$count', style: Theme.of(context).textTheme.displayLarge),
            ],
          ),
        ),
      ),
      floatingActionButton: Column(
        mainAxisSize: MainAxisSize.min,
        children: [
          FloatingActionButton(
            heroTag: 'increment',
            onPressed: viewModel.increment,
            tooltip: 'Increment',
            child: const Icon(Icons.add),
          ),
          const SizedBox(height: 8),
          FloatingActionButton(
            heroTag: 'decrement',
            onPressed: viewModel.decrement,
            tooltip: 'Decrement',
            child: const Icon(Icons.remove),
          ),
        ],
      ),
    );
  }
}

// ============================================================================
// Example 2: Todo List - ReactiveList Usage
// ============================================================================

class Todo {
  Todo({required this.title, this.completed = false});

  final String title;
  final bool completed;

  Todo copyWith({String? title, bool? completed}) {
    return Todo(
      title: title ?? this.title,
      completed: completed ?? this.completed,
    );
  }

  @override
  String toString() => 'Todo(title: $title, completed: $completed)';
}

class TodoViewModel extends ViewModel {
  final ReactiveList<Todo> todos = ReactiveList<Todo>([
    Todo(title: 'Learn MVVM Core'),
    Todo(title: 'Build an awesome app'),
    Todo(title: 'Share with the community'),
  ]);

  final Reactive<String> newTodoText = Reactive<String>('');

  int get completedCount => todos.where((t) => t.completed).length;
  int get totalCount => todos.length;

  void addTodo() {
    if (newTodoText.value.trim().isEmpty) return;
    todos.add(Todo(title: newTodoText.value.trim()));
    newTodoText.value = '';
  }

  void toggleTodo(int index) {
    todos[index] = todos[index].copyWith(completed: !todos[index].completed);
  }

  void deleteTodo(int index) {
    todos.removeAt(index);
  }

  void clearCompleted() {
    todos.removeWhere((todo) => todo.completed);
  }

  @override
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
    properties.add(DiagnosticsProperty('todos', todos));
    properties.add(IntProperty('completedCount', completedCount));
  }
}

class TodoView extends ViewHandler<TodoViewModel> {
  const TodoView({super.key});

  @override
  TodoViewModel viewModelFactory() => TodoViewModel();

  @override
  Widget build(BuildContext context, TodoViewModel viewModel, Widget? child) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Todo List'),
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        actions: [
          // Only rebuild this part when todos change
          viewModel.todos.select(
            selector: (todos) => todos.any((t) => t.completed),
            builder: (context, hasCompleted) => IconButton(
              icon: const Icon(Icons.delete_sweep),
              onPressed: hasCompleted ? viewModel.clearCompleted : null,
              tooltip: 'Clear completed',
            ),
          ),
        ],
      ),
      body: Column(
        children: [
          // Input field
          Padding(
            padding: const EdgeInsets.all(16),
            child: Row(
              children: [
                Expanded(
                  child: TextField(
                    decoration: const InputDecoration(
                      hintText: 'Add a new todo...',
                      border: OutlineInputBorder(),
                    ),
                    onChanged: (value) => viewModel.newTodoText.value = value,
                    onSubmitted: (_) => viewModel.addTodo(),
                  ),
                ),
                const SizedBox(width: 8),
                viewModel.newTodoText.listen(
                  builder: (context, text, _) => IconButton.filled(
                    icon: const Icon(Icons.add),
                    onPressed: text.trim().isNotEmpty
                        ? viewModel.addTodo
                        : null,
                  ),
                ),
              ],
            ),
          ),

          // Stats bar
          viewModel.todos.listen(
            builder: (context, todos, _) => Container(
              padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
              color: Theme.of(context).colorScheme.surfaceContainerHighest,
              child: Row(
                mainAxisAlignment: MainAxisAlignment.spaceBetween,
                children: [
                  Text(
                    '${viewModel.completedCount}/${viewModel.totalCount} completed',
                  ),
                  if (todos.isEmpty)
                    const Text('Add your first todo!')
                  else if (viewModel.completedCount == viewModel.totalCount)
                    const Text('🎉 All done!'),
                ],
              ),
            ),
          ),

          // Todo list
          Expanded(
            child: viewModel.todos.listen(
              builder: (context, todos, _) => todos.isEmpty
                  ? const Center(child: Text('No todos yet'))
                  : ListView.builder(
                      itemCount: todos.length,
                      itemBuilder: (context, index) {
                        final todo = todos[index];
                        return ListTile(
                          leading: Checkbox(
                            value: todo.completed,
                            onChanged: (_) => viewModel.toggleTodo(index),
                          ),
                          title: Text(
                            todo.title,
                            style: TextStyle(
                              decoration: todo.completed
                                  ? TextDecoration.lineThrough
                                  : null,
                            ),
                          ),
                          trailing: IconButton(
                            icon: const Icon(Icons.delete_outline),
                            onPressed: () => viewModel.deleteTodo(index),
                          ),
                        );
                      },
                    ),
            ),
          ),
        ],
      ),
    );
  }
}

// ============================================================================
// Example 3: Async Data - ReactiveFuture Usage
// ============================================================================

class User {
  User({required this.id, required this.name, required this.email});

  final int id;
  final String name;
  final String email;

  @override
  String toString() => 'User(id: $id, name: $name, email: $email)';
}

class UserViewModel extends ViewModel {
  final ReactiveFuture<User> user = ReactiveFuture<User>.idle();

  @override
  void init(BuildContext context) {
    super.init(context);
    loadUser();
  }

  Future<void> loadUser() async {
    await user.run(() async {
      // Simulate network delay
      await Future.delayed(const Duration(seconds: 2));

      // Simulate random success/failure
      if (DateTime.now().millisecond % 5 == 0) {
        throw Exception('Network error');
      }

      return User(id: 1, name: 'John Doe', email: 'john.doe@example.com');
    });
  }

  @override
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
    properties.add(DiagnosticsProperty('user', user));
  }
}

class UserView extends ViewHandler<UserViewModel> {
  const UserView({super.key});

  @override
  UserViewModel viewModelFactory() => UserViewModel();

  @override
  Widget build(BuildContext context, UserViewModel viewModel, Widget? child) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Async Data'),
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        actions: [
          IconButton(
            icon: const Icon(Icons.refresh),
            onPressed: viewModel.loadUser,
            tooltip: 'Reload',
          ),
        ],
      ),
      body: Center(
        child: viewModel.user.listenWhen(
          idle: () => ElevatedButton(
            onPressed: viewModel.loadUser,
            child: const Text('Load User'),
          ),
          loading: () => const Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              CircularProgressIndicator(),
              SizedBox(height: 16),
              Text('Loading user...'),
            ],
          ),
          data: (user) => Card(
            margin: const EdgeInsets.all(16),
            child: Padding(
              padding: const EdgeInsets.all(24),
              child: Column(
                mainAxisSize: MainAxisSize.min,
                children: [
                  CircleAvatar(
                    radius: 40,
                    child: Text(
                      user.name[0],
                      style: Theme.of(context).textTheme.headlineLarge,
                    ),
                  ),
                  const SizedBox(height: 16),
                  Text(
                    user.name,
                    style: Theme.of(context).textTheme.headlineSmall,
                  ),
                  const SizedBox(height: 8),
                  Text(
                    user.email,
                    style: Theme.of(context).textTheme.bodyLarge?.copyWith(
                      color: Theme.of(context).colorScheme.secondary,
                    ),
                  ),
                  const SizedBox(height: 16),
                  Text('User ID: ${user.id}'),
                ],
              ),
            ),
          ),
          error: (error, _) => Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              Icon(
                Icons.error_outline,
                size: 64,
                color: Theme.of(context).colorScheme.error,
              ),
              const SizedBox(height: 16),
              Text(
                'Failed to load user',
                style: Theme.of(context).textTheme.headlineSmall,
              ),
              const SizedBox(height: 8),
              Text(error.toString()),
              const SizedBox(height: 24),
              ElevatedButton.icon(
                onPressed: viewModel.loadUser,
                icon: const Icon(Icons.refresh),
                label: const Text('Try Again'),
              ),
            ],
          ),
        ),
      ),
    );
  }
}
3
likes
160
points
19
downloads

Documentation

API reference

Publisher

verified publisherrutvik.dev

Weekly Downloads

Simple yet powerful MVVM state management for Flutter. Reactive properties, async handling, collections, and DevTools integration.

Repository (GitHub)
View/report issues

Topics

#state-management #mvvm #reactive #viewmodel #architecture

License

MIT (license)

Dependencies

flutter

More

Packages that depend on mvvm_core