vexo 1.0.0 copy "vexo: ^1.0.0" to clipboard
vexo: ^1.0.0 copied to clipboard

Vexo is the ultimate Flutter state management — zero dependencies, as simple as GetX, yet as powerful as Riverpod and BLoC combined. Fast, reactive, clean.

⚡ Vexo — The Ultimate Flutter State Management #

Zero dependencies. Simple as GetX. Powerful as Riverpod + BLoC combined.

Dart SDK Flutter External Dependencies License Lines of Code


Table of Contents #

  1. Why Vexo?
  2. The Problem With Every Other Package
  3. Deep Comparison: Vexo vs GetX vs Provider vs Riverpod vs BLoC
  4. Feature Matrix
  5. Side-by-Side Code Comparison
  6. Core Concepts
  7. Full API Reference
  8. Architecture Guide
  9. Migration Cheat-Sheet
  10. Installation

Why Vexo? #

Flutter's state management ecosystem is fragmented. Every popular package solves some problems brilliantly while creating new ones. Developers are forced to choose between simplicity and power — until now.

Vexo was built by studying every weakness of every major package and eliminating them all, while keeping only the best ideas from each. The result is a single, pure-Dart package that requires zero external dependencies and gives you everything.

// This is all you need to know to start:
import 'package:vexo/vexo.dart';

class CounterController extends VexoController {
  final count = VexoState(0);                        // reactive atom
  late final doubled = count.select((v) => v * 2);  // derived state — auto updates
  void increment() => count.increment();             // dead simple
}

// Register once
Vexo.put(() => CounterController(), permanent: true);

// Use anywhere — no context, no ref, no boilerplate
VexoConsumer<CounterController>(
statesOf: (c) => [c.count],
builder: (ctx, c) => Text('${c.count.value}'),
)

The Problem With Every Other Package #

GetX — Too Magical, Too Opinionated #

GetX is beloved for its simplicity, but it comes at a steep cost:

  • Global state pollution.obs variables are scattered everywhere with no clear ownership
  • Hidden magicGet.find() can silently fail at runtime, no compile-time safety
  • Monolith dependency — GetX bundles routing, HTTP, storage, i18n — you get it all whether you want it or not
  • Testing nightmare — tightly coupled to Get, mocking is painful
  • No computed/derived state — you manage derived values manually
  • No async state type-safety — loading/error/data states are manual booleans
  • No event-driven architecture — no BLoC-style events, just direct method calls
  • Lifecycle surprisesonInit / onClose timing issues plague production apps

Provider — Too Verbose, Too Primitive #

Provider was Flutter's "official" recommendation, but developers quickly outgrow it:

  • Widget tree pollution — everything must live in the widget tree via ChangeNotifierProvider
  • Massive boilerplateMultiProvider with 10+ providers is unreadable
  • No built-in reactivity — you must manually call notifyListeners() everywhere
  • No derived state — computing values from multiple providers is painful
  • No async supportFutureProvider / StreamProvider are clunky and limited
  • Context dependency — you cannot access state outside the widget tree without workarounds
  • No lifecycle hooks — controllers have no onInit / onClose
  • Memory leaks — forgetting dispose() is easy and completely silent
  • Not a real DI system — no dependency injection, just widget-scoped values

Riverpod — Too Complex, Too Much Ceremony #

Riverpod is architecturally sound but has a brutal learning curve:

  • Code generation required for real-world use (@riverpod, build_runner) — adds build time, complexity, and tooling fragility
  • ref is everywhere — you must pass ref through your entire call chain
  • Steep learning curveProvider, StateProvider, FutureProvider, StreamProvider, NotifierProvider, AsyncNotifierProvider... which one do you use?
  • Verbose providers — even a simple counter needs many lines of boilerplate
  • No event-driven pattern — BLoC-style events are not supported
  • No built-in forms — form validation is completely out of scope
  • No worker helpersever, debounce, interval must be implemented manually
  • No global middleware — no equivalent of BLoC's BlocObserver
  • ProviderScope required — tight coupling to the widget tree root

flutter_bloc — Too Verbose, Too Much Boilerplate #

BLoC is powerful but exhausting:

  • Extreme boilerplate — every feature needs: Event class, State class, Bloc/Cubit class, BlocProvider, BlocBuilder, BlocListener — minimum 5 files for one screen
  • Over-engineering — simple counters become 100-line codebases
  • No built-in reactive atoms — every piece of state must be a full Bloc
  • No derived state — computing values from multiple blocs is manual
  • No async state typing — loading/error states must be manually added to every State class
  • No form management — completely out of scope
  • No worker helpersever, debounce, etc. must be built from scratch
  • Steep learning curveEventTransformer, StreamTransformer, concurrency handling
  • Verbose widgetsBlocConsumer with both listenWhen and buildWhen becomes deeply nested

Deep Comparison: Vexo vs GetX vs Provider vs Riverpod vs BLoC #

GetX vs Vexo #

Criterion GetX Vexo
Simplicity Simple Equally simple
Reactive state .obs VexoState
Derived / computed state Manual VexoComputed / .select()
Type-safe DI Runtime errors possible Compile-time safe
Lifecycle hooks onInit, onClose onInit, onReady, onClose
Worker helpers ever, once, debounce All of those + throttle, interval
BLoC / event-driven None VexoBloc / VexoCubitBloc
Async state Manual booleans Typed VexoAsyncState<T>
Reactive forms None VexoForm + VexoField
Global middleware None VexoMiddleware
External dependencies Huge monolith Zero
Testability Get-coupled Pure Dart, easy to test
Scoped state Limited Full scoped DI
Selector rebuild optimization None VexoSelector

Verdict: Vexo has every feature GetX has, plus BLoC events, typed async, computed state, forms, and middleware — with zero external dependencies and better type safety. GetX is a monolith; Vexo is surgical.


Provider vs Vexo #

Criterion Provider Vexo
Setup boilerplate Heavy (MultiProvider, ChangeNotifier) One line: Vexo.put(...)
Reactivity Manual notifyListeners() Automatic
Derived state Not supported VexoComputed
Async state Clunky FutureProvider VexoAsyncState<T>
Access outside widget tree Requires context Vexo.find<T>() anywhere
Lifecycle management None onInit, onReady, onClose
Auto-dispose Manual Automatic on VexoScope close
BLoC-style events None VexoBloc
Worker helpers None VexoWorker
Forms None VexoForm
Global middleware None VexoMiddleware
Scoped controllers Via widget tree only Widget tree + global registry
External dependencies Zero Zero

Verdict: Vexo is strictly a superset of Provider. Everything Provider does, Vexo does better with automatic reactivity, no manual notifyListeners(), and a complete feature set Provider never had.


Riverpod vs Vexo #

Criterion Riverpod Vexo
Learning curve Steep (8+ provider types) Gentle (one VexoState concept)
Code generation Required for real apps Never needed
Boilerplate High Minimal
ref passing Must thread through call chain Vexo.find<T>() anywhere
Derived state Excellent Equally excellent
Async state AsyncValue VexoAsyncState (simpler API)
Type safety Excellent Excellent
BLoC / event-driven None VexoBloc
Worker helpers Manual VexoWorker
Reactive forms None VexoForm
Global middleware None VexoMiddleware
Scoped state Excellent Excellent
ProviderScope required Yes, must wrap app root No widget tree requirement
External dependencies Yes (riverpod package) Zero
Hot-reload friendly Yes Yes

Verdict: Vexo matches Riverpod's power with a fraction of the ceremony. No code generation, no ref threading, no ProviderScope, no 8 different provider types to memorize — and Vexo adds BLoC events, forms, and workers that Riverpod simply does not have.


flutter_bloc vs Vexo #

Criterion flutter_bloc Vexo
Event-driven pattern Excellent VexoBloc / VexoCubitBloc
Typed state transitions Excellent VexoCubitBloc<E, S>
Boilerplate Extreme Minimal
Simple reactive state Must write full Bloc VexoState(0)
Derived state None VexoComputed
Async state Manual state classes VexoAsyncState<T>
Global middleware BlocObserver VexoMiddleware
Worker helpers None VexoWorker
Reactive forms None VexoForm
Lifecycle hooks None onInit, onReady, onClose
DI / controller registry None (use get_it separately) Built-in
Files per feature 3–5 files minimum 1 file
Testing Good (bloc_test package) Pure Dart, no extra package needed
External dependencies Yes Zero

Verdict: Vexo gives you everything flutter_bloc gives you — typed events, typed states, state transitions, global observer — but eliminates the extreme boilerplate. A BLoC feature that took 5 files now takes 1. Vexo also adds reactive state atoms, DI, async state, forms, and workers that BLoC never had.


Feature Matrix #

Feature GetX Provider Riverpod BLoC Vexo
Reactive state atoms
Derived / computed state
Selector (granular rebuild)
BLoC event-driven pattern
Typed async state ⚠️
Built-in DI / registry
Scoped controllers ⚠️ ⚠️
Lifecycle hooks
Worker helpers (ever/debounce)
Reactive forms
Global middleware / observer
Multi-state listener widget
Access state without context
Auto-dispose on scope exit
Code generation required ⚠️
Zero external dependencies
Debounce / throttle built-in
Type-safe at compile time ⚠️ ⚠️
State interceptors
Stream access on state
Named scopes
Total score 9/21 4/21 12/21 8/21 21/21

Side-by-Side Code Comparison #

Counter App #

GetX

// controller
class CounterController extends GetxController {
  var count = 0.obs;
  void increment() => count++;
}
Get.put(CounterController());

// widget
Obx(() => Text('${Get.find<CounterController>().count}'))

Provider

// model
class Counter extends ChangeNotifier {
  int _count = 0;
  int get count => _count;
  void increment() { _count++; notifyListeners(); }
}
// main
ChangeNotifierProvider(create: (_) => Counter(), child: MyApp())
// widget
Consumer<Counter>(builder: (_, c, __) => Text('${c.count}'))

Riverpod

// provider (requires @riverpod annotation + build_runner)
@riverpod
class Counter extends _$Counter {
  @override int build() => 0;
  void increment() => state++;
}
// widget
Consumer(builder: (ctx, ref, _) {
final count = ref.watch(counterProvider);
return Text('$count');
})

flutter_bloc — minimum 3 files

// events.dart
abstract class CounterEvent {}
class IncrementEvent extends CounterEvent {}

// states.dart
class CounterState { final int count; const CounterState(this.count); }

// bloc.dart
class CounterBloc extends Bloc<CounterEvent, CounterState> {
  CounterBloc() : super(CounterState(0)) {
    on<IncrementEvent>((event, emit) => emit(CounterState(state.count + 1)));
  }
}
// main
BlocProvider(create: (_) => CounterBloc(), child: MyApp())
// widget
BlocBuilder<CounterBloc, CounterState>(
builder: (ctx, state) => Text('${state.count}'),
)

Vexo — cleanest of all

// controller.dart
class CounterController extends VexoController {
  final count = VexoState(0);
  void increment() => count.increment();
}

// main.dart
Vexo.put(() => CounterController(), permanent: true);

// widget — done
VexoConsumer<CounterController>(
statesOf: (c) => [c.count],
builder: (ctx, c) => Text('${c.count.value}'),
)

Async Data Loading #

GetX

class PostsController extends GetxController {
  var posts = <Post>[].obs;
  var isLoading = false.obs;
  var error = ''.obs;
  @override
  void onInit() { fetchPosts(); super.onInit(); }
  void fetchPosts() async {
    isLoading.value = true;
    error.value = '';
    try {
      posts.value = await api.getPosts();
    } catch (e) {
      error.value = e.toString();
    } finally {
      isLoading.value = false;
    }
  }
}
// Three separate Obx() widgets required for loading/error/data

Riverpod

@riverpod
Future<List<Post>> posts(PostsRef ref) => api.getPosts();

// widget
ref.watch(postsProvider).when(
loading: () => Spinner(),
error: (e, s) => Text('$e'),
data: (posts) => PostList(posts),
)

flutter_bloc — ~80 lines across 3 files

// Requires: PostEvent, PostState (Loading/Loaded/Error classes),
// PostBloc, BlocProvider, BlocBuilder with 3 state checks
// Minimum 80 lines across 3 separate files

Vexo

class PostsController extends VexoController {
  final posts = VexoAsyncState<List<Post>>();
  @override
  void onInit() { posts.execute(api.getPosts); super.onInit(); }
}

// widget — one widget, three states handled
VexoAsyncBuilder<List<Post>>(
state:   ctrl.posts,
loading: () => Spinner(),
error:   (e, _) => Text('$e'),
data:    (posts) => PostList(posts),
)

Event-Driven Authentication #

flutter_bloc

// auth_event.dart, auth_state.dart, auth_bloc.dart — 3 files
// BlocProvider + BlocBuilder + BlocListener in widget tree
// ~120 lines total

Vexo — same power, 1 file

abstract class AuthEvent extends VexoEvent {}
class Login  extends AuthEvent { final String id; Login(this.id); }
class Logout extends AuthEvent {}

abstract class AuthState {}
class LoggedIn  extends AuthState { final String user; LoggedIn(this.user); }
class LoggedOut extends AuthState {}

class AuthBloc extends VexoCubitBloc<AuthEvent, AuthState> {
  AuthBloc() : super(LoggedOut());
  @override
  void onEvent(AuthEvent event) async {
    switch (event) {
      case Login(:var id):  emit(LoggedIn(id));
      case Logout():        emit(LoggedOut());
    }
  }
}

// Usage
bloc.add(Login('user123'));

// Widget — one builder, full type-safety
VexoBuilder(
states: [bloc.stateAtom],
builder: (ctx) => switch (bloc.state) {
LoggedIn(:var user) => HomeScreen(user),
LoggedOut()         => LoginScreen(),
_                   => SplashScreen(),
},
)

Reactive Forms #

  • GetX — No built-in form system
  • Provider — No built-in form system
  • Riverpod — No built-in form system
  • flutter_bloc — No built-in form system
  • Vexo — Full reactive form system included
class LoginForm extends VexoForm {
  final email = VexoField<String>(
    initial: '',
    validators: [
      VexoValidators.required('Email is required'),
      VexoValidators.email(),
    ],
  );
  final password = VexoField<String>(
    initial: '',
    validators: [VexoValidators.minLength(6)],
  );

  @override
  List<VexoField> get fields => [email, password];
}

// Widget
VexoFieldBuilder<String>(
field: form.email,
builder: (_, field) => TextField(
onChanged: (v) => field.value = v,
decoration: InputDecoration(errorText: field.error),
),
)

// Submit with auto loading/error state
await form.submit(() => authService.login(form.email.value, form.password.value));

Global State Observer / Middleware #

  • GetX — Not supported
  • Provider — Not supported
  • Riverpod — Not supported
  • flutter_blocBlocObserver (covers only Blocs, not general state)
  • Vexo — Covers ALL controllers and ALL state
class AppMiddleware extends VexoMiddleware {
  @override
  void onCreate(VexoController ctrl) =>
      analytics.log('created', {'type': '${ctrl.runtimeType}'});

  @override
  void onChange(VexoController ctrl, VexoChange change) =>
      logger.d('${change.stateName}: ${change.previous} → ${change.next}');

  @override
  void onError(VexoController ctrl, Object error, StackTrace stack) =>
      crashlytics.recordError(error, stack);

  @override
  void onClose(VexoController ctrl) =>
      analytics.log('closed', {'type': '${ctrl.runtimeType}'});
}

// Register once at app start
VexoObserver.middleware = AppMiddleware();

Core Concepts #

VexoState<T> — Reactive Atom #

final count = VexoState(0);
final name  = VexoState('Alice');

// Read / write
print(count.value);
count.value = 5;
count.emit(10);
count.update((v) => v * 2);
count.setSilent(99);       // silent — no notification

// Stream
count.stream.listen(print);

// Integer helpers
count.increment();
count.increment(5);
count.decrement();
count.reset();

// Bool helpers
final isDark = VexoState(false);
isDark.toggle();
isDark.setTrue();
isDark.setFalse();

// List helpers
final items = VexoState<List<String>>([]);
items.add('hello');
items.remove('hello');
items.removeAt(0);
items.clear();

// Map helpers
final map = VexoState<Map<String, int>>({});
map.put('a', 1);
map.remove('a');

// Intercept changes
count.intercept((prev, next) => print('$prev → $next'));

// Derive
final doubled = count.select((v) => v * 2);

VexoComputed<T> — Derived State #

final a = VexoState(10);
final b = VexoState(5);

final sum  = VexoComputed(() => a.value + b.value, [a, b]);

// Chaining selectors
final name  = VexoState('hello world');
final upper = name.select((v) => v.toUpperCase());
final words = upper.select((v) => v.split(' '));

VexoController — Business Logic #

class UserController extends VexoController {
  final user      = VexoState<User?>(null);
  final isLoading = VexoState(false);
  final error     = VexoState<String?>(null);

  late final displayName = user.select((u) => u?.name ?? 'Guest');

  @override
  void onInit() {
    ever(user, (u) => print('Changed: $u'));
    once(user, (u) => print('First: $u'));
    when(isLoading, (v) => !v, (_) => print('Done'));
    debounce(user, (_) => cacheUser(), duration: Duration(seconds: 2));
    super.onInit();
  }

  @override
  void onReady() => fetchUser();

  @override
  void onClose() => print('Cleaning up');

  Future<void> fetchUser() => run(
        () => api.getUser(),
    loading: isLoading,
    errorState: error,
    onSuccess: (u) => user.emit(u),
  );
}

VexoAsyncState<T> — Async Data #

final posts = VexoAsyncState<List<Post>>();

await posts.execute(() => api.getPosts());

// Pattern match
posts.value.when(
loading: () => 'loading',
data:    (d) => 'got ${d.length} posts',
error:   (e, _) => 'error: $e',
);

// Widget
VexoAsyncBuilder<List<Post>>(
state:   posts,
loading: () => CircularProgressIndicator(),
error:   (e, _) => Text('$e'),
data:    (list) => PostList(list),
)

VexoBloc / VexoCubitBloc — Event-Driven #

abstract class CounterEvent extends VexoEvent {}
class Add   extends CounterEvent { final int n; Add(this.n); }
class Reset extends CounterEvent {}

class CounterBloc extends VexoCubitBloc<CounterEvent, int> {
  CounterBloc() : super(0);

  @override
  void onEvent(CounterEvent event) {
    switch (event) {
      case Add(:var n): emit(state + n);
      case Reset():     emit(0);
    }
  }
}

bloc.add(Add(5));
bloc.add(Reset());

Full API Reference #

Vexo Facade #

Vexo.put<T>(() => T(), lazy: true, permanent: false, scope: null)
Vexo.putInstance<T>(instance, permanent: false)
Vexo.find<T>(scope: null)
Vexo.findOrNull<T>(scope: null)
Vexo.findOrCreate<T>(() => T())
Vexo.isRegistered<T>()
Vexo.delete<T>()
Vexo.resetAll()
Vexo.state<T>(initial)           // creates VexoState<T>
Vexo.computed<T>(fn, deps)       // creates VexoComputed<T>

Reactive Widgets #

VexoBuilder(states: [...], builder: (ctx) => Widget)
VexoConsumer<T>(statesOf: (ctrl) => [...], builder: (ctx, ctrl) => Widget)
VexoListener<T>(state: s, onChanged: (ctx, v) => ..., child: Widget)
VexoSelector<C, R>(state: (ctrl) => s, select: (v) => r, builder: (ctx, r) => Widget)
VexoMulti(states: [...], builder: (ctx) => Widget)
VexoAsyncBuilder<T>(state: s, loading: ..., error: ..., data: ...)
VexoFieldBuilder<T>(field: f, builder: (ctx, f) => Widget)
VexoListenerBuilder<T>(state: s, onChanged: fn, builder: (ctx, v) => Widget)

Scoping #

VexoScope<T>(create: () => T(), child: Widget, permanent: false)
VexoMultiScope(scopes: [VexoScopeConfig(() => T())], child: Widget)

Workers #

VexoWorker.ever<T>(state, callback)
VexoWorker.once<T>(state, callback)
VexoWorker.when<T>(state, condition, callback)
VexoWorker.debounce<T>(state, callback, duration: Duration(milliseconds: 300))
VexoWorker.throttle<T>(state, callback, interval: Duration(milliseconds: 500))
VexoWorker.interval(duration, callback)
// All return VexoDisposable — call .dispose() to cancel

Built-in Validators #

VexoValidators.required([message])
VexoValidators.email([message])
VexoValidators.minLength(n, [message])
VexoValidators.maxLength(n, [message])
VexoValidators.pattern(regex, [message])
VexoValidators.custom<T>((v) => bool, [message])

Architecture Guide #

lib/
├── main.dart                        ← Vexo.put registrations + VexoObserver
├── middleware/
│   └── app_middleware.dart          ← VexoMiddleware subclass
├── controllers/
│   ├── auth_controller.dart         ← VexoController
│   ├── theme_controller.dart        ← VexoController
│   └── cart_controller.dart         ← VexoController
├── blocs/
│   └── auth_bloc.dart               ← VexoCubitBloc
├── forms/
│   └── checkout_form.dart           ← VexoForm
└── pages/
    ├── home_page.dart               ← VexoConsumer / VexoBuilder
    └── auth_page.dart               ← VexoBuilder(bloc.stateAtom)

Global singletons (auth, theme, cart):

Vexo.put(() => AuthController(), permanent: true);

Route-scoped controllers (per-screen logic):

VexoScope(create: () => CheckoutController(), child: CheckoutPage())

Named scopes (multi-tenant / multi-instance):

Vexo.put(() => ChatController(), scope: 'room_$roomId');
final ctrl = Vexo.find<ChatController>(scope: 'room_$roomId');

Migration Cheat-Sheet #

From GetX #

GetX Vexo
Get.put(Ctrl()) Vexo.put(() => Ctrl())
Get.find<Ctrl>() Vexo.find<Ctrl>()
0.obs / Rx<int>(0) VexoState(0)
Obx(() => Text(...)) VexoBuilder(states:[s], builder:...)
GetxController VexoController
ever(rx, fn) ctrl.ever(state, fn)
debounce(rx, fn) ctrl.debounce(state, fn)
GetBuilder<T> VexoConsumer<T>

From Provider #

Provider Vexo
ChangeNotifier + notifyListeners() VexoController (auto-notify)
ChangeNotifierProvider(create:...) VexoScope(create:...)
MultiProvider([...]) VexoMultiScope(scopes:[...])
Consumer<T> VexoConsumer<T>
context.read<T>() Vexo.find<T>()
context.watch<T>() VexoConsumer<T>(statesOf:...)

From Riverpod #

Riverpod Vexo
StateProvider((ref) => 0) VexoState(0)
NotifierProvider VexoController
AsyncNotifierProvider VexoController + VexoAsyncState
ref.watch(provider) VexoConsumer<T>(statesOf:...)
ref.read(provider) Vexo.find<T>()
AsyncValue<T> VexoAsyncValue<T>
ProviderScope Not needed

From flutter_bloc #

flutter_bloc Vexo
Bloc<E, S> VexoBloc<E>
Cubit<S> VexoCubitBloc<E, S>
emit(state) emit(state) — identical
bloc.add(event) bloc.add(event) — identical
BlocProvider VexoScope / Vexo.put
BlocBuilder VexoBuilder(states:[bloc.stateAtom],...)
BlocListener VexoListener(state:bloc.stateAtom,...)
BlocConsumer VexoListenerBuilder
BlocObserver VexoMiddleware
MultiBlocProvider VexoMultiScope

Installation #

dependencies:
  vexo:
    path: ./vexo
import 'package:vexo/vexo.dart';

That is the only import you will ever need. No vexo_flutter, no vexo_hooks, no vexo_codegen. Just Vexo.


MIT License — Mysterious Coder

2
likes
150
points
19
downloads

Documentation

API reference

Publisher

verified publishermysteriouscoder.com

Weekly Downloads

Vexo is the ultimate Flutter state management — zero dependencies, as simple as GetX, yet as powerful as Riverpod and BLoC combined. Fast, reactive, clean.

Repository (GitHub)
View/report issues

License

MIT (license)

Dependencies

flutter

More

Packages that depend on vexo