sdui_flutter_sanity 0.3.0
sdui_flutter_sanity: ^0.3.0 copied to clipboard
A Server-Driven UI Flutter package powered by Sanity.io headless CMS with realtime config updates.
SDUI Flutter Sanity #
A Server-Driven UI (SDUI) Flutter package powered by Sanity.io headless CMS with realtime config updates.
Sanity.io stores the component tree for each screen as structured content. The Flutter app renders it live β including picking up edits published in Sanity Studio in realtime, with no app rebuild required.
Features #
- π¨ Dynamic widget rendering powered by Sanity.io GROQ queries.
- β‘ Real-time updates via Server-Sent Events (SSE) with exponential backoff.
- πΎ Offline-first local caching via Hive for last-known-good config.
- π Extensible β register custom components and action handlers.
- π CLI code generator β scaffold Flutter widgets + Sanity schemas in one command.
- π§© State management agnostic β works with Riverpod, Bloc, Provider, or anything.
Installation #
Add to your pubspec.yaml:
dependencies:
sdui_flutter_sanity: ^0.3.0
Quick Start #
1. Initialize the Engine #
In your main.dart, initialize the engine before runApp. Register any custom components and action handlers here.
import 'package:flutter/material.dart';
import 'package:sdui_flutter_sanity/sdui_flutter_sanity.dart';
// Import generated components and action handlers
import 'sdui_components/product_card_component.dart';
import 'sdui_actions/add_to_cart_action_handler.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await SduiEngine.initialize(
config: const SduiConfig(
projectId: 'your_sanity_project_id',
dataset: 'production',
// token: 'YOUR_READ_TOKEN', // only needed for private datasets
),
components: [
ProductCardComponent(), // <-- Generated by CLI or hand-written
],
actionHandlers: [
AddToCartActionHandler(), // <-- Generated by CLI or hand-written
],
);
runApp(const MyApp());
}
2. Render a Screen #
Drop SduiView anywhere in your widget tree. It fetches the Sanity document for the given slug, caches it locally, and subscribes to live updates automatically.
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('Home')),
body: SduiView(
slug: 'home', // Matches a Page Config document slug in Sanity
scrollable: true, // Wraps in SingleChildScrollView (default: true)
padding: EdgeInsets.all(16),
loadingWidget: const Center(child: CircularProgressIndicator()),
errorBuilder: (ctx, err, stack) => Center(child: Text('$err')),
),
),
);
}
}
CLI Code Generator #
The fastest way to add custom components or actions. Run from your project root:
dart run sdui_flutter_sanity:generate
The interactive CLI will ask a few questions and generate everything automatically.
=== SDUI Code Generator ===
Commands:
c / component β scaffold a Flutter component + Sanity schema
a / action β scaffold a Flutter action handler
s / seed β generate a sample .ndjson to seed a fresh Sanity project
Generating a Component #
=== SDUI Code Generator ===
Generate a Component (c) or Action (a)? [c]: c
Component name (e.g. product_card): product_card
Fields (comma separated name:type, e.g. title:string, price:number): title:string, price:number, imageUrl:image, inStock:boolean
Has children? (y/n) [n]: n
Flutter output directory [lib/sdui_components]:
Sanity schema directory [sanity_studio/schemas]:
β
Generated Dart Component: lib/sdui_components/product_card_component.dart
β
Generated Sanity Schema: sanity_studio/schemas/components/productCardComponent.ts
β
Updated uiElements.ts
β
Updated index.ts
π Done! Don't forget to register your new ProductCardComponent in SduiEngine.initialize()
Supported field types:
| Type | Sanity Schema Type | Dart Type |
|---|---|---|
string |
string |
String |
text |
text (multi-line) |
String |
number |
number |
double |
boolean |
boolean |
bool |
image |
image |
String (URL) |
url |
string |
String |
color |
string |
String? |
The generator auto-patches uiElements.ts and index.ts so you never have to touch those files manually.
What gets generated #
lib/sdui_components/product_card_component.dart β A fully typed Flutter component stub:
import 'package:flutter/material.dart';
import 'package:sdui_flutter_sanity/sdui_flutter_sanity.dart';
class ProductCardComponent extends SduiComponent {
@override
String get type => 'productCard'; // Matches Sanity schema initialValue
@override
Widget build(BuildContext context, UiNode node, Widget Function(UiNode) buildChild) {
final title = node.props['title'] as String? ?? '';
final price = (node.props['price'] as num?)?.toDouble() ?? 0.0;
final imageUrl = node.props['imageUrl'] as String? ?? '';
final inStock = node.props['inStock'] as bool? ?? false;
// TODO: Implement your custom widget using the extracted properties.
return const Placeholder();
}
}
sanity_studio/schemas/components/productCardComponent.ts β The matching Sanity schema:
import {defineField, defineType} from 'sanity'
export default defineType({
name: 'productCardComponent',
title: 'Product Card',
type: 'object',
fields: [
defineField({ name: 'type', type: 'string', hidden: true, initialValue: 'productCard' }),
defineField({
name: 'props',
title: 'Properties',
type: 'object',
fields: [
defineField({ name: 'title', title: 'Title', type: 'string' }),
defineField({ name: 'price', title: 'Price', type: 'number' }),
defineField({ name: 'imageUrl', title: 'Image Url', type: 'image' }),
defineField({ name: 'inStock', title: 'In Stock', type: 'boolean' }),
],
}),
],
preview: {
select: {subtitle: 'props.title'},
prepare(selection) {
return {title: 'Product Card', subtitle: selection.subtitle}
},
},
})
Generating an Action Handler #
Generate a Component (c) or Action (a)? [c]: a
Action name (e.g. add_to_cart): add_to_cart
Flutter output directory [lib/sdui_actions]:
β
Generated Dart Action Handler: lib/sdui_actions/add_to_cart_action_handler.dart
π Done! Don't forget to register your new AddToCartActionHandler in SduiEngine.initialize()
lib/sdui_actions/add_to_cart_action_handler.dart:
import 'package:flutter/material.dart';
import 'package:sdui_flutter_sanity/sdui_flutter_sanity.dart';
class AddToCartActionHandler implements SduiActionHandler {
@override
bool canHandle(SduiAction action) => action.type == 'add_to_cart';
@override
void handle(BuildContext context, SduiAction action) {
final payload = action.payload; // e.g. a product ID
final params = action.params; // optional extra fields from CMS
// TODO: Implement your custom action logic.
// Works with any state management: Riverpod, Bloc, Provider, etc.
debugPrint('AddToCartActionHandler: Handled action with payload $payload');
}
}
Writing Custom Components Manually #
If you prefer not to use the CLI, you can implement SduiComponent directly:
import 'package:flutter/material.dart';
import 'package:sdui_flutter_sanity/sdui_flutter_sanity.dart';
class PromoBannerComponent extends SduiComponent {
@override
String get type => 'promoBanner'; // Must match the Sanity schema 'initialValue'
@override
Widget build(BuildContext context, UiNode node, Widget Function(UiNode) buildChild) {
final headline = node.props['headline'] as String? ?? '';
final subtitle = node.props['subtitle'] as String? ?? '';
final bgColor = node.props['bgColor'] as String? ?? '#6200EA';
// Extract an optional CMS-defined action (e.g. navigate to a promo page)
final action = SduiAction.tryParse(node.props['action']);
return GestureDetector(
onTap: () {
if (action != null) {
// Dispatch to any registered SduiActionHandler
SduiEngine.handleAction(context, action);
}
},
child: Container(
padding: const EdgeInsets.all(20),
color: Color(int.parse(bgColor.replaceFirst('#', '0xFF'))),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(headline, style: Theme.of(context).textTheme.headlineSmall),
Text(subtitle, style: Theme.of(context).textTheme.bodyMedium),
// Recursively render any CMS children embedded in this component
...node.children.map(buildChild),
],
),
),
);
}
}
Writing Custom Action Handlers Manually #
import 'package:flutter/material.dart';
import 'package:sdui_flutter_sanity/sdui_flutter_sanity.dart';
class AddToCartActionHandler implements SduiActionHandler {
@override
// The `type` must match the prefix of the CMS action string, e.g. "cart_add:product-123"
bool canHandle(SduiAction action) => action.type == 'cart_add';
@override
void handle(BuildContext context, SduiAction action) {
final productId = action.payload; // e.g. "product-123"
final quantity = action.params['qty'] as int? ?? 1;
// Use any state management β the package doesn't care!
// context.read<CartNotifier>().add(productId, quantity);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Added $productId (qty: $quantity) to cart!')),
);
}
}
CMS Action String formats accepted:
"cart_add:product-123"β legacy colon-separated string{"type": "cart_add", "payload": "product-123", "params": {"qty": 2}}β structured Map
Sanity Studio Setup #
1. Create a Studio project #
npm create sanity@latest
2. Copy the bundled schemas #
Copy the entire sanity_studio/schemas/ folder from this repository into your Sanity project's schemaTypes/ or schemas/ folder. This gives you:
pageConfig.tsβ Schema for a single app screen.components/uiElements.tsβ Array schema listing all available UI component types.components/*.tsβ Individual schemas fortext,image,button,column,row,card,spacer.
3. Seed Sample Content (Recommended for new projects) #
A fresh Sanity project has no documents, so your Flutter app will show a loading spinner with nothing to render. The CLI can generate a ready-to-import .ndjson seed file that pre-populates two sample pages showcasing every built-in component:
dart run sdui_flutter_sanity:generate
# Choose: s (seed)
# Output path: sdui_seed.ndjson
Then import it into your Sanity dataset:
cd your_sanity_studio_dir
sanity dataset import /path/to/sdui_seed.ndjson production --replace
This creates two published pageConfig documents:
| Slug | Content |
|---|---|
home |
Heading, body text, image, two stat cards (row), feature card, CTA buttons |
catalog |
Product list with image cards using text, row, card, button |
Your Flutter app will immediately render them via SduiView(slug: 'home') β no app changes needed.
Tip: The seed documents are real Sanity documents. Open Sanity Studio after importing to see them and edit freely.
4. Export the schemas #
Ensure your schemaTypes/index.ts exports all schemas:
import pageConfig from './pageConfig'
import textComponent from './components/textComponent'
import imageComponent from './components/imageComponent'
import buttonComponent from './components/buttonComponent'
import spacerComponent from './components/spacerComponent'
import columnComponent from './components/columnComponent'
import rowComponent from './components/rowComponent'
import cardComponent from './components/cardComponent'
import uiElements from './components/uiElements'
export const schemaTypes = [
pageConfig,
textComponent,
imageComponent,
buttonComponent,
spacerComponent,
columnComponent,
rowComponent,
cardComponent,
uiElements,
]
Custom components added via the CLI are auto-imported here.
4. Run the Studio and create content #
npm run dev
- Open
http://localhost:3333. - Create a new Page Config document.
- Set the Slug to
home(or any screen identifier your Flutter app uses). - Build the Layout by adding components from the CMS.
- Click Publish β the Flutter app will immediately render the update via SSE.
SduiConfig Reference #
SduiConfig(
projectId: 'abc123', // Required. Your Sanity project ID.
dataset: 'production', // Optional. Default: 'production'.
apiVersion: 'v2024-01-01', // Optional. Sanity API version.
token: 'sk...', // Optional. Required for private datasets.
enableCache: true, // Optional. Hive disk cache. Default: true.
supportedVersion: 1, // Optional. Schema version guard. Default: 1.
)
SduiView Reference #
SduiView(
slug: 'home', // Required. Sanity Page Config slug.
scrollable: true, // Wraps in SingleChildScrollView. Default: true.
padding: EdgeInsets.all(16), // Padding around the rendered tree. Default: 16.
loadingWidget: CircularProgressIndicator(), // Shown while fetching. Default: spinner.
errorBuilder: (ctx, err, stack) => Text('$err'), // Custom error UI.
)
Core Use Cases #
1. E-Commerce & Promotional Campaigns #
Marketing teams use Sanity Studio to update banners, hero layouts, and product carousels β no developer involvement, no app store review. Changes arrive on-device in milliseconds via SSE.
2. A/B Testing & Personalization #
Pass different slug values to SduiView based on user segments (premium vs free, region, etc.) to serve entirely different UI trees from the same Flutter code.
3. Feature Flags & Phased Rollouts #
Wrap new features in a custom SDUI component. Add or remove it from the Sanity layout to toggle the feature for 100% of live users instantly β no code change, no deployment.
State Management Agnostic #
SDUI Flutter Sanity does not force any state management library. Custom components return standard Flutter widgets, so you can use:
// Riverpod
class MyComponent extends SduiComponent {
@override Widget build(ctx, node, buildChild) =>
Consumer(builder: (ctx, ref, _) {
final cart = ref.watch(cartProvider);
return Text('${cart.count} items');
});
}
// Bloc
class MyComponent extends SduiComponent {
@override Widget build(ctx, node, buildChild) =>
BlocBuilder<CartBloc, CartState>(
builder: (ctx, state) => Text('${state.count} items'),
);
}