{"id":21260725,"url":"https://github.com/erf/mu_state","last_synced_at":"2026-04-17T06:34:41.442Z","repository":{"id":56834994,"uuid":"469905721","full_name":"erf/mu_state","owner":"erf","description":"Minimal Cubit-inspired state management using Flutter's built-in primitives.","archived":false,"fork":false,"pushed_at":"2025-07-02T00:18:17.000Z","size":1157,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2026-03-29T21:06:09.959Z","etag":null,"topics":["cubit","dart","flutter","flutter-state-management","state","state-management","valuelistenablebuilder","valuenotifier"],"latest_commit_sha":null,"homepage":"https://pub.dev/packages/mu_state","language":"Dart","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/erf.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2022-03-14T21:09:23.000Z","updated_at":"2025-08-22T20:17:24.000Z","dependencies_parsed_at":"2023-12-06T12:39:38.731Z","dependency_job_id":"e313fd50-9d18-4114-8a24-b0be15f1653f","html_url":"https://github.com/erf/mu_state","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/erf/mu_state","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/erf%2Fmu_state","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/erf%2Fmu_state/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/erf%2Fmu_state/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/erf%2Fmu_state/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/erf","download_url":"https://codeload.github.com/erf/mu_state/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/erf%2Fmu_state/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31918742,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-16T18:22:33.417Z","status":"online","status_checked_at":"2026-04-17T02:00:06.879Z","response_time":62,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["cubit","dart","flutter","flutter-state-management","state","state-management","valuelistenablebuilder","valuenotifier"],"created_at":"2024-11-21T04:20:21.266Z","updated_at":"2026-04-17T06:34:41.430Z","avatar_url":"https://github.com/erf.png","language":"Dart","funding_links":[],"categories":[],"sub_categories":[],"readme":"# mu_state\n\nMinimal Cubit-inspired state management using Flutter's built-in primitives. No external dependencies.\n\n\u003e See my article [From ValueNotifier to Cubit-inspired state management](https://medium.com/@erlendf/from-valuenotifier-to-cubit-inspired-a-pragmatic-evolution-80eea6dba605) for more info\n\n## Overview\n\n`mu_state` provides a lightweight alternative to Bloc/Cubit using Flutter's built-in `ValueNotifier`, `ValueListenableBuilder` and `InheritedWidget`. It follows the same patterns you know from Bloc but with zero dependencies and minimal boilerplate.\n\n**Key concepts:**\n\n- **`MuLogic\u003cS\u003e`** - Your business logic (like `Cubit`)\n- **`MuBuilder\u003cS\u003e`** - Rebuilds UI on state changes (like `BlocBuilder`)\n- **`MuListener\u003cS\u003e`** - Performs side effects (like `BlocListener`)\n- **`MuConsumer\u003cS\u003e`** - Combines builder and listener (like `BlocConsumer`)\n- **`MuProvider\u003cT\u003e`** - Provides values down the widget tree (like `Provider` or `BlocProvider`)\n\nAdditional widgets are available for handling multiple states or providers: `MuMultiBuilder`, `MuMultiListener`, and `MuMultiProvider`. The `MuComparable` mixin is also available for state equality comparison. See the [Components](#components) section for details.\n\n## Screenshots\n\n\u003cdiv align=\"center\"\u003e\n  \u003cimg src=\"https://raw.githubusercontent.com/erf/mu_state/main/screenshots/idle.png\" width=\"180\"/\u003e\n  \u003cimg src=\"https://raw.githubusercontent.com/erf/mu_state/main/screenshots/success.png\" width=\"180\"/\u003e\n  \u003cimg src=\"https://raw.githubusercontent.com/erf/mu_state/main/screenshots/loading.png\" width=\"180\"/\u003e\n  \u003cimg src=\"https://raw.githubusercontent.com/erf/mu_state/main/screenshots/error.png\" width=\"180\"/\u003e\n\u003c/div\u003e\n\n## Usage\n\nLet's create a simple counter to see how `mu_state` works:\n\n### counter_logic.dart\n\n```dart\nimport 'package:mu_state/mu_state.dart';\n\nclass CounterState with MuComparable {\n  final int counter;\n  final bool isLoading;\n  final String? error;\n\n  const CounterState({required this.counter, required this.isLoading, this.error});\n\n  CounterState copyWith({int? counter, bool? isLoading, String? error}) {\n    return CounterState(\n      counter: counter ?? this.counter,\n      isLoading: isLoading ?? this.isLoading,\n      error: error ?? this.error,\n    );\n  }\n\n  @override\n  List\u003cObject?\u003e get props =\u003e [counter, isLoading, error];\n}\n\nclass CounterLogic extends MuLogic\u003cCounterState\u003e {\n  CounterLogic() : super(const CounterState(counter: 0, isLoading: false));\n\n  void increment() {\n    value = value.copyWith(counter: value.counter + 1);\n  }\n\n  Future\u003cvoid\u003e incrementAsync() async {\n    value = value.copyWith(isLoading: true, error: null);\n    await Future.delayed(const Duration(seconds: 1));\n    \n    // Simulate random error\n    if (DateTime.now().millisecondsSinceEpoch % 3 == 0) {\n      value = value.copyWith(isLoading: false, error: 'Failed to increment');\n    } else {\n      value = value.copyWith(counter: value.counter + 1, isLoading: false);\n    }\n  }\n}\n```\n\n### main.dart\n\n```dart\nimport 'package:flutter/material.dart';\nimport 'package:mu_state/mu_state.dart';\nimport 'counter_logic.dart';\n\nvoid main() =\u003e runApp(CounterApp());\n\nclass CounterApp extends StatelessWidget {\n  @override\n  Widget build(BuildContext context) {\n    return MaterialApp(\n      home: MuProvider\u003cCounterLogic\u003e(\n        value: CounterLogic(),\n        child: CounterPage(),\n      ),\n    );\n  }\n}\n```\n\n### counter_page.dart\n\n```dart\nclass CounterPage extends StatelessWidget {\n  @override\n  Widget build(BuildContext context) {\n    final logic = context.logic\u003cCounterLogic\u003e();\n    \n    return Scaffold(\n      appBar: AppBar(title: Text('Counter')),\n      body: MuListener\u003cCounterState\u003e(\n        logic: logic,\n        listener: (context, state) {\n          if (state.error != null) {\n            ScaffoldMessenger.of(context).showSnackBar(\n              SnackBar(content: Text('⚠️ ${state.error}')),\n            );\n          }\n        },\n        listenWhen: (prev, curr) =\u003e prev.error != curr.error \u0026\u0026 curr.error != null,\n        child: Center(\n          child: Column(\n            mainAxisAlignment: MainAxisAlignment.center,\n            children: [\n              MuBuilder\u003cCounterState\u003e(\n                valueListenable: logic,\n                builder: (context, state, child) {\n                  return Column(\n                    children: [\n                      Text('Counter: ${state.counter}', style: Theme.of(context).textTheme.headlineMedium),\n                      if (state.isLoading) CircularProgressIndicator(),\n                    ],\n                  );\n                },\n              ),\n              SizedBox(height: 20),\n              ElevatedButton(\n                onPressed: () =\u003e logic.increment(),\n                child: Text('Increment'),\n              ),\n              SizedBox(height: 8),\n              ElevatedButton(\n                onPressed: () =\u003e logic.incrementAsync(),\n                child: Text('Async Increment'),\n              ),\n            ],\n          ),\n        ),\n      ),\n    );\n  }\n}\n```\n\nAt this point we have successfully separated our presentational layer from our business logic layer. Notice that `CounterPage` knows nothing about what happens when a user taps the buttons. The widget simply notifies the `CounterLogic` that the user has pressed increment.\n\n\u003e **Alternative:** You could also use `MuConsumer` to combine the listener and builder functionality in a single widget. See the [MuConsumer](#muconsumer) section below for details.\n\n## Components\n\n### MuLogic\n\n`MuLogic\u003cS\u003e` is a typedef for `ValueNotifier\u003cS\u003e` that serves as the foundation for your business logic. It manages state and notifies listeners when the state changes. `MuLogic` is essentially an alias that makes the code more semantic for state management purposes.\n\n```dart\nclass CounterLogic extends MuLogic\u003cCounterState\u003e {\n  CounterLogic() : super(const CounterState(counter: 0, isLoading: false));\n\n  void increment() {\n    value = value.copyWith(counter: value.counter + 1);\n  }\n}\n```\n\nSince `MuLogic` extends `ValueNotifier`, you get all the familiar methods like `addListener`, `removeListener`, and the `value` property for getting and setting state.\n\n### MuBuilder\n\n`MuBuilder\u003cS\u003e` is a typedef for `ValueListenableBuilder\u003cS\u003e` that rebuilds the widget when the state changes. It's essentially an alias that makes the code more semantic for state management, but underneath it's just Flutter's built-in `ValueListenableBuilder`.\n\nSee `MuListener` if you want to \"do\" anything in response to state changes such as navigation, showing a dialog, etc...\n\n```dart\nMuBuilder\u003cCounterState\u003e(\n  valueListenable: logic,\n  builder: (context, state, child) {\n    return Text('Counter: ${state.counter}');\n  }\n)\n```\n\n### MuComparable\n\n`MuComparable` is a mixin that provides equality comparison for state classes. It's a lightweight alternative to packages like Equatable and helps `MuBuilder` and other listeners determine when to rebuild.\n\n```dart\nclass CounterState with MuComparable {\n  final int counter;\n  final String? error;\n\n  const CounterState({required this.counter, this.error});\n\n  @override\n  List\u003cObject?\u003e get props =\u003e [counter, error];\n}\n```\n\nThe `props` list should include all properties that determine equality. When state changes, widgets will only rebuild if the new state is different from the previous state based on these properties.\n\n### MuListener\n\n`MuListener` performs side effects in response to state changes - navigation, showing dialogs, etc. The `listener` is called once per state change and by default NOT on initial state (`lazy: true`). Set `lazy: false` to call the listener immediately with the current state.\n\n```dart\nMuListener\u003cCounterState\u003e(\n  logic: logic,\n  listener: (context, state) {\n    if (state.error != null) {\n      ScaffoldMessenger.of(context).showSnackBar(\n        SnackBar(content: Text('Error: ${state.error}')),\n      );\n    }\n  },\n  listenWhen: (prev, curr) =\u003e prev.error != curr.error,\n  child: Container(),\n)\n```\n\n### MuProvider\n\n`MuProvider` is a Flutter widget which provides any value to its children via dependency injection. While originally designed for `MuLogic` instances, it's now fully generic and can provide any type - making it perfect for injecting repositories, services, or other dependencies throughout your widget tree.\n\nYou provide the instance to `MuProvider` via the `value` parameter. `MuProvider` will automatically handle disposing the value when the provider is disposed (if it implements `ChangeNotifier`, which `MuLogic` does).\n\n```dart\nMuProvider\u003cCounterLogic\u003e(\n  value: CounterLogic(),\n  child: CounterPage(),\n);\n```\n\nAccess from anywhere in the subtree:\n\n```dart\n// For MuLogic instances\nfinal logic = context.logic\u003cCounterLogic\u003e();\n\n// For any type\nfinal repo = context.read\u003cAuthRepository\u003e();\n```\n\n### MuMultiProvider\n\n`MuMultiProvider` is a Flutter widget that merges multiple `MuProvider` widgets into one. `MuMultiProvider` improves the readability and eliminates the need to nest multiple `MuProviders`. By using `MuMultiProvider` we can go from:\n\n```dart\nMuProvider\u003cLogicA\u003e(\n  value: LogicA(),\n  child: MuProvider\u003cLogicB\u003e(\n    value: LogicB(),\n    child: MuProvider\u003cLogicC\u003e(\n      value: LogicC(),\n      child: ChildA(),\n    )\n  )\n)\n```\n\nto:\n\n```dart\nMuMultiProvider([\n  (child) =\u003e MuProvider\u003cLogicA\u003e(value: LogicA(), child: child),\n  (child) =\u003e MuProvider\u003cLogicB\u003e(value: LogicB(), child: child),\n  (child) =\u003e MuProvider\u003cLogicC\u003e(value: LogicC(), child: child),\n], child: ChildA())\n```\n\n### MuMultiBuilder\n\n`MuMultiBuilder` is a Flutter widget which listens to multiple `MuLogic` instances and rebuilds when any of them change. This is useful when you need to build UI that depends on multiple state sources.\n\n```dart\nMuMultiBuilder(\n  listenables: [logicA, logicB],\n  builder: (context, values, child) {\n    final stateA = values[0] as StateA;\n    final stateB = values[1] as StateB;\n    return Text('A: ${stateA.value}, B: ${stateB.value}');\n  },\n)\n```\n\n### MuMultiListener\n\n`MuMultiListener` is a Flutter widget that merges multiple `MuListener` widgets into one. `MuMultiListener` improves the readability and eliminates the need to nest multiple `MuListener`s. By using `MuMultiListener` we can go from:\n\n```dart\nMuListener\u003cStateA\u003e(\n  logic: logicA,\n  listener: (context, state) {\n    // handle state A changes\n  },\n  child: MuListener\u003cStateB\u003e(\n    logic: logicB,\n    listener: (context, state) {\n      // handle state B changes\n    },\n    child: ChildWidget(),\n  ),\n)\n```\n\nto:\n\n```dart\nMuMultiListener(\n  listeners: [\n    (child) =\u003e MuListener\u003cStateA\u003e(\n      logic: logicA,\n      listener: (context, state) {\n        // handle state A changes\n      },\n      child: child,\n    ),\n    (child) =\u003e MuListener\u003cStateB\u003e(\n      logic: logicB,\n      listener: (context, state) {\n        // handle state B changes\n      },\n      child: child,\n    ),\n  ],\n  child: ChildWidget(),\n)\n```\n\n### MuConsumer\n\n`MuConsumer` combines `MuBuilder` and `MuListener` functionality in a single widget.\n\n```dart\nMuConsumer\u003cCounterState\u003e(\n  logic: logic,\n  listener: (context, state) {\n    if (state.error != null) {\n      ScaffoldMessenger.of(context).showSnackBar(\n        SnackBar(content: Text('Error: ${state.error}')),\n      );\n    }\n  },\n  builder: (context, state, child) {\n    return Text('Counter: ${state.counter}');\n  },\n)\n```\n\nAn optional `listenWhen` and `buildWhen` can be implemented for more granular control over when `listener` and `builder` are called. The `listenWhen` and `buildWhen` functions take the previous state and the current state and return a `bool` which determines whether or not the `listener` or `builder` function will be invoked.\n\n```dart\nMuConsumer\u003cCounterState\u003e(\n  logic: logic,\n  listenWhen: (previous, current) {\n    // return true/false to determine whether or not\n    // to invoke listener with state\n    return previous.error != current.error;\n  },\n  listener: (context, state) {\n    // do stuff here based on state\n  },\n  buildWhen: (previous, current) {\n    // return true/false to determine whether or not\n    // to rebuild the widget with state\n    return previous.counter != current.counter;\n  },\n  builder: (context, state, child) {\n    // return widget here based on state\n    return Text('Counter: ${state.counter}');\n  }\n)\n```\n\nSee the [example project](example/) for a complete implementation with more features.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ferf%2Fmu_state","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ferf%2Fmu_state","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ferf%2Fmu_state/lists"}