json_dynamic_widget 5.0.0 icon indicating copy to clipboard operation
json_dynamic_widget: ^5.0.0 copied to clipboard

A library to dynamically generate widgets within Flutter from JSON or other Map-like structures.

json_dynamic_widget #

Table of Contents #

Live Example #

First Party Plugins #

Here's a list of first party plugins that exist for this library.

Migration to 4.X.X version #

With 4.0.0 version the new syntax has been introduced for JSONs. The way of expressing variables and functions has been change. Total table of change and the way how to migrate the JSON definitions to support new format.

The changePreviousNew
Variables syntax{{varName}}${varName}
Function syntax##func1(string,{{varName}})##${func1('string', varName)}
Lack of static variables!{{staticVarName}}All listen variables can be defined as "listen":["var1","var2"] in JSON. Lack of the staticVarName in such an array is equivalent of static variable in the previous syntax
Removed JsonPath from variables{{dynamic;$.person.firstName}}${dynamic['person']['firstName']}
Removed NamedFunctionArg##myFunction(key:keyName, value:{{value}})##${myFunction({"key":'keyName','value':value})}
Escaping of commas is not needed in function params##sayHello(Hello\\, {{firstName}}!)##${sayHello('Hello, '+ firstName)}

Introduction #

Important Note: Because this library allows for dynamic building of Icons, Flutter's built in tree shaker for icons no longer has the ability to guarantee what icons are referenced vs not. Once you include this as a dependency, you must add the --no-tree-shake-icons as a build flag or your builds will fail.

Example:

flutter build [apk | web | ios | ...] --no-tree-shake-icons

This library provides Widgets that are capable of building themselves from JSON structures. The general structure follows:

{
  "type": "<lower_case_type>",
  "args": {
    "...": "..."
  },
  "child": {
    "...": "..."
  },
  "children": [{
    "...": "...",
  }],
  "listen": []
}

Where the child and children are mutually exclusive. From a purely technical standpoint, there's no difference between passing in a child or a children with exactly one element.

The listen array is used to define variable names that specified JsonWidgetData listen to. Thanks to that JsonWidgetData will be rebuilt with every change of such a variables. In case of not defining such a array the JsonWidgetRegistry will try to built such a array dynamically and use any met variable. Good practice is to define it by a hand to reduce amount of rebuilds.

See the documentation and / or example app for the currently supported widgets. All built types are encoded using a lower-case and underscore separator as opposed to a camel case strategy. For instance, a ClipRect will have a type of clip_rect.

Once you have the JSON for a widget, you will use the JsonWidgetData to build the resulting Widget that can be added to the tree. For performance reasons, the JsonWidgetData should be instantiated once and then cached rather than created in each build cycle.

Example

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

class MyStatefulWidget extends StatefulWidget {
  MyStatefulWidget({
    @required this.jsonData,
    this.registry,
    Key key,
  }): assert(jsonData != null),
    super(key: key)

  final Map<String, dynamic> jsonData;
  final JsonWidgetRegistry registry;

  @override
  _MyWidgetState createState() => _MyWidgetState();
}

class _MyWidgetState extends State<MyStatefulWidget> {
  @override
  void initState() {
    super.initState();

    _data = JsonWidgetData.fromDynamic(widget.jsonData);
  }

  @override
  Wiget build(BuildContext context) => _data.build(
    context, 
    registry: widget.registry ?? JsonWidgetRegistry.instance,
  );
}

Understanding the Registry #

The JsonWidgetRegistry is the centralized processing warehouse for building and using the JSON Dynamic Widgets. Widgets must be registered to the registry to be available for building. The registry also supports providing dynamic variables and dynamic functions to the widgets that it builds. The Registry is also repsonsible for processing JsonWidgetData args to their real values via arg processors. Users can define their own arg processors which is giving the possibility to define the custom syntax.

When a value changes on the registry, it posts a notification to the valueStream so any potential processing logic can be executed. The dynamic widgets that use variable values also listen to this stream so they can update their widget state when a value they use for rendering change.

The registry always has a default instance that will be used when a substitute registry is not given. Substitute registeries can be created and used to isolate variables and functions within the app as needed. For instance, you may want a separate registry per page if each page may set dynamic values on the registry. This can prevent the values from one page being overwritten by another.

Built In Widgets #

The structure for all the args is defined in each widget builder, which are defined below:

Widget BuildersExample Location
alignalign.json
animated_alignanimated_align.json
animated_containeranimated_container.json
animated_cross_fadeanimated_cross_fade.json
animated_default_text_styleanimated_default_text_style.json
animated_opacityanimated_opacity.json
animated_paddinganimated_padding.json
animated_physical_modelanimated_physical_model.json
animated_positionedanimated_positioned.json
animated_positioned_directionalanimated_positioned_directional.json
animated_sizeanimated_size.json
animated_switcheranimated_switcher.json
animated_themeanimated_theme.json
app_baralign.json
aspect_ratioaspect_ratio.json
asset_imageasset_images.json
baselinebaseline.json
button_barcard.json
cardcard.json
centercenter.json
checkboxcheckbox.json
circular_progress_indicatorcircular_progress_indicator.json
clip_ovalclips.json
clip_pathclips.json
clip_rectclips.json
clip_rrectbank_example.json
columnbank_example.json
commentscroll_view.json
conditionalconditional.json
containerbank_example.json
cupertino_switchcupertino_switch.json
custom_scroll_viewslivers.json
directionalitydirectionality.json
dropdown_button_form_fieldform.json
dynamicdynamic.json
elevated_buttonbuttons.json
expandedconditional.json
fitted_boxfitted_box.json
flat_buttonconditional.json
flexibleform.json
floating_action_buttonbuttons.json
formform.json
fractional_translationfractional_translation.json
fractionally_sizedfractionally_sized.json
gesture_detectorgestures.json
grid_viewfor_each.json
heroasset_images.json
iconcard.json
icon_buttonbuttons.json
ignore_pointergestures.json
indexed_stackindexed_stack.json
ink_wellasset_images.json
input_errorinput_error.json
interactive_viewerinteractive_viewer.json
intrinsic_heightintrinsic_height.json
intrinsic_widthintrinsic_width.json
limited_boxlimited_box.json
linear_progress_indicatorlinear_progress_indicator.json
list_tilecard.json
list_viewlist_view.json
materialbank_example.json
memory_imageimages.json
network_imageimages.json
offstageoffstage.json
opacityopacity.json
outlined_buttonbuttons.json
overflow_boxoverflow_box.json
paddingbank_example.json
placeholderplaceholder.json
popup_menu_buttonpopup_menu_button.json
positionedbank_example.json
primary_scroll_controllerscroll_view.json
radioradio.json
raised_buttonraised_button.json
rowbank_example.json
safe_areaform.json
save_contextform.json
scaffoldform.json
scroll_configuration_scroll_view.json
scrollbarscroll_view.json
set_default_valueset_default_value.json
set_scroll_controllerscroll_view.json
set_valueset_default_value.json
single_child_scroll_viewbank_example.json
sized_boxbank_example.json
sliver_gridslivers.json
sliver_paddingslivers.json
sliver_listslivers.json
sliver_to_box_adapterslivers.json
stackalign.json
switchswitch.json
textbank_example.json
text_buttonbuttons.json
text_form_fieldform.json
themetheme.json
tooltiptooltip.json
tween_animationtween_animation.json
wrapwrap.json

Using Expressions #

The library since version 4.0.0 has a tight integration with expressions library. By integrating the JsonWidgetRegistry variables and functions with that library there is possible to define different kind of simple expressions placed between ${}.

Using Variables #

Variables can be defined on the JsonWidgetRegistry that is used to render the dynamic widgets.

A variable can be used in any of the child / children / args values and for certain types of properties, a variable reference if the only way to actually assign that value.

Widgets that accept user input will assign that user input to a variable named the value inside of the id option, if an id exists. This allows widgets the ability to listen to input value updates.

There is a possibility to use them in JSON definition thanks to expressions library. Few examples:

${dynamicVariable}
${dynamic['persons'][0]}
${'Hello ' + name}

More examples is available at variables.json.

The built in variables are defined below:

Variable NameExampleDescription
${curveName}_curve
  • ${linear_curve}
  • ${bounce_in_curve}
Provides a const instance of any of the Curves const values. The name of the Curve constant should be transformed into snake_case.

Dynamic Functions #

Basic function usage

Like any other expression functions defined in JsonWidgetRegistry can be used in JSON by placing their name and params between ${}. For example:

${sayHello('Hello,' + firstName)}

Assuming the function sayHello is implemented as, and the firstName variable is "Ted":

print(args[0]);

... then the output of that could would be:

Hello, Ted!

Named args in functions

Additionally we can pass a map to the function:

${myFunction({'key':'keyName', 'value':value})}

Now, in your function, the args will be passed as such:

[
  {"key":"keyName", "value": <<value of the variable from the registry>>}
]

This allows function that take multiple, optional, values to be more easily created and called vs having to do something like...

${myFunction(value, null, null, null, '#ff0000')}

Complex function calls

This is possible to construct really complex function calls:

${func1(func2(func3()+' text'+var1), func4(1+2))}

Built functions

The built in functions are defined below:

Function NameExampleArgsDescription
dynamic${dynamic('operationVar1', 'operationVar2'...)}The variable names which contains values convertable into DynamicOperation.Executes every DynamicOperation passed as args.
for_each${for_each(items['data']['items'], 'templateName', 'value', 'key')}
  1. The variable containing the items to iterate over
  2. The variable containing the template to use when iterating.
  3. Optional: the name of the variable to put the value in
  4. Optional: the name of the variable to put the index or key in
Iterates over the list or map defined by the first arg and builds the widget defined in the template / second argument. The value will be placed in either the variable named value or the passed in third argument. Finally, the index or key will be placed in key or the fourth arg's name.
length${length(myVar)}
  1. The variable or value to return the length from.
Returns the length of the first argument. If the argument is a JSON encoded String, this will first decode it to the native representation. Next, the return value depends on the type of argument. If the arg is a String, a Map, a List, a Set, or an Iterable, the result of calling .length on it will be returned. Otherwise if the arg is an int or a double, the int value of the arg will be returned. Other types will result in an exception.
log${log('my message', 'info')}
  1. The message to write to the logger
  2. Optional: level to log the message at; defaults to finest
Logs the given message out to the logger using the optional level or finest if not set.
navigate_named${navigate_named('home', someValue)}
  1. The route name
  2. Optional: an arguments object to provide
Navigates to the named route. The GlobalKey<NavigatorState> must be provided to the registry before this will work.
navigate_pop${navigate_pop(false)}
  1. Optional: the value to pop with
Pop's the navigator stack. The GlobalKey<NavigatorState> must be provided to the registry before this will work.
noop${noop()}n/aSimple no-arg no-op function that can be used to enable buttons for UI testing.
remove_value${remove_value('varName')}
  1. The variable name
Removes the variable named in the first argument from the registry.
set_value${set_value('varName', 'some value')}
  1. The variable name
  2. The variable value
Sets the value of the variable in the registry.

Creating Custom Widgets #

Creating a custom widget requires first creating a JsonWidgetBuilder for the widget you would like to add.

For example, if you would like to create a new widget that can render a SVG, you would create a SvgWidgetBuilder like the following:

import 'package:child_builder/child_builder.dart';
import 'package:flutter/material.dart';
import 'package:json_class/json_class.dart';
import 'package:json_dynamic_widget/json_dynamic_widget.dart';
import 'package:json_theme/json_theme.dart';
import 'package:meta/meta.dart';
import 'package:websafe_svg/websafe_svg.dart';

class SvgBuilder extends JsonWidgetBuilder {
  SvgBuilder({
    this.asset,
    this.color,
    this.height,
    this.url,
    this.width,
  })  : assert(asset == null || url == null),
        assert(asset != null || url != null);

  static const type = 'svg';

  final String asset;
  final Color color;
  final double height;
  final String url;
  final double width;

  static SvgBuilder fromDynamic(
    dynamic map, {
    JsonWidgetRegistry registry,
  }) {
    SvgBuilder result;

    if (map != null) {
      result = SvgBuilder(
        asset: map['asset'],
        color: ThemeDecoder.decodeColor(
          map['color'],
          validate: false,
        ),
        height: JsonClass.parseDouble(map['height']),
        url: map['url'],
        width: JsonClass.parseDouble(map['width']),
      );
    }

    return result;
  }

  @override
  Widget buildCustom({
    ChildWidgetBuilder childBuilder,
    @required BuildContext context,
    @required JsonWidgetData data,
    Key key,
  }) {
    assert(
      data.children?.isNotEmpty != true,
      '[SvgBuilder] does not support children.',
    );

    return asset != null
        ? WebsafeSvg.asset(
            asset,
            color: color,
            height: height,
            width: width,
          )
        : WebsafeSvg.network(
            url,
            color: color,
            height: height,
            width: width,
          );
  }
}

Widget builders can also have well defined JSON schemas associated to them. If a widget builder has an associated JSON schema then in DEBUG modes, the JSON for the widget will be processed through the schema validator before attempting to build the widget. This can assist with debugging by catching JSON errors early.

An example schema for the SvgWidgetBuilder might look something like this:

import 'package:json_theme/json_theme_schemas.dart';

class SvgSchema {
  static const id =
      'https://your-url-here.com/schemas/svg';

  static final schema = {
    r'$schema': 'http://json-schema.org/draft-06/schema#',
    r'$id': '$id',
    'title': 'SvgBuilder',
    'type': 'object',
    'additionalProperties': false,
    'properties': {
      'asset': SchemaHelper.stringSchema,
      'color': SchemaHelper.objectSchema(ColorSchema.id),
      'height': SchemaHelper.numberSchema,
      'url': SchemaHelper.stringSchema,
      'width': SchemaHelper.numberSchema,
    },
  };
}

Once the builder has been created, it needs to be registered with a JsonWidgetRegistry. This must be done before you ever reference the widget. It's recommended, but not required, that this registration happen in your app's main function.

When registring the widget, you can create a new instance of the registry, or simply get a reference to the default instance, which is the approach below follows.

  var registry = JsonWidgetRegistry.instance;
  registry.registerCustomBuilder(
    SvgBuilder.type,
    JsonWidgetBuilderContainer(
      builder: SvgBuilder.fromDynamic,
      schemaId: SvgSchema.id, // this is optional
    ),
  );

Once the widget is registered, you can safely use the registry to build the widget from JSON. For this example widget, the following JSON would construct an instance:

{
  "type": "svg",
  "args": {
    "asset": "assets/images/visa.svg",
    "color": "#fff",
    "height": 40,
    "width": 56
  }
}

Creating Custom Arg Processor #

Custom arg processors are allowing to extend JSON syntax with custom one.

For example let's create the arg processor which will convert "TRUE" and "FALSE" into booleans as a result of JsonWidgetRegistry args processing.

First the ArgProcessor interface has to be implemented.

import 'package:json_dynamic_widget/json_dynamic_widget.dart';

class BooleanStringArgProcessor implements ArgProcessor {
  final _matchRegexp = RegExp(r'^TRUE|FALSE$');

  @override
  bool support(dynamic arg) {
    return arg != null && arg is String && _matchRegexp.hasMatch(arg);
  }

  @override
  ProcessedArg process(
      JsonWidgetRegistry registry, dynamic arg, Set<String>? listenVariables) {
    var resultListenVariables = listenVariables ?? <String>{};
    var boolStr = _matchRegexp.firstMatch(arg)!.toString();
    return ProcessedArg(
        listenVariables: resultListenVariables,
        value: boolStr == 'TRUE',
    );
  }
}

Then such a processor has to be placed into JsonWidgetRegistry. By default ArgProcessors.defaults are used but there is a possibility to change that via JsonWidgetRegistry.registerArgProcessors.

  var registry = JsonWidgetRegistry.instance;

  registry.registerArgProcessors(
   <ArgProcessor>[BooleanStringArgProcessor()].addAll(ArgProcessors.defaults)
  );

The arg processors are executed from the first to the last one in the list. To make sure that BooleanStringArgProcessor will be used the best is to add it as a first element of the list.