Every e-commerce team runs into the same problem. Marketing wants a new banner for tonight's campaign, a flash sale countdown for the weekend, or a brand row moved above the fold. On the web that's a CMS change. On mobile it's a release: build, store review, and then waiting for people to update.
The usual fix is server-driven UI (SDUI). The backend describes the screen as data, and the app only knows how to draw the pieces. This post shows how to build a server-driven e-commerce home screen in Flutter with serenay_ecommerce_widgets, an open-source package with 36 ready-made e-commerce widgets that render straight from JSON.
👉 Live demo (runs in your browser) · Docs · GitHub
The idea: the screen is a JSON array
A screen is a list of { "type", "params" } entries, drawn top to bottom:
{
"data": [
{
"type": "ANNOUNCEMENTBAR",
"params": {
"bg_color": "#111111",
"text_color": "#FFFFFF",
"list": ["Free shipping over 500 TL"]
}
},
{ "type": "SLIDER", "params": { "id": 1, "height_percent": 0.3, "autoplay": 1 } },
{
"type": "CATEGORYMENU",
"params": {
"list": [
{ "title": "Shoes", "image": "https://example.com/shoes.png", "type": "category", "id": 1 },
{ "title": "Sale", "image": "https://example.com/sale.png", "type": "filter", "id": "sale" }
]
}
},
{
"type": "FLASHSALE",
"params": {
"title": "Flash Sale",
"end_time": 1782000000,
"category_id": 75,
"display_mode": "inline"
}
},
{ "type": "TEXT", "params": { "text": "Best sellers" } },
{ "type": "CAROUSEL", "params": { "category_id": 75, "limit": 10 } }
]
}
To rearrange the home page, change this JSON. Nothing gets released.
Rendering it in Flutter
flutter pub add serenay_ecommerce_widgets
The package doesn't care what your backend is. It never makes HTTP calls on its own. You pass in callbacks for navigation, data and cart:
import 'package:serenay_ecommerce_widgets/serenay_ecommerce_widgets.dart';
final callbacks = WidgetCallbacks(
onAction: (action) {
// action.type is "category", "product", "collection", "brand", ...
router.open(action);
},
fetchProducts: (query) => api.fetchProducts(query),
fetchSlides: (id) => api.fetchSlides(id),
isLoggedIn: () => auth.isLoggedIn,
onAddToCart: (product, variant, qty) => cart.add(product, variant, qty),
);
final json = await api.fetchScreen('home');
final data = WidgetCatalog.fromJson(json);
ListView(children: WidgetCatalog.getScreen(data: data, callbacks: callbacks));
Only onAction and fetchProducts are required. If you leave out the callback a widget needs (say, fetchVideos for VIDEOLIST), that widget hides itself. It doesn't crash.
Product widgets like CAROUSEL, GRID and FLASHSALE don't carry products in the JSON. They carry a query (category_id, order_by, limit, ...), and the package hands it to your fetchProducts. That way the JSON stays small and the prices come from your backend, not from a cached file.
What's in the box
Out of the box you get the widgets an e-commerce home or category page usually needs:
-
Merchandising:
SLIDER,IMAGECAROUSEL,BANNERGRID,IMAGE,TIMEIMAGE(banner with a countdown),ANNOUNCEMENTBAR -
Products:
CAROUSEL,GRID,PRODUCTCARD,MIXEDCAROUSEL,RECOMMENDEDFORYOU,VISITEDPRODUCTS,COMPARISON,BUNDLE -
Urgency & conversion:
FLASHSALE,COUPON,URGENCY,SOCIALPROOF,ABANDONEDCART,LOYALTYPROGRESS -
Discovery & trust:
CATEGORYMENU,BRANDS,STORY,SEARCH,REVIEWS,RATING,QNA,TRUSTBADGES,SIZEGUIDE -
Media:
VIDEOLIST,YOUTUBE
You can see all of them in the live demo, which is the package's example app built for the web.
Things that matter in production
Old app versions keep working. If an older app gets a type it doesn't know yet, it draws an empty 1px box. So the backend can roll out a new widget before every user has updated.
Your own widgets. Need something the package doesn't have? Register it in the app:
WidgetCatalog.registerBuilder('CUSTOM_BANNER', (entry, callbacks, theme) {
return MyCustomBanner(params: entry.params);
});
Branding. Use one theme object instead of forking the package:
WidgetCatalog.getScreen(
data: data,
callbacks: callbacks,
theme: EcommerceWidgetTheme(
primaryColor: const Color(0xFF7B2CBF),
discountColor: const Color(0xFFE63946),
),
);
For anything the theme can't cover, there's formatPrice for your currency and locale, productCardBuilder to swap in your own product card, and imageErrorBuilder for broken images.
Bonus: let an AI write the screen JSON
The repo ships an AI prompt that describes every widget's schema. Paste it into Claude or ChatGPT and ask for something like "a Black Friday home page with a countdown, a coupon, and a grid of discounted shoes". You get valid JSON back that you can serve as-is. The docs also include an llms.txt, so AI coding assistants can read the whole catalog in one go.
If you use Claude Code, there's also a plugin. It teaches Claude the package API and every widget's schema, so it can wire the package into your app and write screen JSON for you:
/plugin install serenay-ecommerce-widgets --marketplace serenayyazilim/serenay-ecommerce-widgets
Wrap-up
Server-driven UI turns "we need an app release" into "we updated a JSON file". If you're building a Flutter shopping app, try the live demo, read the docs, and if it saves you a sprint, a ⭐ on GitHub or a 👍 on pub.dev helps a lot.
Top comments (1)
The 1px box for unknown types lets you ship new widgets before users update. The harder case is a known type whose params change shape later. An old build may get a FLASHSALE with a renamed field. Then it shows a broken countdown. If the app sends its schema version, the backend can match that build. Do you validate params per widget, or can a bad value reach the builder?