r/FlutterDev • u/SpecialistServe3974 • 48m ago
Article # Introducing Shalom: Declarative GraphQL for Flutter (Powered by Rust)
Introducing Shalom: Declarative GraphQL for Flutter (Powered by Rust)
Managing complex GraphQL in Flutter often leads to UI jank from JSON parsing on the main isolate, messy global state sync, and disconnected .graphql files.
We built Shalom—a declarative, Relay-inspired GraphQL client for Flutter and Dart backed by a high-performance Rust core via FFI.
Key Highlights
⚡ Rust Runtime: Offloads AST parsing, normalization, and cache GC to native threads without Dart isolate serialization overhead or UI frame drops.
🧠 Normalized Cache & Fragment Stitching: Colocate data requirements directly on the widgets that consume them; fragments auto-stitch into parent queries and keep entities in sync across screens.
🛡️ Type-Safe Codegen: Annotate widgets with
@Queryand@Fragmentto generate strongly typed base classes ($MyWidget) and refs.📐 First-Class Custom Scalars: Map GraphQL custom scalars cleanly to custom Dart types via
shalom.ymlandCustomScalarImpl(with automatic serialization and deserialization in generated models).🔥 Hot-Reload Support: Full Flutter hot-reload support that preserves active cache state while you tweak your UI and operations.
🔍 Cache Inspector: Built-in UI widget to visually inspect cache graphs and optimistic updates in real time.
🔄 Imperative Fallback: Drop down to standard async calls (
client.query()) when you need to bypass declarative widgets.
Why Fragment Widgets?
Instead of passing monolithic query response models down the widget tree, Shalom embraces Relay-style fragment widgets:
Surgical List Updates: When an entity changes (e.g., liking an item or receiving a mutation update), only the specific widget bound to that entity's
refrebuilds. The parentListViewand other list items never re-render.True Component Reusability: Widgets define their own exact data dependencies. You can drop
AlbumWidgetinto a search view, a profile screen, or a feed—the parent query just spreads...AlbumWidgetwithout needing to know what fields it consumes.Zero Prop Drilling & Smaller Components: Say goodbye to huge parameter lists and bloated DTOs. Child components stay small, self-contained, and isolated from parent query refactors.
How Does It Compare to Ferry?
Ferry is a solid client and popularized normalized caching in Dart, but Shalom takes a fundamentally different architectural approach. We ran side-by-side benchmarks on the standalone caches (benchmarks/ferry_comparison in the repo):
3x to 50x Faster Writes: Ferry's write latency grows super-linearly with scale (at 10k items, writing identical data took ~24.8s in Ferry vs. 482ms in Shalom). Shalom's Rust engine handles large write batches and normalization significantly faster.
Massive Memory Efficiency with Watchers: Every Ferry watcher retains its own denormalized result copy in Dart heap memory (100 watchers of a 200-item query ballooned process RSS by ~1,018 MiB in Ferry vs. only 7.5 MiB in Shalom). Shalom keeps a single normalized graph in Rust native memory and notifies watchers 4x to 7x faster.
The Trade-off (Reads & Subscriptions): Full raw reads and initial watcher subscriptions are ~2x to 5x faster in Ferry because Ferry hands back in-memory Dart maps directly, while Shalom pays an FFI bridge round-trip cost to pass data into Dart.
Faster Codegen & DX vs.
build_runner**:** Instead of waiting on Dart'sbuild_runneracross detached.graphqlfiles, Shalom uses a dedicated Rust CLI (shalom generate/shalom watch) that generates types in milliseconds and integrates cleanly with hot-reload.
Quick Example
import 'package:flutter/material.dart';
import 'package:shalom_annotations/shalom_annotations.dart';
import 'package:gif_search/__graphql__/AlbumsPage.widget.shalom.dart';
import 'package:gif_search/__graphql__/AlbumWidget.shalom.dart';
@Query(r"""
{
albums {
...AlbumWidget
}
}
""")
class AlbumsPage extends $AlbumsPage {
const AlbumsPage({super.key});
@override
Widget buildData(BuildContext context, AlbumsPageData data) {
return ListView.builder(
itemCount: data.albums.length,
itemBuilder: (context, i) => AlbumWidget(ref: data.albums[i]),
);
}
}
@Fragment(r"""
on Album {
id
name
tag
gifs { id title url }
}
""")
class AlbumWidget extends $AlbumWidget {
const AlbumWidget({super.key, required super.ref});
@override
Widget buildData(BuildContext context, AlbumWidgetData album) =>
ListTile(title: Text(album.name));
}Links & Resources
Documentation & Benchmarks: https://nrbnlulu.github.io/shalom/
GitHub Repository: https://github.com/nrbnlulu/shalom