serve_dynamic_ui 是一个开源的 Flutter 服务端驱动 UI 库。通过 JSON 在 Flutter 中创建动态小部件,并可扩展以创建您自己的动态小部件。

Screenshot 2023-07-10 at 9 07 05 PM

安装

安装 server_dynamic_ui

运行命令

  flutter pub add serve_dynamic_ui

功能

features

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_serializationjson_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 {}
}

handleActionActionHandler 中处理逻辑任务的唯一方法。

现在,进行注册。

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:stateCachecontrollerCache,它们存储状态类和控制器类。这样,对于唯一的微件实例,您将拥有状态类和控制器类的单个实例。

  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,它具有 onScrolledonScrolledToEndonScrolledToStart 方法,这些方法都有 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。?

GitHub

查看 Github