vexo 1.0.0
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.
// ============================================================
// example/lib/main.dart
// Vexo Full Demo — Counter, Auth, Todo, Theme, Async, BLoC
// ============================================================
import 'package:flutter/material.dart';
import 'package:vexo/vexo.dart';
// ═══════════════════════════════════════════════════════════
// 1. SIMPLE CONTROLLER — Counter (GetX-style simplicity)
// ═══════════════════════════════════════════════════════════
class CounterController extends VexoController {
final count = VexoState(0);
final step = VexoState(1);
// Derived state — auto-updates when count or step changes
late final doubled = count.select((v) => v * 2);
late final isEven = count.select((v) => v.isEven);
@override
void onInit() {
// Log every change
ever(count, (v) => debugPrint('Counter: $v'));
// Debounce rapid increments
debounce(count, (v) => debugPrint('Settled at $v'),
duration: const Duration(milliseconds: 500));
super.onInit();
}
void increment() => count.update((v) => v + step.value);
void decrement() => count.update((v) => v - step.value);
void reset() => count.emit(0);
}
// ═══════════════════════════════════════════════════════════
// 2. ASYNC CONTROLLER — Posts loader (Riverpod-style async)
// ═══════════════════════════════════════════════════════════
class Post {
final int id;
final String title;
const Post(this.id, this.title);
}
class PostsController extends VexoController {
final posts = VexoAsyncState<List<Post>>();
final search = VexoState('');
late final filtered = VexoComputed<List<Post>>(
() => (posts.data ?? [])
.where((p) => p.title
.toLowerCase()
.contains(search.value.toLowerCase()))
.toList(),
[posts, search],
);
@override
void onInit() {
loadPosts();
// Re-filter on search input
debounce(search, (_) {}, duration: const Duration(milliseconds: 200));
super.onInit();
}
Future<void> loadPosts() => posts.execute(() async {
await Future.delayed(const Duration(seconds: 1));
return List.generate(
20,
(i) => Post(i + 1, 'Post number ${i + 1}'),
);
});
}
// ═══════════════════════════════════════════════════════════
// 3. BLOC-STYLE — Auth (BLoC-style events + typed state)
// ═══════════════════════════════════════════════════════════
abstract class AuthEvent extends VexoEvent {}
class LoginEvent extends AuthEvent { final String user; LoginEvent(this.user); }
class LogoutEvent extends AuthEvent {}
abstract class AuthState {}
class AuthInitial extends AuthState {}
class AuthLoading extends AuthState {}
class AuthLoggedIn extends AuthState { final String user; AuthLoggedIn(this.user); }
class AuthLoggedOut extends AuthState {}
class AuthFailed extends AuthState { final String msg; AuthFailed(this.msg); }
class AuthBloc extends VexoCubitBloc<AuthEvent, AuthState> {
AuthBloc() : super(AuthInitial());
@override
void onEvent(AuthEvent event) async {
switch (event) {
case LoginEvent(:var user):
emit(AuthLoading());
await Future.delayed(const Duration(seconds: 1));
if (user.isEmpty) {
emit(AuthFailed('Username cannot be empty'));
} else {
emit(AuthLoggedIn(user));
}
case LogoutEvent():
emit(AuthLoading());
await Future.delayed(const Duration(milliseconds: 500));
emit(AuthLoggedOut());
}
}
}
// ═══════════════════════════════════════════════════════════
// 4. THEME CONTROLLER — Global singleton
// ═══════════════════════════════════════════════════════════
class ThemeController extends VexoController {
final isDark = VexoState(false);
void toggle() => isDark.toggle();
}
// ═══════════════════════════════════════════════════════════
// 5. TODO CONTROLLER — List state extensions
// ═══════════════════════════════════════════════════════════
class Todo {
final String title;
final bool done;
const Todo(this.title, {this.done = false});
Todo copyWith({bool? done}) => Todo(title, done: done ?? this.done);
}
class TodoController extends VexoController {
final todos = VexoState<List<Todo>>([]);
final showDone = VexoState(true);
final inputField = VexoField<String>(
initial: '',
validators: [VexoValidators.required('Todo cannot be empty')],
);
late final visible = VexoComputed<List<Todo>>(
() => showDone.value
? todos.value
: todos.value.where((t) => !t.done).toList(),
[todos, showDone],
);
late final doneCount =
todos.select((list) => list.where((t) => t.done).length);
void addTodo() {
if (!inputField.validate()) return;
todos.add(Todo(inputField.value));
inputField.reset();
}
void toggle(int index) {
final list = [...todos.value];
list[index] = list[index].copyWith(done: !list[index].done);
todos.emit(list);
}
void remove(int index) => todos.removeAt(index);
}
// ═══════════════════════════════════════════════════════════
// MAIN — Register controllers + run app
// ═══════════════════════════════════════════════════════════
void main() {
// Global middleware — logs all state transitions
VexoObserver.middleware = _AppMiddleware();
// Permanent singletons
Vexo.put(() => ThemeController(), permanent: true);
Vexo.put(() => CounterController(), permanent: true);
Vexo.put(() => AuthBloc(), permanent: true);
Vexo.put(() => PostsController(), permanent: true);
Vexo.put(() => TodoController(), permanent: true);
runApp(const VexoApp());
}
class _AppMiddleware extends VexoMiddleware {
@override
void onCreate(VexoController ctrl) =>
debugPrint('🟢 Vexo: ${ctrl.runtimeType} created');
@override
void onClose(VexoController ctrl) =>
debugPrint('🔴 Vexo: ${ctrl.runtimeType} closed');
}
// ═══════════════════════════════════════════════════════════
// APP ROOT
// ═══════════════════════════════════════════════════════════
class VexoApp extends StatelessWidget {
const VexoApp({super.key});
@override
Widget build(BuildContext context) {
final theme = Vexo.find<ThemeController>();
return VexoBuilder(
states: [theme.isDark],
builder: (ctx) => MaterialApp(
title: 'Vexo Demo',
debugShowCheckedModeBanner: false,
theme: theme.isDark.value ? ThemeData.dark() : ThemeData.light(),
home: const HomePage(),
),
);
}
}
// ═══════════════════════════════════════════════════════════
// HOME PAGE — Tab navigator
// ═══════════════════════════════════════════════════════════
class HomePage extends StatefulWidget {
const HomePage({super.key});
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
int _tab = 0;
final _pages = const [
CounterPage(),
TodoPage(),
PostsPage(),
AuthPage(),
];
@override
Widget build(BuildContext context) {
final theme = Vexo.find<ThemeController>();
return Scaffold(
appBar: AppBar(
title: const Text('⚡ Vexo Demo'),
actions: [
VexoBuilder(
states: [theme.isDark],
builder: (_) => IconButton(
icon: Icon(theme.isDark.value
? Icons.light_mode
: Icons.dark_mode),
onPressed: theme.toggle,
),
),
],
),
body: _pages[_tab],
bottomNavigationBar: BottomNavigationBar(
currentIndex: _tab,
type: BottomNavigationBarType.fixed,
onTap: (i) => setState(() => _tab = i),
items: const [
BottomNavigationBarItem(icon: Icon(Icons.add), label: 'Counter'),
BottomNavigationBarItem(icon: Icon(Icons.check_box), label: 'Todo'),
BottomNavigationBarItem(icon: Icon(Icons.list), label: 'Posts'),
BottomNavigationBarItem(icon: Icon(Icons.person), label: 'Auth'),
],
),
);
}
}
// ═══════════════════════════════════════════════════════════
// COUNTER PAGE
// ═══════════════════════════════════════════════════════════
class CounterPage extends StatelessWidget {
const CounterPage({super.key});
@override
Widget build(BuildContext context) {
final ctrl = Vexo.find<CounterController>();
return VexoBuilder(
states: [ctrl.count, ctrl.step],
builder: (_) => Padding(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Count', style: Theme.of(context).textTheme.titleLarge),
Text(
'${ctrl.count.value}',
style: Theme.of(context).textTheme.displayLarge,
),
Text('× 2 = ${ctrl.doubled.value}',
style: Theme.of(context).textTheme.titleMedium),
Text(ctrl.isEven.value ? '(even)' : '(odd)',
style: const TextStyle(color: Colors.grey)),
const SizedBox(height: 32),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
FloatingActionButton(
heroTag: 'dec',
onPressed: ctrl.decrement,
child: const Icon(Icons.remove)),
const SizedBox(width: 16),
FloatingActionButton(
heroTag: 'inc',
onPressed: ctrl.increment,
child: const Icon(Icons.add)),
const SizedBox(width: 16),
FloatingActionButton(
heroTag: 'rst',
onPressed: ctrl.reset,
child: const Icon(Icons.refresh)),
],
),
const SizedBox(height: 24),
Text('Step: ${ctrl.step.value}'),
Slider(
value: ctrl.step.value.toDouble(),
min: 1,
max: 10,
divisions: 9,
label: '${ctrl.step.value}',
onChanged: (v) => ctrl.step.emit(v.toInt()),
),
],
),
),
);
}
}
// ═══════════════════════════════════════════════════════════
// TODO PAGE
// ═══════════════════════════════════════════════════════════
class TodoPage extends StatelessWidget {
const TodoPage({super.key});
@override
Widget build(BuildContext context) {
final ctrl = Vexo.find<TodoController>();
return Column(
children: [
Padding(
padding: const EdgeInsets.all(16),
child: VexoFieldBuilder<String>(
field: ctrl.inputField,
builder: (_, field) => TextField(
onChanged: (v) => field.value = v,
onSubmitted: (_) => ctrl.addTodo(),
decoration: InputDecoration(
hintText: 'New todo…',
errorText: field.isTouched ? field.error : null,
suffixIcon: IconButton(
icon: const Icon(Icons.add),
onPressed: ctrl.addTodo,
),
),
),
),
),
VexoBuilder(
states: [ctrl.todos, ctrl.showDone],
builder: (_) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
children: [
VexoBuilder(
states: [ctrl.todos],
builder: (_) => Text(
'${ctrl.doneCount.value}/${ctrl.todos.value.length} done',
style: const TextStyle(color: Colors.grey),
),
),
const Spacer(),
const Text('Show done'),
Switch(
value: ctrl.showDone.value,
onChanged: (_) => ctrl.showDone.toggle(),
),
],
),
),
),
Expanded(
child: VexoBuilder(
states: [ctrl.todos, ctrl.showDone],
builder: (_) => ListView.builder(
itemCount: ctrl.visible.value.length,
itemBuilder: (_, i) {
final todo = ctrl.visible.value[i];
return ListTile(
leading: Checkbox(
value: todo.done,
onChanged: (_) => ctrl.toggle(i),
),
title: Text(
todo.title,
style: todo.done
? const TextStyle(
decoration: TextDecoration.lineThrough)
: null,
),
trailing: IconButton(
icon: const Icon(Icons.delete),
onPressed: () => ctrl.remove(i),
),
);
},
),
),
),
],
);
}
}
// ═══════════════════════════════════════════════════════════
// POSTS PAGE (Async)
// ═══════════════════════════════════════════════════════════
class PostsPage extends StatelessWidget {
const PostsPage({super.key});
@override
Widget build(BuildContext context) {
final ctrl = Vexo.find<PostsController>();
return Column(
children: [
Padding(
padding: const EdgeInsets.all(12),
child: VexoBuilder(
states: [ctrl.search],
builder: (_) => TextField(
decoration: const InputDecoration(
hintText: 'Search posts…',
prefixIcon: Icon(Icons.search),
),
onChanged: (v) => ctrl.search.value = v,
),
),
),
Expanded(
child: VexoAsyncBuilder<List<Post>>(
state: ctrl.posts,
loading: () => const Center(
child: CircularProgressIndicator()),
error: (e, _) => Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text('Error: $e'),
ElevatedButton(
onPressed: ctrl.loadPosts,
child: const Text('Retry'),
),
],
)),
data: (_) => VexoBuilder(
states: [ctrl.search],
builder: (__) => ListView.builder(
itemCount: ctrl.filtered.value.length,
itemBuilder: (_, i) {
final post = ctrl.filtered.value[i];
return ListTile(
leading: CircleAvatar(child: Text('${post.id}')),
title: Text(post.title),
);
},
),
),
),
),
],
);
}
}
// ═══════════════════════════════════════════════════════════
// AUTH PAGE (BLoC events)
// ═══════════════════════════════════════════════════════════
class AuthPage extends StatefulWidget {
const AuthPage({super.key});
@override
State<AuthPage> createState() => _AuthPageState();
}
class _AuthPageState extends State<AuthPage> {
final _controller = TextEditingController();
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final bloc = Vexo.find<AuthBloc>();
return Padding(
padding: const EdgeInsets.all(24),
child: VexoBuilder(
states: [bloc.stateAtom],
builder: (_) {
final state = bloc.state;
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
switch (state) {
AuthLoggedIn() => Icons.check_circle,
AuthFailed() => Icons.error,
AuthLoading() => Icons.hourglass_empty,
_ => Icons.person_outline,
},
size: 80,
color: switch (state) {
AuthLoggedIn() => Colors.green,
AuthFailed() => Colors.red,
_ => Colors.grey,
},
),
const SizedBox(height: 16),
Text(
switch (state) {
AuthLoggedIn(:var user) => 'Welcome, $user!',
AuthFailed(:var msg) => msg,
AuthLoading() => 'Loading…',
AuthLoggedOut() => 'You are logged out.',
_ => 'Please log in.',
},
style: Theme.of(context).textTheme.titleLarge,
textAlign: TextAlign.center,
),
const SizedBox(height: 32),
if (state is! AuthLoggedIn) ...[
TextField(
controller: _controller,
decoration:
const InputDecoration(hintText: 'Enter username'),
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: state is AuthLoading
? null
: () => bloc.add(LoginEvent(_controller.text)),
child: const Text('Login'),
),
] else ...[
ElevatedButton(
onPressed: () {
_controller.clear();
bloc.add(LogoutEvent());
},
child: const Text('Logout'),
),
],
],
);
},
),
);
}
}