ionex 2.0.0
ionex: ^2.0.0 copied to clipboard
A lightweight, high-performance, molecular state management library for Flutter.
ionex #
Ionex is a lightweight Flutter state-management package built around a single atomic reactive primitive: Ion<T>. It keeps UI state small, explicit, predictable, and easy to compose without introducing a heavy framework.
Why Ionex? #
- Minimal API: Zero friction, Fast, readable state creation and mutation.
- Predictable Flow: Built on Flutter's native
ValueNotifierfor predictable state flow. - No Magic Lifecycles: No complex background streams or hidden garbage collection issues.
- Hybrid Architecture: Works flawlessly as global atoms (Signals/Jotai style) or as scoped local business logic components via context-driven injection (Bloc/Provider style).
- Zero Dependencies: Keeps your app bundle lightweight and future-proof.
Highlights #
Ion<T>for strongly-typed reactive state.IonBuilder<T>for targeted, lightweight widget subtree rebuilds.IonProvider<T extends Ion>for type-safe, scoped tree dependency injection.IonConsumer<T extends Ion<S>, S>for automatic context lookup and state listening.MultiIonProviderto flatten provider structures and kill nesting boilerplate.- 100% Widget and Unit test coverage verified.
- Example application included in
example/lib/main.dart
Current project status #
- Version:
2.0.0 - Flutter tests: passing
- Flutter analyzer: no issues found
- License: MIT
- Changelog:
CHANGELOG.md
Installation #
Add ionex to your project:
flutter pub add ionex
Or add it directly to pubspec.yaml:
dependencies:
ionex: ^2.0.0
Then fetch dependencies:
flutter pub get
Requirements #
- Flutter SDK >= 3.0.0
- Dart SDK >= 3.0.0 < 4.0.0
Package layout #
lib/ionex.dart: public exportslib/src/core/ion.dart:Ion<T>reactive state primitivelib/src/widgets/ion_builder.dart: scoped UI rebuild widgetlib/src/widgets/ion_provider.dart: scoped controller injectionlib/src/widgets/ion_consumer.dart: context-driven listener widgetlib/src/widgets/multi_ion_provider.dart: multi-provider compositionexample/lib/main.dart: runnable demonstration apptest/: unit and widget test coverage
Core API #
Ion<T> #
An Ion<T> is the fundamental reactive molecule of your application. It holds a typed state value and alerts listeners exclusively when its value changes.
// Instantiate with an initial value
final counter = Ion<int>(0);
print(counter.state); // Read synchronously: 0
counter.set(1); // Direct mutation
counter.update((current) => current + 1); // Derived mutation
counter.reset(); // Automatically snaps back to 0!
Key methods #
set(T newValue): Immediately replaces the state and triggers updates if the new value differs.update(T Function(T currentState) updateFn): Mutates state using a callback function. Out-of-the-box support forces UI rebuilds even for in-place collection mutations (likeList.add).reset(): Automatically rolls the state back to the exact initial value defined in the constructor.state: Getter that synchronously exposes the current value.
Note:
Ion.reset()no longer accepts an arbitrary value. It always returns the controller to its original initial value.
UI Components & Dependency Injection #
IonBuilder<T> #
Listens to an external or global Ion atom and rebuilds only the builder's local widget tree layout.
IonBuilder<int>(
ion: counter,
builder: (context, value) {
return Text('Count: $value');
},
);
IonProvider<T extends Ion> #
Injects a state controller into a local widget subtree using Flutter's native InheritedWidget mechanisms. It encapsulates business logic lifecycle, calling dispose() automatically when removed from the tree.
class AuthController extends Ion<String> {
AuthController() : super('unauthenticated');
void login(String user) => set('Welcome, $user');
}
// Ingesting into the tree
IonProvider<AuthController>(
create: (_) => AuthController(),
child: const ProfileScreen(),
);
You can read the controller anywhere below using IonProvider.of<T>(context).
IonProvider.of<T> also accepts a listen flag, so you can choose whether the caller rebuilds when the state changes:
// Fetch instance without listening (optimal for button action triggers)
final controller = IonProvider.of<AuthController>(context, listen: false);
controller.login('AtomicDev');
IonConsumer<T extends Ion<S>, S> #
Combines dependency lookup and state listening into a single sleek component. It automatically resolves the requested controller from the context, listens to its state variations, and exposes both values directly in the builder function.
IonConsumer<AuthController, String>(
builder: (context, state, controller) {
return Column(
children: [
Text('Status: $state'),
ElevatedButton(
onPressed: () => controller.login('User123'),
child: const Text('Log In'),
),
],
);
},
);
MultiIonProvider #
Annihilates the nested "Pyramid of Doom" widget tree when injecting multiple controllers at the same level.
MultiIonProvider(
providers: [
IonProvider<ThemeController>(create: (_) => ThemeController()),
IonProvider<LabMessageController>(create: (_) => LabMessageController('Init')),
IonProvider<LabStatusController>(create: (_) => LabStatusController(true)),
],
child: const HomeScreen(),
);
Breaking changes in 2.0.0 #
- Smar
Ion.reset(): No longer accepts an input argument. It implicitly rolls the state back to the originalinitialValuegiven during construction. - Simplified
IonProviderSignature: Constraints changed fromT extends Ion<dynamic>to simplyT extends Ion, improving syntactic clarity and IDE autocomplete engines. - Flexible
IonProvider(child): Thechildproperty is now optional inside theIonProviderconstructor to supportMultiIonProviderflattening. Standalone implementations are guarded by a runtime assertion requiring a non-nullchild.
Usage examples #
A complete, working integration showcasing the power of global atoms alongside clean, scoped multi-controllers injection: #
import 'package:flutter/material.dart';
import 'package:ionex/ionex.dart';
// Global Atoms (Application wide states)
final counterIon = Ion<int>(0);
final themeIon = Ion<ThemeMode>(ThemeMode.light);
// Scoped Context Controllers (Screen specific business logic)
class LabMessageController extends Ion<String> {
LabMessageController(super.value);
void changeMessage(String msg) => set(msg);
}
class LabStatusController extends Ion<bool> {
LabStatusController(super.value);
void toggleStatus() => set(!state);
}
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return IonBuilder<ThemeMode>(
ion: themeIon,
builder: (context, currentTheme) {
return MaterialApp(
themeMode: currentTheme,
theme: ThemeData.light(useMaterial3: true),
darkTheme: ThemeData.dark(useMaterial3: true),
home: MultiIonProvider(
providers: [
IonProvider<LabMessageController>(create: (_) => LabMessageController('Atomic Lab Active')),
IonProvider<LabStatusController>(create: (_) => LabStatusController(true)),
],
child: const HomeScreen(),
),
);
},
);
}
}
class HomeScreen extends StatelessWidget {
const HomeScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Ionex Molecular Lab'),
actions: [
IconButton(
icon: const Icon(Icons.brightness_6),
onPressed: () => themeIon.update(
(c) => c == ThemeMode.light ? ThemeMode.dark : ThemeMode.light,
),
)
],
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Consuming context via IonConsumer
IonConsumer<LabMessageController, String>(
builder: (context, message, controller) {
return Column(
children: [
Text(message),
ElevatedButton(
onPressed: () => controller.changeMessage('State mutated!'),
child: const Text('Mutate Message'),
),
],
);
},
),
const SizedBox(height: 24),
// Consuming sister controller from MultiIonProvider
IonConsumer<LabStatusController, bool>(
builder: (context, isActive, controller) {
return ActionChip(
label: Text(isActive ? 'SYSTEM: ONLINE' : 'SYSTEM: OFFLINE'),
onPressed: () => controller.toggleStatus(),
);
},
),
const Divider(height: 48),
// Consuming global atoms via IonBuilder
IonBuilder<int>(
ion: counterIon,
builder: (context, count) => Text('Count: $count', style: const TextStyle(fontSize: 32)),
),
ElevatedButton(
onPressed: () => counterIon.update((c) => c + 1),
child: const Text('Increment Global Atom'),
)
],
),
),
);
}
}
Example app #
The example app in example/lib/main.dart demonstrates:
- theme switching with a global
Ion<ThemeMode> - context-scoped controllers with
IonProvider - reactive UI updates with
IonConsumerandIonBuilder - flattened provider composition using
MultiIonProvider
Run the example:
cd example
flutter run
Testing and verification #
flutter test→ passedflutter analyze→ no issues found
The test suite validates:
Ioninitialization and state accessset()andupdate()workflowsreset()restoring original values- listener notification behavior
IonBuilderreactive rebuildsIonProviderdisposal and context lookupIonConsumerimplicit controller resolutionMultiIonProviderprovider composition
Changelog #
See CHANGELOG.md for full release notes.
License #
Ionex is distributed under the MIT License. See LICENSE for the full text.
Contributing #
Contributions are welcome. A good contribution generally includes:
- updating or adding tests for any behavior change
- updating documentation when the public API changes
- keeping examples aligned with the current package API
- verifying changes with
flutter testandflutter analyze