serve_dynamic_ui 是一个开源的 Flutter 服务端驱动 UI 库。通过 JSON 在 Flutter 中创建动态小部件,并可扩展以创建您自己的动态小部件。
安装
安装 server_dynamic_ui
运行命令
flutter pub add serve_dynamic_ui
功能

1. 内置 DynamicWidgets。
- 脚手架
- Container
- SizedBox
- 文本
- 按钮
- Image
- Card
- Column
- Row
- TabView
- ListView
- Loader
- GestureDetector
- Align
- Positioned
- Stack
- TextField
2. 扩展以创建自定义 DynamicWidgets。
3. 内置操作 (目前有导航操作、更新动态小部件操作、处理用户输入操作)。
4. 扩展以创建自定义操作。
5. 调用 DynamicWidget 中的方法。
6. 更新 DynamicWidget 状态。
7. 监听控制器。
8. 处理表单输入。
9. 从资源文件或网络加载 JSON。
入门
使用 serve_dynamic_ui
初始化包。
void main() {
ServeDynamicUI.init();
runApp(const MyApp());
}
从资源文件加载 JSON 并渲染小部件。
@override
Widget build(BuildContext context) {
return ServeDynamicUIMaterialApp(
home: (context) {
return ServeDynamicUI.fromAssets('assets/json/sample.json', context);
},
title: 'Flutter Demo',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.green),
useMaterial3: false,
),
);
}
用法
初始化包。
void main() {
ServeDynamicUI.init();
runApp(const MyApp());
}
JSON 结构
{
"type": "registered_widget_name",
//here you can add properties to configure the dynamic widget.
"data": {
"key": "required value to find the widget in widget tree",
//other properties go here.
}
}
示例 JSON
{
"type": "dy_scaffold",
"data": {
"key": "123456",
"pageTitle": "Flutter Server UI Home Screen",
"child": {
"type": "dy_column",
"data": {
"key": "423243",
"children": [
{
"type": "dy_gesture_detector",
"data": {
"key": "1234234",
"onTapAction": {
"actionString": "/moveToScreen",
"extras": {
"url": "https://raw.githubusercontent.com/Arunshaik2001/demo_server_driven_ui/master/assets/json/dy_scaffold.json",
"urlType": "network",
"requestType": "get",
"navigationType": "screen",
"navigationStyle": "push",
"loaderWidgetAssetPath": "assets/json/loader.json"
}
},
"child": {
"type": "dy_widget_card",
"data": {
"key": "1323",
"margin": "10",
"elevation": 10,
"padding": "10",
"borderRadius": 10,
"body": {
"type": "dy_container",
"data": {
"key": "15345",
"padding": "10,0,0,0",
"child": {
"type": "dy_column",
"data": {
"key": "2457231",
"mainAxisAlignment": "spaceBetween",
"crossAxisAlignment": "start",
"children": [
{
"type": "dy_text",
"data": {
"key": "1734233",
"text": "Scaffold",
"style": {
"color": "0xffff0000",
"fontSize": 20,
"fontWeight": "bold"
}
}
},
{
"type": "dy_container",
"data": {
"key": "813123",
"width": 200,
"child": {
"type": "dy_text",
"data": {
"key": "8327757234",
"text": "a widget that provides basic structure for building app's layout.",
"maxLines": 3
}
}
}
}
]
}
}
}
},
"prefixImage": {
"type": "dy_image",
"data": {
"key": "16323233",
"src": "assets/images/icon_scaffold.png",
"height": 50,
"width": 50,
"imageType": "asset",
"fit": "fill",
"placeholderImagePath": "assets/images/icon_placeholder.png"
}
},
"action": {
"actionString": "/showSnackbar",
"extras": {
"title": "Scaffold"
}
}
}
}
}
},
{
"type": "dy_gesture_detector",
"data": {
"key": "24234234",
"onTapAction": {
"actionString": "/moveToScreen",
"extras": {
"url": "assets/json/container.json",
"urlType": "local",
"navigationType": "screen",
"navigationStyle": "push"
}
},
"child": {
"type": "dy_widget_card",
"data": {
"key": "22323",
"margin": "10",
"elevation": 10,
"padding": "10",
"borderRadius": 10,
"action": {
"actionString": "/showSnackbar",
"extras": {
"title": "Container"
}
},
"body": {
"type": "dy_container",
"data": {
"key": "345345",
"padding": "10,0,0,0",
"child": {
"type": "dy_column",
"data": {
"key": "2457231",
"mainAxisAlignment": "spaceBetween",
"crossAxisAlignment": "start",
"children": [
{
"type": "dy_text",
"data": {
"key": "2232734233",
"text": "Container",
"style": {
"color": "0xffff0000",
"fontSize": 20,
"fontWeight": "bold"
}
}
},
{
"type": "dy_text",
"data": {
"key": "223667757234",
"text": "a widget that holds other widgets"
}
}
]
}
}
}
},
"prefixImage": {
"type": "dy_image",
"data": {
"key": "2226323233",
"src": "https://www.saloodo.com/wp-content/uploads/2021/09/container-1-1.png",
"height": 50,
"width": 50,
"imageType": "network",
"fit": "fill",
"clipBorderRadius": 20,
"placeholderImagePath": "assets/images/icon_placeholder.png"
}
}
}
}
}
}
]
}
}
}
}
从资源文件加载 JSON 并渲染小部件。
@override
Widget build(BuildContext context) {
return ServeDynamicUIMaterialApp(
home: (context) {
return ServeDynamicUI.fromAssets('assets/json/sample.json');
},
title: 'Flutter Demo',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.green),
useMaterial3: false,
),
);
}
从网络加载 JSON 并渲染小部件。
@override
Widget build(BuildContext context) {
return ServeDynamicUIMaterialApp(
home: (context) {
return ServeDynamicUI.fromNetwork(DynamicRequest(
url: 'https://github.com/Arunshaik2001/demo_server_driven_ui/blob/master/assets/json/dy_scaffold.json', requestType: RequestType.get));
},
title: 'Flutter Demo',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.green),
useMaterial3: false,
),
);
}
扩展以创建自定义 DynamicWidget
开发者需要扩展 DynamicWidget 类。
DynamicWidget 看起来是这样的
abstract class DynamicWidget {
@JsonKey(required: true)
final String key;
@JsonKey(includeFromJson: false, includeToJson: false)
DynamicWidget? parent;
DynamicWidget({required this.key, this.parent});
///helps to build in-built flutter widget.
Widget build(BuildContext context);
///this factory constructor takes the json and creates a dynamic widget and its sub children.
factory DynamicWidget.fromJson(Map<String, dynamic> json) {
try {
String type = json[Strings.type];
DynamicWidgetHandler? dynamicWidgetHandler =
DynamicWidgetHandlerRepo.getDynamicWidgetHandlerForType(type);
if (dynamicWidgetHandler != null && json.containsKey(Strings.data)) {
DynamicWidget widget = dynamicWidgetHandler(json[Strings.data]);
List<DynamicWidget?>? children = widget.childWidgets;
children?.forEach((element) {
element?.parent = widget;
});
return widget;
} else {
debugPrint(
'failed to create dynamic widget ${json[Strings.type]} ${json[Strings.data][Strings.key]}');
return DynamicContainer(width: 0.0, showBorder: false);
}
} catch (e) {
debugPrint(
'failed to create dynamic widget ${json[Strings.type]} ${json[Strings.data][Strings.key]}');
return DynamicContainer(width: 0.0, showBorder: false);
}
}
///used to invoke methods in a dynamic widget
FutureOr<dynamic> invokeMethod(
String methodName, {
Map<String, dynamic>? params,
});
List<DynamicWidget?>? get childWidgets;
}
DynamicWidget.fromJson 工厂构造函数,用于将 JSON 转换为 DynamicWidget 对象。
build 方法,用于创建 Widget。
invokeMethod 方法,用于调用 DynamicWidget 中的任何方法。
childWidgets getter,用于维护此小部件下的子小部件。
自定义 DynamicWidget 示例
您可以定义自己的属性,JSON 中的键名应与类属性名相同。此项目使用 json_serialization 和 json_annotation。
class DynamicWidgetCard extends DynamicWidget {
DynamicWidget? prefixImage;
DynamicWidget? body;
double? elevation;
double? borderRadius;
@JsonKey(fromJson: WidgetUtil.getEdgeInsets)
EdgeInsets? margin;
@JsonKey(fromJson: WidgetUtil.getEdgeInsets)
EdgeInsets? padding;
ActionDTO? action;
DynamicWidgetCard({
required String key,
this.prefixImage,
this.body,
this.elevation,
this.borderRadius,
this.margin,
this.padding,
this.action,
}) : super(
key: key,
);
factory DynamicWidgetCard.fromJson(Map<String, dynamic> json) =>
_$DynamicWidgetCardFromJson(json);
@override
Widget build(BuildContext context) {
return GestureDetector(
///just long press on the card.
onLongPress: () {
if (action != null) {
ActionHandlersRepo.handle(action, this, context, (value) {
debugPrint(value);
});
}
},
child: Card(
elevation: elevation,
margin: margin ?? EdgeInsets.zero,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(borderRadius ?? 0),
),
child: Padding(
padding: padding ?? EdgeInsets.zero,
child: Row(
children: [
if (prefixImage != null) prefixImage!.build(context),
if (body != null) body!.build(context),
],
),
),
),
);
}
@override
List<DynamicWidget?>? get childWidgets => [prefixImage, body];
@override
FutureOr invokeMethod(String methodName, {Map<String, dynamic>? params}) {}
}
现在进行注册。
void main() {
Map<String, DynamicWidgetHandler> widgetHandlerMap = {
"dy_widget_card": (json) => DynamicWidgetCard.fromJson(json)
};
ServeDynamicUI.init(
widgetHandlers: widgetHandlerMap,
);
runApp(const MyApp());
}
ServeDynamicUI.init() 接受一个可选参数 widgetHandlers,您可以在其中传递自定义小部件到一个 Map 中,以便添加更多自定义的动态小部件。
创建自定义操作
abstract class ActionHandler {
void handleAction(BuildContext? context, Uri action,
Map<String, dynamic>? extras, OnHandledAction? onHandledAction) async {}
}
handleAction 是 ActionHandler 中处理逻辑任务的唯一方法。
现在,进行注册。
void main() {
Map<RegExp, ActionHandler> actionHandlerMap = {
RegExp(r'(^/?showSnackbar/?$)'): SnackBarActionHandler()
};
ServeDynamicUI.init(
actionHandlers: actionHandlerMap,
);
runApp(const MyApp());
}
这里使用了正则表达式。因此,您需要将操作作为操作字符串传递 *actionString: /actionName?query1=value1&query2=value2 建议遵循以上模式。
所以在 JSON 中是这样的
"onTapAction": {
"actionString": "/moveToScreen",
"extras": {
"url": "https://raw.githubusercontent.com/Arunshaik2001/demo_server_driven_ui/master/assets/json/dy_scaffold.json",
"urlType": "network",
"requestType": "get",
"navigationType": "screen",
"navigationStyle": "push",
"loaderWidgetAssetPath": "assets/json/loader.json"
}
}
您可以在 extras Map 中传递所需值来处理操作。
FormWidget
一个验证输入数据并以 Map 形式获取值的微件。
abstract class FormWidget{
Map<String,dynamic> getValues();
bool validate();
}
像 TextField 一样,用 FormWidget 扩展自定义动态小部件。
class DynamicTextField extends DynamicWidget implements FormWidget {
final String initialText;
TextFieldDTO? textFieldDecoration;
DynamicTextField(
{required String key,
required this.initialText,
this.textFieldDecoration})
: super(key: key);
@override
List<DynamicWidget?>? get childWidgets => [];
@override
Widget build(BuildContext context) {
....
}
@override
Map<String, dynamic> getValues() {
return {Strings.textFieldData: _controller?.text ?? ''};
}
@override
bool validate() {
TextEditingController? controller = _controller;
if (controller?.text != null && (controller?.text.isNotEmpty ?? false)) {
return true;
}
return false;
}
@override
FutureOr invokeMethod(String methodName, {Map<String, dynamic>? params}) {}
}
您可以验证并决定如何发送数据到动态小部件。
在屏幕之间导航
要从一个屏幕导航到另一个屏幕,您需要使用 /moveToScreen 操作。
{
"actionString": "/moveToScreen",
"extras": {
"url": "https://raw.githubusercontent.com/Arunshaik2001/demo_server_driven_ui/master/assets/json/dy_scaffold.json",
"urlType": "network",
"requestType": "get",
"navigationType": "screen",
"navigationStyle": "push",
"loaderWidgetAssetPath": "assets/json/loader.json"
}
}
这里在 extras 中
- url: 这里您可以传递资源路径或网络路径。
- urlType: 定义 url 是
network还是local。 - requestType: 请求类型 (get, post, delete, put)。
- navigationType: 导航类型 (screen, dialog, bottomSheet)。
- loaderWidgetAssetPath: 您可以传递资源路径,当包获取网络 JSON 时,该路径将显示为加载器小部件。
获取表单输入
要在页面上获取用户输入,请使用 /form 操作。
{
"actionString": "/form"
}
更新动态小部件
要更新小部件,您需要使用 /updateWidget 操作,并在 extras Map 中传递 widgetKey 和您想调用的 methodName,并使用 params Map 传递所需数据。
"action": {
"actionString": "/updateWidget",
"extras": {
"widgetKey": "update_text_key",
"methodName": "UPDATE_TEXT",
"params": {
"newText": "Updated Text Value"
}
}
}
重要提示: 要进行状态更改,根小部件必须是 DynamicProvider,但您无需担心,因为包已处理了。
如果您想更新一个动态小部件。首先,创建一个状态类。
我以 DynamicText 类为例给您展示。
class DynamicTextState {
final ValueNotifier<String?> textNotifier;
DynamicTextState(
String? title,
) : textNotifier = ValueNotifier<String?>(title);
void updateTitle(String? newTitle) {
textNotifier.value = newTitle;
}
}
现在,创建一个 getter,它返回一个唯一小部件键的状态类实例。DynamicProvider 有两个 Map:stateCache 和 controllerCache,它们存储状态类和控制器类。这样,对于唯一的微件实例,您将拥有状态类和控制器类的单个实例。
DynamicTextState? __dynamicTextState;
DynamicTextState get _dynamicTextState {
DynamicProvider? dynamicProvider =
WidgetResolver.getTopAncestorOfType<DynamicProvider>(this);
if (dynamicProvider == null) {
return DynamicTextState(text);
}
if (__dynamicTextState != null) {
return __dynamicTextState!;
} else {
if (key == null) {
__dynamicTextState = DynamicTextState(text);
} else {
__dynamicTextState = dynamicProvider.stateCache.putIfAbsent(
key,
() => DynamicTextState(text),
) as DynamicTextState?;
}
}
return __dynamicTextState!;
}
监听控制器 如果您需要监听动态小部件中的滚动控制器或文本控制器。您可以这样做。
对于滚动监听器,请扩展 ScrollListener,它具有 onScrolled、onScrolledToEnd、onScrolledToStart 方法,这些方法都有 widget key,即正在滚动的微件的键。
class WidgetScrollListener extends ScrollListener {
@override
void onScrolled(String? widgetKey) {
debugPrint('onScrolled $widgetKey');
}
@override
void onScrolledToEnd(String? widgetKey) {
debugPrint('onScrolledToEnd $widgetKey');
}
@override
void onScrolledToStart(String? widgetKey) {
debugPrint('onScrolledToStart $widgetKey');
}
}
现在,您可以这样注册它。
DynamicListeners.addListener(WidgetScrollListener());
同样,对于文本更改监听器。
class TextUpdateListener extends TextChangeListener{
@override
void onTextChanged(String? widgetKey, String newValue) {
debugPrint('onTextChanged $widgetKey $newValue');
}
}
添加此作为监听器
DynamicListeners.addListener(TextUpdateListener());
想了解更多,请查看 示例应用程序。
附加信息
请在此 创建 issue。
如果您想为该项目贡献力量。请随时提交 pull requests。?
