json_dynamic_widget

Table of Contents

Introduction

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

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

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.

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.

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 registryo. 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
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_rectclip_rect.json
clip_rrectbank_example.json
columnbank_example.json
conditionalconditional.json
containerbank_example.json
cupertino_switchcupertino_switch.json
dropdown_button_form_fieldform.json
expandedconditional.json
fitted_boxfitted_box.json
flat_buttonconditional.json
flexibleform.json
formform.json
gesture_detectorgestures.json
heroasset_images.json
iconcard.json
ignore_pointergestures.json
indexed_stackindexed_stack.json
ink_wellasset_images.json
input_errorinput_error.json
linear_progress_indicatorlinear_progress_indicator.json
list_tilecard.json
list_viewlist_view.json
materialbank_example.json
memory_imageimages.json
network_imageimages.json
opacityopacity.json
paddingbank_example.json
positionedbank_example.json
radioradio.json
raised_buttonraised_button.json
rowbank_example.json
safe_areaform.json
save_contextform.json
scaffoldform.json
set_valueasset_images.json
single_child_scroll_viewbank_example.json
sized_boxbank_example.json
stackalign.json
switchswitch.json
textbank_example.json
text_form_fieldform.json
themetheme.json

Using Variables

Variables can be defined on the JsonWidgetRegistry that is used to render the dynamic widgets. Within the JSON, the template engine uses the mustache format for variable references. Example: {{variableName}}.

A variable can be used in any of the child / children / args values and for certain types of properties, a variable reference iw 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.

Dynamic Functions

Similar to the variables, the JsonWidgetRegistry supports registering dynamic functions that can be called to create values. If a value is a dynamic function then it must begin and end with two pound signs: ##. For example: ##set_value(variableName, 5)##. Dynamic values can refer to variables using the mustache format.

The built in functions are defined below:

Function NameExampleArgsDescription
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.
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
  }
}

Libraries

json_dynamic_widget
json_dynamic_widget_schemas