axiom_flutter 0.0.6
axiom_flutter: ^0.0.6 copied to clipboard
Axiom Flutter SDK
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: ^0.0.1
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