axiom_flutter
The Flutter bindings for AxiomCore.
axiom_flutter provides a robust, Rust-powered runtime for managing Server State in Flutter applications. It connects your UI directly to AxiomCore, allowing you to execute API contracts with cryptographic safety, automatic caching, and request deduplication.
❓ Why AxiomCore?
Modern mobile development suffers from the "Network-UI Gap".
- Fragility: APIs change, but mobile apps update slowly. This leads to runtime crashes when JSON schemas drift.
- Boilerplate: Developers write thousands of lines of
isLoading,isError, and JSON serialization code. - State Confusion: Developers often stuff API caching logic into UI state managers (Bloc/Provider/Riverpod), leading to race conditions and memory leaks.
Axiom solves this by moving the network layer into a pre-compiled, verified Contract.
How it works
- Define your API in a schema (Axiom Contract).
- Generate the client code.
- Run the
AxiomRuntime(powered by Rust) in your app.
The Rust core handles serialization, validation, caching, and networking. Flutter simply consumes the resulting Stream.
🧠 Server State vs. UI State
To use Axiom effectively, it is helpful to understand the distinction between the two types of state in an application.
| UI State (Use Provider/Bloc/Riverpod) | Server State (Use Axiom) |
|---|---|
| Synchronous | Asynchronous (Requires latency) |
| Client-Owned (Dropdown open, form input) | Remote-Owned (Database data, User Profile) |
| Ephemeral (Reset on restart) | Persistent (Outlives the session) |
| Deterministic | Stale (Can change without you knowing) |
axiom_flutter is designed strictly for Server State. It handles the complexity of "Stale-While-Revalidate", optimistic updates, and background refetching so your UI State managers don't have to.
✨ Key Features
- Rust-Powered Runtime: Uses
dart:ffito communicate with a high-performance Rust core. - Smart Caching: Automatic cache persistence and invalidation. If data exists in the Sled cache (managed by Rust), it is shown immediately while the network request updates in the background.
- Request Deduplication: If two widgets request the same data simultaneously, Axiom only sends one network request and broadcasts the result to both.
- Rich Error Handling: Errors are categorized (
Network,Auth,Contract,Timeout) and include retry strategies. - Deterministic Query Keys: Arguments are normalized and sorted.
{a: 1, b: 2}generates the same cache key as{b: 2, a: 1}.
📦 Installation
Add axiom_flutter to your pubspec.yaml. You will also need the compiled Rust libraries (handled by your project setup or the Axiom CLI).
dependencies:
axiom_flutter:
git:
url: https://github.com/AxiomCore/axiom-sdk.git
ref: main
path: flutter/axiom_flutter
path_provider: ^2.0.0
🚀 Initialization
Before using any queries, initialize the runtime. This usually happens in main.dart.
import 'package:axiom_flutter/axiom_flutter.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// 1. Load your contract bytes (usually an asset)
final contractBytes = await loadContractAsset();
// 2. Start the Rust Runtime
await AxiomRuntime().startup(
baseUrl: "https://api.myapp.com",
contractBytes: contractBytes,
// optional: signature and publicKey for contract verification
);
runApp(const MyApp());
}
🛠 Usage
1. The AxiomBuilder Widget
The generic way to consume data is using AxiomBuilder. It handles the stream subscription, initial loading state, and error states for you.
class UserProfile extends StatelessWidget {
final int userId;
const UserProfile({super.key, required this.userId});
@override
Widget build(BuildContext context) {
// 1. Create the query (Usually generated code)
final query = MyApi.getUser(id: userId);
return AxiomBuilder<User, User>(
query: query,
// 2. Optional: Select specific data to prevent unnecessary rebuilds
selector: (user) => [user.name, user.avatarUrl],
// 3. Handle Loading (Optional, defaults to SizedBox)
loading: (context) => const CircularProgressIndicator(),
// 4. Handle Error (Optional, defaults to Text)
error: (context, error) => Text(error.message),
// 5. Build Data
builder: (context, state, user) {
return Column(
children: [
if (state.isFetching) const LinearProgressIndicator(), // Show background refresh
Text(user.name),
Image.network(user.avatarUrl),
],
);
},
);
}
}
2. Manual Stream Usage
You can use the query stream directly in your BLoCs or Providers.
final query = MyApi.getDashboard();
// Listen to the stream
query.stream.listen((state) {
if (state.hasData) {
print("Got data from ${state.source}: ${state.data}");
}
});
// Force a refresh from network
query.refresh();
3. One-shot Futures (Extensions)
Sometimes you just want a Future (e.g., on a button press), not a stream. axiom_flutter provides extension methods for this.
void onRefreshPressed() async {
try {
// .unwrap() waits for the first valid data or throws an error
final data = await MyApi.getDashboard().stream.unwrap();
print("Refreshed: $data");
} catch (e) {
print("Failed: $e");
}
}
⚙️ Architecture Deep Dive
Query Management & Keys
Inside lib/src/query_manager.dart, Axiom maintains a Map<String, ActiveQuery>.
When you request a query:
- Normalization: The arguments are sorted alphabetically (see
AxiomQueryKeyinquery_key.dart). - Key Generation: A unique string key is built (e.g.,
endpoint_42:{"id":10}). - Lookup:
- If an
ActiveQueryexists for that key, you get a subscription to the existing stream. - If not, a new FFI call is made to Rust, and a new Stream is created.
- If an
This ensures that if 5 different widgets request User(id: 1), only one network request is sent, and all 5 widgets update simultaneously.
The FFI Boundary
Communication happens via dart:ffi.
- Request: Dart serializes the request body to
Uint8Listand passes pointers to C++. - Processing: Rust processes the request (Cache lookup -> Network -> Cache Write).
- Response: Rust invokes a C-function pointer callback registered by Dart.
- Isolate Communication: The callback sends data via a
SendPortto the main Flutter isolate to ensure thread safety.
Error Handling
Axiom returns rich error objects defined in state.dart. You can check:
stage: Did it fail duringcacheRead,networkSend, ordeserialize?category: Is it anetworkissue,authissue, orvalidationissue?retryable: Does the Rust core recommend retrying?
🛡 Security
Axiom contracts can be signed. If you provide a signature and publicKey during startup, the Rust core will cryptographically verify that the API definition hasn't been tampered with.
If the contract is unsigned, AxiomRuntime will print a security warning to the console in debug mode.
License
MIT