This is a demo of a 'Flutter Lowder' app.
import 'package:flutter/material.dart';
import 'package:lowder/factory/action_factory.dart';
import 'package:lowder/factory/property_factory.dart';
import 'package:lowder/factory/widget_factory.dart';
import 'package:lowder/widget/lowder.dart';
import 'factory/actions.dart';
import 'factory/properties.dart';
import 'factory/widgets.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
runApp(DemoApp());
}
class DemoApp extends Lowder {
DemoApp({super.environment, super.editorMode, super.editorServer, super.key})
: super("Demo Solution");
@override
AppState createState() => _DemoAppState();
@override
WidgetFactory createWidgetFactory() => SolutionWidgets();
@override
ActionFactory createActionFactory() => SolutionActions();
@override
PropertyFactory createPropertyFactory() => SolutionProperties();
@override
List<SolutionSpec> get solutions => [
SolutionSpec(
"Demo Solution",
filePath: "assets/solution.low",
widgets: SolutionWidgets(),
actions: SolutionActions(),
properties: SolutionProperties(),
),
];
@override
getTheme() => ThemeData.dark(useMaterial3: true);
}
class _DemoAppState extends AppState with WidgetsBindingObserver {
@override
void initState() {
WidgetsBinding.instance.addObserver(this);
super.initState();
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
}
Below is an example of how to make new Widgets available to the Editor.
Here we're registering a new Widget named "EditableText", with the resolver function "buildEditableText", and four properties named "alias", "value", "style" and "editableStyle". The resolver will build a [StatefulBuilder] that can switch from a [Text] to a [TextFormField], so the user can edit its value.
The "alias" property refers to a key on the Screen's state
Map for obtaining and storing its value.
The "value" property sets the Widget's initial value.
The "style" and "editableStyle" properties enables style customization for the [Text] and [TextFormField] widgets.
import 'package:flutter/material.dart';
import 'package:lowder/factory/widgets.dart';
import 'package:lowder/factory/widget_factory.dart';
import 'package:lowder/model/editor_node.dart';
class SolutionWidgets extends WidgetFactory with IWidgets {
@override
void registerWidgets() {
registerWidget("EditableText", buildEditableText, properties: {
"alias": Types.string,
"value": Types.string,
"style": Types.textStyle,
"editableStyle": Types.textStyle,
});
}
Widget buildEditableText(BuildParameters params) {
bool editable = false;
final style = Types.textStyle.build(params.props["style"]);
final editableStyle = Types.textStyle.build(params.props["editableStyle"]);
final alias = params.props["alias"] ?? params.id;
final value = "${params.props["value"] ?? ""}";
final controller = TextEditingController()..text = value;
return StatefulBuilder(builder: (context, setState) {
Widget child;
if (!editable) {
child = Text(controller.text, style: style);
} else {
child = TextFormField(
controller: controller,
style: editableStyle ?? style,
onSaved: (val) {
if (alias != null) {
params.state[alias] = val;
}
},
);
}
return Row(
children: [
Expanded(child: child),
if (!editable)
InkWell(
onTap: () => setState(() => editable = !editable),
child: const Icon(Icons.edit),
)
],
);
});
}
}
Below is an example of how to make new Actions available to the Editor.
Here we're registering a new Action named "Math", with the resolver function "onMath", and three properties named "input", "operation" and "value". Both "input" and "value" are double, while "operation" is a set of possible values. The resolver will execute an addition, subtraction, multiplication, or division using the "input" and "value" values and return the result.
import 'package:lowder/factory/actions.dart';
import 'package:lowder/factory/action_factory.dart';
import 'package:lowder/model/action_context.dart';
import 'package:lowder/model/editor_node.dart';
import 'package:lowder/model/node_spec.dart';
import 'package:lowder/util/parser.dart';
class SolutionActions extends ActionFactory with IActions {
@override
void registerActions() {
registerAction("Math", onMath, properties: {
"input": Types.double,
"operation": const EditorPropertyListType(
["add", "subtract", "multiply", "divide"]),
"value": Types.double
});
}
Future<ActionResult> onMath(
NodeSpec action, ActionContext context) async {
final input = parseDouble(action.props["input"]);
final value = parseDouble(action.props["value"]);
late double result;
switch (action.props["operation"] ?? "") {
case "add":
result = input + value;
break;
case "subtract":
result = input - value;
break;
case "multiply":
result = input * value;
break;
case "divide":
result = input / value;
break;
default:
ActionResult(false);
}
return ActionResult(true, returnData: result);
}
}
Below is an example of how to make new Properties available to the Editor.
Here we're registering a new Property named "BorderSide", with the resolver function "getBorderSide", and two properties named "color" and "width".
The "color" value sets the color of the [BorderSide]. The "width" value sets the width of the [BorderSide].
import 'package:flutter/material.dart';
import 'package:lowder/factory/properties.dart';
import 'package:lowder/factory/property_factory.dart';
import 'package:lowder/model/editor_node.dart';
import 'package:lowder/util/parser.dart';
class SolutionProperties extends PropertyFactory with IProperties {
@override
void registerProperties() {
registerSpecType("BorderSide", getBorderSide, {
"color": Types.color,
"width": Types.int,
});
}
BorderSide? getBorderSide(Map? spec) {
if (spec == null || spec.isEmpty) {
return null;
}
return BorderSide(
color: parseColor(spec["color"], defaultColor: Colors.black),
width: parseDouble(spec["width"], defaultValue: 1.0),
);
}
}
Now a Widget can use this property like this:
registerWidget("DemoWidget", (params) {
return Container(
width: params.buildProp("width"),
height: params.buildProp("height"),
color: params.buildProp("color"),
decoration: BoxDecoration(
border: Border.fromBorderSide(params.buildProp("borderSide"))),
);
}, properties: {
"width": Types.double,
"height": Types.double,
"color": Types.color,
"borderSide": const EditorPropertyType("BorderSide"),
});
A value of a property can be a reference to objects within an evaluation context.
String evaluation occurs upon building a Widget or an Action, in which each of the Node's properties will be sanitized.
An object can be refered using a syntax like ${state.address}
. In this case we're referring to the key address
of the Screen's state
Map.
Some of the objects are:
-
env
: refers to the Model'senvironment variables
, managed in the editor. E.g.${env.api_url}
. -
global
: refers to a static Map accessible viaLowder.globalVariables
where global key/value pairs can be stored, like the user's profile or an access token. E.g.${global.user.name}
. -
state
: refers to a Map each Screen has where its state is stored. E.g.${state.email}
. -
media
: a Map containing some media properties likeisWeb
,isMobile
,isAndroid
,portrait
, etc. -
entry
: available only when working with a List, where upon building each row, theentry
object will be available referring to an element of the array of data.${entry.firstName} ${entry.lastName}
.
Feel free to make your own objects available during evaluation by overriding the method getEvaluatorContext
of the PropertyFactory
.