Member-only story

Server-Driven UI (SDUI) with Flutter:

4 min readJul 21, 2025

Build Dynamic, Updatable Apps via JSON

Hardcoding your app's UI isn’t scalable.

Want to update your app layout without publishing updates to App Store or Play Store?
Want to A/B test layouts?
Want your marketing or backend teams to control UI?

Server-Driven UI (SDUI) makes this possible.

In this article, you'll learn:

  • What SDUI is and why it matters
  • How to implement SDUI using Mirai (Flutter framework)
  • How your backend can control Flutter app UIs via JSON
  • How to build a full dynamic UI rendering app using SDUI
  • Future-proofing apps without frequent releases

What is Server-Driven UI (SDUI)?

In SDUI, your app doesn’t have a hardcoded UI. Instead:

  • Backend sends JSON defining UI components, layout, and content
  • Flutter app parses JSON and renders the UI dynamically
  • No need for app updates when UI changes
  • Think of it as React Native’s declarative model — but powered by the server

Example server response:

{
"type": "Column",
"children": [
{ "type": "Text", "value": "Welcome to SDUI!" },
{ "type": "Button", "value": "Click Me", "action": "showAlert" }
]
}

Your app dynamically builds UI using this JSON.

Framework Choice: Why Mirai?

  • Mirai is an open-source SDUI engine for Flutter
  • Actively maintained
  • Simple JSON-driven widget rendering
  • Extensible with custom widgets and actions
  • Production-ready
Press enter or click to view image in full size

Setting Up Mirai in Flutter

Add Dependency:

dependencies:
mirai: ^0.0.5

Create Main Widget:

import 'package:flutter/material.dart';
import 'package:mirai/mirai.dart';

void main() {
runApp(const MyApp());
}

class MyApp extends StatelessWidget {
const MyApp({super.key});

@override
Widget build(BuildContext context) {
return MaterialApp(
home: SDUIScreen(),
);
}
}

Create SDUI Rendering Screen:

class SDUIScreen extends StatefulWidget {
const SDUIScreen({super.key});

@override
State<SDUIScreen> createState() => _SDUIScreenState();
}

class _SDUIScreenState extends State<SDUIScreen> {
late Future<Map<String, dynamic>> _jsonUI;

@override
void initState() {
super.initState();
_jsonUI = fetchUIFromServer();
}

Future<Map<String, dynamic>> fetchUIFromServer() async {
await Future.delayed(const Duration(seconds: 2)); // Simulate API
return {
"type": "Column",
"mainAxisAlignment": "center",
"children": [
{"type": "Text", "value": "Dynamic UI Rendered from Server!"},
{
"type": "ElevatedButton",
"child": {"type": "Text", "value": "Click Here"},
"onPressed": {"actionType": "showAlert"}
}
]
};
}

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Server-Driven UI')),
body: FutureBuilder(
future: _jsonUI,
builder: (context, snapshot) {
if (snapshot.connectionState != ConnectionState.done) {
return const Center(child: CircularProgressIndicator());
}

if (snapshot.hasError) {
return Center(child: Text('Failed to load UI'));
}

final uiJson = snapshot.data!;
return Mirai.fromJson(uiJson, context);
},
),
);
}
}

Now, your UI is rendered fully from server-sent JSON.

Adding Actions (Example: showAlert)

Mirai supports actions like navigation, showing dialogs, or triggering custom code.

Define Action Handler:

void handleAction(Map<String, dynamic> action) {
if (action['actionType'] == 'showAlert') {
showDialog(
context: context,
builder: (_) => AlertDialog(
title: const Text('Button Clicked!'),
content: const Text('This button was rendered from server JSON.'),
actions: [
TextButton(onPressed: () => Navigator.pop(context), child: const Text('OK'))
],
),
);
}
}

Update Widget Rendering:

In your button’s onPressed, call your action handler using Mirai's callback system.

Mirai also supports registering custom actions globally using Mirai.registerActions.

Backend JSON Example (Expandable)

Here’s an advanced JSON layout your backend can send:

{
"type": "Column",
"children": [
{ "type": "Image", "src": "https://example.com/image.png" },
{ "type": "Text", "value": "Limited Offer!", "style": { "fontSize": 24 } },
{
"type": "ElevatedButton",
"child": { "type": "Text", "value": "Buy Now" },
"onPressed": { "actionType": "navigate", "route": "/checkout" }
}
]
}

Without pushing an update, your app can show:

  • Banners
  • New product layouts
  • Button-based promotions

Backend teams control your UI layout directly.

Extending Mirai with Custom Widgets

Need a widget that Mirai doesn’t support?

  • Create a custom widget parser
  • Register your widget parser using Mirai.registerWidgetParser

Example use cases:

  • Custom analytics triggers
  • Lottie animations
  • Advanced card widgets

Mirai is modular and extensible.

Why Use SDUI?

  • Dynamic content updates without app releases
  • A/B testing layouts without developer intervention
  • Backend-driven promotional banners
  • Feature flag-driven UI changes
  • Content teams can manage app UI

Beyond Mirai: Alternatives

  • DivKit (by Yandex) — heavy-duty but Russian-language docs
  • Bricks (by Nubank) — less open
  • Beagle — for cross-platform, heavier setup
  • Custom Engine — for full control, build your own JSON-to-UI parser

For Flutter specifically, Mirai is the best balance of simplicity and power.

Conclusion: Is SDUI for Every App?

No. Use SDUI when:

  • UI needs frequent updates
  • Non-developers manage content
  • A/B testing or promotional UI is required
  • Minimizing app releases matters (e.g., enterprise apps, banking apps)

Avoid SDUI when:

  • UI is static or rarely changes
  • App complexity doesn’t justify dynamic rendering
Santhosh Adiga U

Written by Santhosh Adiga U

Founder & CEO @Anakramy | Mobile Dev (10+ yrs) | Flutter Expert (6 yrs) | Cybersecurity & Bug Bounty Hunter 🛡️ | Top 1% @TryHackMe | 300+ CTFs

No responses yet

Surajbhandari
Surajbhandari



More from Santhosh Adiga U

Comments