flutter-state-management

Implement MVVM and Unidirectional Data Flow patterns for Flutter state management. Provides decision logic to differentiate between ephemeral state (local UI state via setState ) and app state (shared state via MVVM and provider package) Enforces strict separation of concerns: data layer (Repository as Single Source of Truth), logic layer (ViewModel extending ChangeNotifier ), and UI layer (pure functions of immutable state) Includes complete code examples for implementing the MVVM pattern with ChangeNotifier , provider injection, and Consumer widgets for targeted rebuilds Establishes Unidirectional Data Flow constraints: data flows down from Repository through ViewModel to View; events flow up through ViewModel commands

INSTALLATION
npx skills add https://github.com/flutter/skills --skill flutter-state-management
Run in your project or agent environment. Adjust flags if your CLI version differs.

SKILL.md

$2a

class LocalStateWidget extends StatefulWidget {

  const LocalStateWidget({super.key});

  @override

  State<LocalStateWidget> createState() => _LocalStateWidgetState();

}

class _LocalStateWidgetState extends State<LocalStateWidget> {

  bool _isToggled = false;

  void _handleToggle() {

    // Validate-and-Fix: Ensure setState wraps the mutation.

    setState(() {

      _isToggled = !_isToggled;

    });

  }

  @override

  Widget build(BuildContext context) {

    return Switch(

      value: _isToggled,

      onChanged: (value) => _handleToggle(),

    );

  }

}

3. Implement App State using MVVM and UDF

For shared state, implement the MVVM pattern enforcing Unidirectional Data Flow (UDF).

A. Create the Model (Data Layer / SSOT)

Handle low-level tasks (HTTP, caching) in a Repository class.

class UserRepository {

  Future<User> fetchUser(String id) async {

    // Implementation for fetching user data

  }

}

B. Create the ViewModel (Logic Layer)

Extend ChangeNotifier. The ViewModel converts app data into UI state and exposes commands (methods) for the View to invoke.

class UserViewModel extends ChangeNotifier {

  UserViewModel({required this.userRepository});

  final UserRepository userRepository;

  User? _user;

  User? get user => _user;

  bool _isLoading = false;

  bool get isLoading => _isLoading;

  String? _errorMessage;

  String? get errorMessage => _errorMessage;

  // Command invoked by the UI

  Future<void> loadUser(String id) async {

    _isLoading = true;

    _errorMessage = null;

    notifyListeners(); // Trigger loading UI

    try {

      _user = await userRepository.fetchUser(id);

    } catch (e) {

      _errorMessage = e.toString();

    } finally {

      _isLoading = false;

      notifyListeners(); // Trigger success/error UI

    }

  }

}

4. Provide State to the Widget Tree

Use the provider package to inject the ViewModel into the widget tree above the widgets that require access to it.

void main() {

  runApp(

    MultiProvider(

      providers: [

        Provider(create: (_) => UserRepository()),

        ChangeNotifierProvider(

          create: (context) => UserViewModel(

            userRepository: context.read<UserRepository>(),

          ),

        ),

      ],

      child: const MyApp(),

    ),

  );

}

5. Consume State in the View (UI Layer)

Build the UI as a function of the ViewModel's state. Use Consumer to rebuild only the specific parts of the UI that depend on the state.

class UserProfileView extends StatelessWidget {

  const UserProfileView({super.key});

  @override

  Widget build(BuildContext context) {

    return Scaffold(

      body: Consumer<UserViewModel>(

        builder: (context, viewModel, child) {

          if (viewModel.isLoading) {

            return const Center(child: CircularProgressIndicator());

          }

          if (viewModel.errorMessage != null) {

            return Center(child: Text('Error: ${viewModel.errorMessage}'));

          }

          if (viewModel.user != null) {

            return Center(child: Text('Hello, ${viewModel.user!.name}'));

          }

          return const Center(child: Text('No user loaded.'));

        },

      ),

      floatingActionButton: FloatingActionButton(

        // Use listen: false when invoking commands outside the build method

        onPressed: () => context.read<UserViewModel>().loadUser('123'),

        child: const Icon(Icons.refresh),

      ),

    );

  }

}

Validate-and-Fix: Verify that Consumer is placed as deep in the widget tree as possible to prevent unnecessary rebuilds of large widget subtrees.

Constraints

  • No Business Logic in Views: StatelessWidget and StatefulWidget classes must only contain UI, layout, and routing logic. All data transformation and business logic MUST reside in the ViewModel.
  • Strict UDF: Data must flow down (Repository -> ViewModel -> View). Events must flow up (View -> ViewModel -> Repository). Views must never mutate Repository data directly.
  • Single Source of Truth: The Data Layer (Repositories) must be the exclusive owner of data mutation. ViewModels only format and hold the UI representation of this data.
  • Targeted Rebuilds: Never use Provider.of<T>(context) with listen: true at the root of a large build method if only a small child needs the data. Use Consumer<T> or Selector<T, R> to scope rebuilds.
  • Command Invocation: When calling a ViewModel method from an event handler (e.g., onPressed), you MUST use context.read<T>() or Provider.of<T>(context, listen: false).
  • Immutability: Treat data models passed to the UI as immutable. If data changes, the ViewModel must fetch/create a new instance and call notifyListeners().
BrowserAct

Let your agent run on any real-world website

Bypass CAPTCHA & anti-bot for free. Start local, scale to cloud.

Explore BrowserAct Skills →

Stop writing automation&scrapers

Install the CLI. Run your first Skill in 30 seconds. Scale when you're ready.

Start free
free · no credit card