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.
import 'package:flutter/material.dart';
import 'package:sdui_flutter_sanity/sdui_flutter_sanity.dart';
// ---------------------------------------------------------------------------
// This example demonstrates the full SDUI Flutter Sanity workflow:
//
// Step 1. Run `dart run sdui_flutter_sanity:generate` to generate:
// - lib/sdui_components/product_card_component.dart
// - lib/sdui_actions/add_to_cart_action_handler.dart
// (The generated files are stubbed inline below for reference.)
//
// Step 2. Implement the generated TODO stubs with your real widget code.
//
// Step 3. Register them in SduiEngine.initialize() below.
//
// Step 4. Drop <SduiView slug: 'home'> anywhere — done!
// ---------------------------------------------------------------------------
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await SduiEngine.initialize(
config: const SduiConfig(
projectId:
'your_project_id_here', // <-- Replace with your Sanity Project ID
dataset: 'production',
// token: 'YOUR_READ_TOKEN_IF_DATASET_IS_PRIVATE',
),
// Register the components generated (or hand-written) for this app.
// The package already includes: text, image, button, column, row, card, spacer.
components: [
ProductCardComponent(),
PromoBannerComponent(),
],
// Register action handlers for interactive CMS-driven events.
actionHandlers: [
AddToCartActionHandler(),
],
);
runApp(const SduiApp());
}
// ---------------------------------------------------------------------------
// APP SHELL
// ---------------------------------------------------------------------------
class SduiApp extends StatelessWidget {
const SduiApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'SDUI + Sanity Demo',
theme: ThemeData(useMaterial3: true, colorSchemeSeed: Colors.indigo),
// Integrate with your router (e.g. go_router) to pass dynamic slugs.
home: const SduiPage(slug: 'home'),
);
}
}
/// Renders whatever component tree Sanity has published for [slug].
/// Replace [slug] with any Page Config document slug from your Sanity Studio.
class SduiPage extends StatelessWidget {
final String slug;
const SduiPage({super.key, required this.slug});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Page: $slug')),
body: SafeArea(
child: SduiView(
slug: slug,
scrollable: true,
padding: const EdgeInsets.all(16),
// Optional: show a branded loading state
loadingWidget: const Center(child: CircularProgressIndicator()),
// Optional: custom error UI with a retry button
errorBuilder: (ctx, err, stack) => Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.cloud_off_outlined,
size: 48, color: Colors.grey),
const SizedBox(height: 12),
Text('Could not load "$slug":\n$err',
textAlign: TextAlign.center),
],
),
),
),
),
);
}
}
// ===========================================================================
// GENERATED COMPONENT — product_card
// ===========================================================================
// Generated by: dart run sdui_flutter_sanity:generate
// Component name: product_card
// Fields: title:string, price:number, imageUrl:image, inStock:boolean
// Has children? no
//
// After generation, replace `return const Placeholder()` with your real widget.
// ===========================================================================
class ProductCardComponent extends SduiComponent {
@override
String get type =>
'productCard'; // Must match the Sanity schema `initialValue`
@override
Widget build(
BuildContext context, UiNode node, Widget Function(UiNode) buildChild) {
// Typed prop extraction — generated automatically by the CLI.
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;
// Actions can be attached to any component in Sanity Studio.
// e.g. the editor sets action = "cart_add:product-abc123"
final action = SduiAction.tryParse(node.props['action']);
// --- Real widget implementation (replacing the generated Placeholder) ---
return Card(
margin: const EdgeInsets.symmetric(vertical: 8),
elevation: 2,
child: InkWell(
onTap: action != null
? () => SduiEngine.handleAction(context, action)
: null,
child: Padding(
padding: const EdgeInsets.all(12),
child: Row(
children: [
if (imageUrl.isNotEmpty)
ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Image.network(imageUrl,
width: 72, height: 72, fit: BoxFit.cover),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title, style: Theme.of(context).textTheme.titleMedium),
const SizedBox(height: 4),
Text(
'\$${price.toStringAsFixed(2)}',
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
color: Colors.green, fontWeight: FontWeight.bold),
),
],
),
),
if (!inStock)
const Chip(
label: Text('Out of Stock'), backgroundColor: Colors.red),
],
),
),
),
);
}
}
// ===========================================================================
// HAND-WRITTEN COMPONENT — promo_banner
// (Demonstrates a component with children + actions, written without the CLI)
// ===========================================================================
class PromoBannerComponent extends SduiComponent {
@override
String get type => 'promoBanner';
@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? ?? '';
// Dispatch CMS-defined navigation actions
final action = SduiAction.tryParse(node.props['action']);
return GestureDetector(
onTap: action != null
? () => SduiEngine.handleAction(context, action)
: null,
child: Container(
margin: const EdgeInsets.only(bottom: 16),
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [Color(0xFF6200EA), Color(0xFF03DAC6)],
),
borderRadius: BorderRadius.circular(12),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(headline,
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
color: Colors.white, fontWeight: FontWeight.bold)),
if (subtitle.isNotEmpty) ...[
const SizedBox(height: 4),
Text(subtitle,
style: Theme.of(context)
.textTheme
.bodyMedium
?.copyWith(color: Colors.white70)),
],
// Recursively render any children the editor added in the CMS
...node.children.map(buildChild),
],
),
),
);
}
}
// ===========================================================================
// GENERATED ACTION HANDLER — add_to_cart
// ===========================================================================
// Generated by: dart run sdui_flutter_sanity:generate
// Action name: add_to_cart
//
// CMS action string format: "cart_add:product-abc123"
// action.type => "cart_add"
// action.payload => "product-abc123"
// action.params => {} (or structured params from Map format)
//
// After generation, replace the TODO body with your business logic.
// Works with any state management — Riverpod, Bloc, Provider, etc.
// ===========================================================================
class AddToCartActionHandler implements SduiActionHandler {
@override
bool canHandle(SduiAction action) => action.type == 'cart_add';
@override
void handle(BuildContext context, SduiAction action) {
final productId = action.payload;
final params = action.params;
// --- Real implementation (replacing the generated debugPrint stub) ---
// Plug in any state management:
// context.read<CartNotifier>().add(productId, params);
// context.read<CartBloc>().add(CartAddEvent(productId));
debugPrint(
'AddToCartActionHandler: Adding "$productId" with params $params');
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Added "$productId" to cart!'),
duration: const Duration(seconds: 2),
),
);
}
}