flutter_dynamic_forms

repository·master·Indexed 42 days ago

https://github.com/ondrejkunc/flutter_dynamic_forms

A suite of libraries for implementing Server Driven UI in Flutter. It allows developers to define complex, reactive forms and widget trees via XML or JSON, featuring a code generator (dynamic_forms_generator) to automate model and parser creation from YAML definitions, and an expression_language library for parsing and evaluating mathematical and logical expressions in Dart.

Tokens
10.9K
Snippets
30
Records
42
Agent score
50%

What's inside flutter_dynamic_forms

  1. Overview of flutter_dynamic_forms

    master

    The flutter_dynamic_forms project is a collection of Dart and Flutter libraries designed for Server Driven UI. It allows you to define UI components and complex logic (like custom validation rules or conditional visibility) via XML or JSON on a server, which the Flutter client then consumes without requiring an app redeployment.

    Key packages include:

    • expression_language: For handling logic within the forms.
    • dynamic_forms: Core logic for form management.
    • dynamic_forms_generator: For generating form definitions.
    • flutter_dynamic_forms: The main Flutter integration.
    • flutter_dynamic_forms_components: A set of predefined Flutter components (e.g., Label, CheckBox, RadioButtonGroup).
  2. Reference external data using @element.propertyName

    master

    To reference values from an external provider within an expression, use the syntax @element.propertyName.

    • element must be a key in the map passed to the ExpressionGrammarParser constructor and must extend ExpressionProviderElement.
    • element and propertyName must consist only of alphanumeric characters or underscores and cannot start with a number.
  3. Define a component in YAML

    master

    Create a <component_name>.yaml file for each component. The structure defines how the component is represented in models and parsers.

    Structure

    • type (Required): A unique identifier for the component. Can include optional generic parameters (e.g., singleSelectGroup<T>).
    • parentType (Optional): The name of the component to inherit properties and contentProperty from.
    • properties (Optional): A map of component properties.
      • type: The data type of the property.
      • default: The default value (if not set, non-nullable types use defaults like 0, '', false, or min DateTime).
      • isMutable: If true, the property is expected to be sent from client to server (default is false).
    • contentProperty (Optional): The name of the property that allows writing the value directly as a child of the element (used for containers).
    type: checkBox
    parentType: formElement
    properties:
      label:
        type: string
      value:
        type: bool
        default: false
        isMutable: true
  4. Mapping YAML to XML and JSON

    master

    The generator facilitates the conversion of YAML definitions into XML or JSON formats used for form representation.

    XML Mapping

    • The type becomes the element name.
    • parentType is omitted.
    • Attributes: Properties can be written as attributes: <element propertyName="value"/>.
    • Complex Values: Properties can be nested: <element><element.propertyName>value</element.propertyName></element>.
    • Expressions: Non-formElement properties can contain expressions: <element.propertyName><expression>1 + 1</expression></element.propertyName>.
    • Content Property: If contentProperty is defined, the property is written directly inside the element (e.g., <container><child1/></container>).

    JSON Mapping

    • Follows a similar structure to XML, but the contentProperty is ignored.
  5. Supported data types in expression_language

    master

    The library supports several non-nullable data types. Since version 1.0, it also supports nullable types. You can cast away nullability in an expression using the postfix exclamation mark operator !.

    TypeDart Mapping
    StringString
    boolbool
    IntegerWrapper around int (extends Number)
    DecimalCustom type (from dart-decimal) for high-precision financial data
    DateTimeDateTime
    DurationDuration
    NumberBase class for Integer and Decimal (similar to Dart num)
  6. Use expression_language to parse and evaluate expressions

    master

    The expression_language library allows you to parse and evaluate complex mathematical and logical expressions in Dart. It supports arithmetic, logical operations, and referencing external data via the @element.propertyName syntax.

    To use it, you must define an ExpressionGrammarParser with a map of ExpressionProviderElement objects (which act as the data source for your @element references), build the parser, and then evaluate the resulting expression tree.

    // 1. Create parser with a map of elements that can hold expressions
    var expressionGrammarDefinition = 
        ExpressionGrammarParser({"element": TestFormElement()});
    var parser = expressionGrammarDefinition.build();
    
    // 2. Parse the expression string
    var result = parser
        .parse("(1 + @element.value < 3*5) && false || (2 + 3*(4 + 21)) >= 15");
    
    // 3. Cast to the expected typed expression
    var expression = result.value as Expression<bool>;
    
    // 4. Evaluate to get the final result
    bool value = expression.evaluate();
  7. Implement and register custom expressions

    master

    To add custom functions to the expression language, follow these two steps:

    1. Implement the Expression class: Extend Expression<T> (where T is the return type) and implement evaluate(), clone(), and getChildren().
    2. Register the Factory: Pass a FunctionExpressionFactory to the customFunctionExpressionFactories parameter of the ExpressionGrammarParser constructor. You can use ExplicitFunctionExpressionFactory<T> for simple implementations to avoid subclassing the factory itself.

    Example of implementing a custom concat function:

    import 'package:expression_language/expression_language.dart';
    
    // 1. Define the Expression logic
    class StringConcatenationExpression extends Expression<String> {
      final Expression<String> left;
      final Expression<String> right;
    
      StringConcatenationExpression(this.left, this.right);
    
      @override
      String evaluate() => left.evaluate() + right.evaluate();
    
      @override
      Expression<String> clone(Map<String, ExpressionProviderElement> elementMap) {
        return StringConcatenationExpression(left.clone(elementMap), right.clone(elementMap));
      }
    
      @override
      List<Expression> getChildren() => [left, right];
    }
    
    // 2. Register it with the parser
    var expressionGrammarDefinition = ExpressionGrammarParser({}, 
        customFunctionExpressionFactories: [
            ExplicitFunctionExpressionFactory(
                name: 'concat',
                createFunctionExpression: (parameters) => 
                    StringConcatenationExpression(parameters[0], parameters[1]),
                parametersLength: 2),
        ],
    );
  8. How to display a dynamic form

    master

    To render a form, you need to follow a three-step process: choose a parser, provide the data, and render the components.

    1. Choose a Form Manager

    Decide if your form data is XML or JSON. Use XmlFormManager for XML or JsonFormManager for JSON. These managers handle parsing, state manipulation, validation, and data collection.

    2. Initialize with ParsedFormProvider

    Use the ParsedFormProvider widget to initialize your FormManager. It takes the raw XML/JSON content and a list of parsers. It uses the Provider package, making the manager available in the widget subtree via FormProvider.of<YourFormProvider>(context).

    3. Render with FormRenderer

    Use the FormRenderer widget to display the UI. You must specify the type of FormManager being used (e.g., FormRenderer<XmlFormManager>) so it can resolve the manager from the ParsedFormProvider.

    import 'package:flutter/material.dart';
    import 'package:dynamic_forms/dynamic_forms.dart';
    import 'package:flutter_dynamic_forms/flutter_dynamic_forms.dart';
    import 'package:flutter_dynamic_forms_components/flutter_dynamic_forms_components.dart';
    
    class SimpleForm extends StatelessWidget {
      final String xmlString;
    
      const SimpleForm({Key key, this.xmlString}) : super(key: key);
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          body: SingleChildScrollView(
            child: ParsedFormProvider( 
              create: (_) => XmlFormManager(), // Or use JsonFormManager() for JSON
              content: xmlString,
              parsers: getDefaultParserList(),
              child: FormRenderer<XmlFormManager>(
                renderers: getReactiveRenderers(),
              ),
            ),
          ),
        );
      }
    }
  9. Implement a custom component

    master

    To create a custom component in flutter_dynamic_forms, you must implement three distinct classes: a Parser, a Model, and a Renderer. These classes must then be registered when building the form.

    1. Parser: Handles deserialization from XML or JSON. It uses ParserNode to extract values and ElementParserFunction to handle recursive child parsing.
    2. Model: A Flutter-independent class that defines the component's structure and properties. Properties should be defined using the Property<T> syntax to support both simple values and expressions. It is recommended to wrap the internal properties map with getters and setters.
    3. Renderer: A Flutter-specific class that converts the Model into a Widget. It should subscribe to property changes (via the property's valueChanged stream or the component's propertyChanged stream) to ensure the UI updates reactively. Use FormElementRendererFunction for recursive child rendering.
    /* To implement a custom component, you need to provide 3 classes: 
    1. Parser
    2. Model
    3. Renderer
    
    Parsers and Models then need to be registered when you are building the form. */