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 7.X.X version

NOTE: There are several breaking changes in this release from the JSON Schema perspective. Almost all of them can be automatically migrated from v6 to v7 using the Migration CLI.


Code Generation

As of 7.0.0 a code generator exists to simplify the creation of the dynamic widgets. The code generator can generate the Dart / Flutter code to build widgets or it can be used in reverse to generate the JSON / YAML from the Dart / Flutter code. For more advanced information on the code generator, see the Code Generator document. For more information on the JSON / YAML generator see Reverse Encoding.

The code that is now required to build a custom widget with the release of 7.0.0 is followed by the code that used to be required prior to 7.0.0.

>= 7.0.0

import 'package:json_dynamic_widget/json_dynamic_widget.dart';

part 'json_column_builder.g.dart';

@jsonWidget
abstract class _JsonColumnBuilder extends JsonWidgetBuilder {
  const _JsonColumnBuilder({
    required super.args,
  });

  @override
  Column buildCustom({
    ChildWidgetBuilder? childBuilder,
    required BuildContext context,
    required JsonWidgetData data,
    Key? key,
  });
}

< 7.0.0

import 'package:child_builder/child_builder.dart';
import 'package:flutter/material.dart';
import 'package:json_dynamic_widget/json_dynamic_widget.dart';
import 'package:json_theme/json_theme.dart';
import 'package:json_theme/json_theme_schemas.dart';

class JsonColumnBuilder extends JsonWidgetBuilder {
  const JsonColumnBuilder({
    required this.crossAxisAlignment,
    required this.mainAxisAlignment,
    required this.mainAxisSize,
    this.textBaseline,
    this.textDirection,
    required this.verticalDirection,
  }) : super(numSupportedChildren: kNumSupportedChildren);

  static const kNumSupportedChildren = -1;

  static const type = 'column';

  final CrossAxisAlignment crossAxisAlignment;
  final MainAxisAlignment mainAxisAlignment;
  final MainAxisSize mainAxisSize;
  final TextBaseline? textBaseline;
  final TextDirection? textDirection;
  final VerticalDirection verticalDirection;

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

    if (map != null) {
      result = JsonColumnBuilder(
        crossAxisAlignment: ThemeDecoder.decodeCrossAxisAlignment(
              map['crossAxisAlignment'],
              validate: false,
            ) ??
            CrossAxisAlignment.center,
        mainAxisAlignment: ThemeDecoder.decodeMainAxisAlignment(
              map['mainAxisAlignment'],
              validate: false,
            ) ??
            MainAxisAlignment.start,
        mainAxisSize: ThemeDecoder.decodeMainAxisSize(
              map['mainAxisSize'],
              validate: false,
            ) ??
            MainAxisSize.max,
        textBaseline: ThemeDecoder.decodeTextBaseline(
          map['textBaseline'],
          validate: false,
        ),
        textDirection: ThemeDecoder.decodeTextDirection(
          map['textDirection'],
          validate: false,
        ),
        verticalDirection: ThemeDecoder.decodeVerticalDirection(
              map['verticalDirection'],
              validate: false,
            ) ??
            VerticalDirection.down,
      );
    }

    return result;
  }

  @override
  Widget buildCustom({
    ChildWidgetBuilder? childBuilder,
    required BuildContext context,
    required JsonWidgetData data,
    Key? key,
  }) {
    return Column(
      crossAxisAlignment: crossAxisAlignment,
      key: key,
      mainAxisAlignment: mainAxisAlignment,
      mainAxisSize: mainAxisSize,
      textBaseline: textBaseline,
      textDirection: textDirection,
      verticalDirection: verticalDirection,
      children: [
        for (var child in data.children ?? <JsonWidgetData>[])
          child.build(
            context: context,
            childBuilder: childBuilder,
          ),
      ],
    );
  }
}

class ColumnSchema {
  static const id =
      'https://peiffer-innovations.github.io/flutter_json_schemas'
        '/schemas/json_dynamic_widget/column.json';

  static final schema = {
    r'$schema': 'http://json-schema.org/draft-06/schema#',
    r'$id': id,
    r'$children': -1,
    r'$comment': 'https://api.flutter.dev/flutter/widgets/Column-class.html',
    'title': 'Column',
    'oneOf': [
      {
        'type': 'null',
      },
      {
        'type': 'object',
        'additionalProperties': false,
        'properties': {
          'crossAxisAlignment':
              SchemaHelper.objectSchema(CrossAxisAlignmentSchema.id),
          'mainAxisAlignment':
              SchemaHelper.objectSchema(MainAxisAlignmentSchema.id),
          'mainAxisSize': SchemaHelper.objectSchema(MainAxisSizeSchema.id),
          'textBaseline': SchemaHelper.objectSchema(TextBaselineSchema.id),
          'textDirection': SchemaHelper.objectSchema(TextDirectionSchema.id),
          'verticalDirection':
              SchemaHelper.objectSchema(VerticalDirectionSchema.id),
        },
      },
    ],
  };
}

Code Generation Annotations

See the Annotations guide for information on all of the code generation annotations available for use.


Widget composition

To share the same arguments/annotations between multiple builders you can create mixins with the values you need.

In the following example, _ColumnBuilder and _RowBuilder shares the same properties (encode/decode/schema) of children when genreated.

mixin ChildrenArguments {
  @JsonArgDecoder('children')
  List<Widget> _decodeChildren({required value}) { ... };

  @JsonArgEncoder('children')
  static String _encodeChildren(List<Widget> value) { ... };

  @JsonArgSchema('children')
  static Map<String, dynamic> _childrenSchema() => { ... };
}


@jsonWidget
abstract class _ColumnBuilder extends JsonWidgetBuilder with ChildrenArguments { ... }

@jsonWidget
abstract class _RowBuilder extends JsonWidgetBuilder with ChildrenArguments { ... }


Migration CLI

This version comes with a script that can migrate existing JSON / YAML files from v6 to v7 automatically. To run the script, first add the package as a dependency:

dependencies:
  json_dynamic_widget: <version>

Then run:

dart run json_dynamic_widget:migrate_7 [path/to/files]

The script will automatically migrate the files it finds and make a backup using the original name + .bak. If you are satisfied with the output from the migration script, feel free to delete those backup files. For more information, see the Migration CLI documentation


Usage

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 or YAML structures. The general structure follows:

{
  "type": "<lower_case_type>",
  "id": "<optional-id>",
  "listen": [
    "var1",
    "var2",
    "..."
  ],
  "args": {
    "...": "..."
  },
}
---
type: <lower_case_type>
id: <optional-id>
listen: 
  - var1
  - var2
  - ...
args:
  ...: ...

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 Builders Example Location
align align.json
animated_align animated_align.json
animated_container animated_container.json
animated_cross_fade animated_cross_fade.json
animated_default_text_style animated_default_text_style.json
animated_opacity animated_opacity.json
animated_padding animated_padding.json
animated_physical_model animated_physical_model.json
animated_positioned animated_positioned.json
animated_positioned_directional animated_positioned_directional.json
animated_size animated_size.json
animated_switcher animated_switcher.json
animated_theme animated_theme.json
app_bar align.json
aspect_ratio aspect_ratio.json
asset_image asset_images.json
baseline baseline.json
button_bar card.json
card card.json
center center.json
checkbox checkbox.json
circular_progress_indicator circular_progress_indicator.json
clip_oval clips.json
clip_path clips.json
clip_rect clips.json
clip_rrect bank_example.json
column bank_example.json
comment scroll_view.json
conditional conditional.json
container bank_example.json
cupertino_switch cupertino_switch.yaml
custom_scroll_view slivers.json
directionality directionality.json
dropdown_button_form_field form.json
dynamic dynamic.json
elevated_button buttons.json
expanded conditional.json
fitted_box fitted_box.json
flexible form.json
floating_action_button buttons.json
form form.json
fractional_translation fractional_translation.json
fractionally_sized fractionally_sized.json
gesture_detector gestures.json
grid_view for_each.json
hero asset_images.json
icon card.json
icon_button buttons.json
ignore_pointer gestures.json
indexed_stack indexed_stack.json
ink_well asset_images.json
input_error input_error.json
interactive_viewer interactive_viewer.json
intrinsic_height intrinsic_height.json
intrinsic_width intrinsic_width.json
limited_box limited_box.json
linear_progress_indicator linear_progress_indicator.json
list_tile card.json
list_view list_view.json
material bank_example.json
memory_image images.json
network_image images.json
offstage offstage.json
opacity opacity.json
outlined_button buttons.json
overflow_box overflow_box.json
padding bank_example.json
placeholder placeholder.json
popup_menu_button popup_menu_button.json
positioned bank_example.json
primary_scroll_controller scroll_view.json
radio radio.json
rich_text rich_text.json
row bank_example.json
safe_area form.json
save_context form.json
scaffold form.json
scroll_configuration_ scroll_view.json
scrollbar scroll_view.json
set_default_value set_default_value.json
set_scroll_controller scroll_view.json
set_value set_default_value.json
single_child_scroll_view bank_example.json
sized_box bank_example.json
sliver_grid slivers.json
sliver_padding slivers.json
sliver_list slivers.json
sliver_to_box_adapter slivers.json
stack align.json
switch switch.json
text bank_example.json
text_button buttons.json
text_form_field form.json
theme theme.json
tooltip tooltip.json
tween_animation tween_animation.json
visibility visibility.yaml
wrap wrap.json

All the internal builders are added to the registry by default.

It is possible to omit that behavior by using overrideInternalBuilders flag. To select manually the internal functions it is recommended to use JsonWidgetInternalBuildersBuilder. const JsonWidgetBuilderContainer( builder: JsonBottomNavigationBarBuilder.fromDynamic, schemaId: BottomNavigationBarSchema.id )

  JsonWidgetRegistry(
    overrideInternalBuilders: true,
     builders: {
    ...JsonWidgetInternalBuildersBuilder().withColumn().build(),
    ...<String, JsonWidgetBuilderContainer>{
      JsonCustomBuilder.kType: JsonWidgetBuilderContainer(
          builder: JsonCustomBuilder.fromDynamic,
          schemaId: JsonCustomBuilderSchema.id)
    }
  });

Passing the nulls to the args

All explicit nulls like {"key" : null} are removed from the args on parsing level.
Sometimes null value and lack of value are two separate pieces of information and there is a need to pass it up to builder level.

A special syntax must be used to fulfill that need:

{
  "maxLines": "${null}"
}

Example: null_value_passing.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 is 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 are available at variables.json.

The built in variables are defined below:

Variable Name Example Description
${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 Name Example Args Description
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/a Simple 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.

All the internal functions are added to the registry by default.

It is possible to omit that behavior by using overrideInternalFunctions flag. To select manually the internal functions it is recommended to use JsonWidgetInternalFunctionsBuilder.

  JsonWidgetRegistry(
    overrideInternalFunctions: true, 
    functions: {
      ...JsonWidgetInternalFunctionsBuilder().withSetValue().build(),
      ...<String, JsonWidgetFunction>{
        'customFunction': ({args, required registry}) {
          print("This is a custom registry function.");
        },
      }
  });

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 SvgBuilder like the following:

import 'package:flutter_svg/flutter_svg.dart';
import 'package:json_dynamic_widget/json_dynamic_widget.dart';

part 'svg_builder.g.dart';

@jsonWidget
abstract class _SvgBuilder extends JsonWidgetBuilder {
  const _SvgBuilder({
    required super.args,
  });

  @override
  _Svg buildCustom({
    ChildWidgetBuilder? childBuilder,
    required BuildContext context,
    required JsonWidgetData data,
    Key? key,
  });
}

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

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

  @override
  Widget build(BuildContext context) {
    return asset != null
        ? SvgPicture.asset(
            asset!,
            height: height,
            width: width,
          )
        : SvgPicture.network(
            url!,
            height: height,
            width: width,
          );
  }
}

Next, you will need to run the code generator command to generate the glue / binding code. To run the code generator, execute:

dart run build_runner build --delete-conflicting-outputs

Once the code is generated, 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.