Skip to content

Instantly share code, notes, and snippets.

@slightfoot
Last active December 6, 2023 22:52
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save slightfoot/4c522d8c6fb90b00df615ec69d250125 to your computer and use it in GitHub Desktop.
Save slightfoot/4c522d8c6fb90b00df615ec69d250125 to your computer and use it in GitHub Desktop.
Widget Inflation Example of possible server-side rendering - by Simon Lightfoot - Humpday Q&A :: 29th November 2023 #Flutter #Dart - https://www.youtube.com/live/jpJw552x1-c?t=6154
// MIT License
//
// Copyright (c) 2023 Simon Lightfoot
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//
import 'dart:async';
import 'package:xml/xml.dart' as xml;
import 'package:flutter/services.dart' show rootBundle;
import 'package:flutter/material.dart';
import 'package:collection/collection.dart';
void main() async {
String asset = 'assets/test.xml';
//String xmlData = await rootBundle.loadString(asset);
runApp(MaterialApp(
debugShowCheckedModeBanner: false,
home: Material(
//child: Inflater(xmlData: xmlData),
child: FutureInflater(asset: asset),
),
));
}
class Inflater extends StatelessWidget {
const Inflater({
super.key,
required this.xmlData,
});
final String xmlData;
@override
Widget build(BuildContext context) {
return expandXmlToWidgets(xmlData);
}
}
class FutureInflater extends StatefulWidget {
const FutureInflater({
super.key,
required this.asset,
});
final String asset;
@override
State<FutureInflater> createState() => _FutureInflaterState();
}
class _FutureInflaterState extends State<FutureInflater> {
late Future<Widget> _future;
@override
void initState() {
super.initState();
_future = inflate();
}
@override
void reassemble() {
super.reassemble();
_future = inflate();
}
Future<Widget> inflate() async {
try {
final xmlData = await rootBundle.loadString(widget.asset);
return expandXmlToWidgets(xmlData);
} catch (e, stackTrace) {
debugPrint('$e\n$stackTrace');
rethrow;
}
}
@override
Widget build(BuildContext context) {
return FutureBuilder(
future: _future,
builder: (BuildContext context, AsyncSnapshot<Widget> snapshot) {
if (snapshot.hasData) {
return snapshot.requireData;
} else if (snapshot.hasError) {
return ErrorWidget(snapshot.error!);
} else {
return const Center(
child: CircularProgressIndicator(),
);
}
},
);
}
}
Widget expandXmlToWidgets(String xmlData) {
final doc = xml.XmlDocument.parse(xmlData);
return _XmlExpander().visit(doc);
}
class _XmlExpander with xml.XmlVisitor {
@override
dynamic visit(xml.XmlHasVisitor node) {
return super.visit(node);
}
dynamic visitWidget(xml.XmlHasVisitor node) {
try {
return visit(node);
} catch (e, stackTrace) {
debugPrint('$e\n$stackTrace');
return ErrorWidget(e);
}
}
@override
Widget? visitDocument(xml.XmlDocument document) {
List<Widget?> children = document.children
.whereType<xml.XmlElement>()
.map((node) => visitWidget(node))
.whereType<Widget>()
.toList();
assert(children.length == 1, 'length: ${children.length}');
return children[0];
}
@override
dynamic visitElement(xml.XmlElement element) {
final inflatable = _registry[element.name.local];
if (inflatable != null) {
final attrs = Map.fromEntries(
element.attributes
.map((a) {
dynamic value = visitAttribute(a);
return value != null
? MapEntry<String, dynamic>(a.name.local, value)
: null;
})
.where((widget) => widget != null)
.whereType<MapEntry<String, dynamic>>(),
);
attrs['text'] = element.innerText;
final children = <Widget>[];
for (final child in element.children.whereType<xml.XmlElement>()) {
final result = visitWidget(child);
if (result == null) continue;
if (child.name.prefix == 'attr') {
attrs[child.name.local] = result;
} else if (result is List<Widget?>) {
children.addAll(result.whereType());
} else if (result is Widget) {
children.add(result);
}
}
if (inflatable is _InflaterLeafFunction) {
return inflatable(attrs);
} else if (inflatable is _InflaterSingleFunction) {
assert(children.length <= 1);
return inflatable(attrs, children.isNotEmpty ? children[0] : null);
} else if (inflatable is _InflaterMultiFunction) {
return inflatable(attrs, children);
} else {
print('Inflatable incorrect: ${element.name}');
return null;
}
} else {
final childElements =
element.children.whereType<xml.XmlElement>().toList();
if (childElements.isEmpty) {
return null;
}
final widgets =
childElements.map(visitWidget).whereType<Widget>().toList();
return widgets.length == 1 ? widgets[0] : widgets;
}
}
@override
dynamic visitAttribute(xml.XmlAttribute node) {
if (node.name.prefix != null) {
return null;
}
switch (node.name.local) {
case 'padding':
return double.parse(node.value);
case 'width':
return double.parse(node.value);
case 'height':
return double.parse(node.value);
case 'color':
if (node.value.startsWith('0x')) {
return Color(int.parse(node.value.substring(2), radix: 16));
} else {
return _colors[node.value];
}
case 'textAlign':
return TextAlign.values.singleWhereOrNull((e) => e.name == node.value);
case 'mainAxisAlignment':
return MainAxisAlignment.values
.singleWhereOrNull((e) => e.name == node.value);
case 'crossAxisAlignment':
return CrossAxisAlignment.values
.singleWhereOrNull((e) => e.name == node.value);
case 'icon':
return _icons[node.value];
default:
print('visitAttribute: ${node.name.local} = ${node.value}');
return null;
}
}
}
final _icons = <String, IconData>{
'business': Icons.business,
'add': Icons.add,
'more_vert': Icons.more_vert,
};
final _colors = <String, Color>{
'white': Colors.white,
'black': Colors.black,
'pink': Colors.pink,
'purple': Colors.purple,
'deepPurple': Colors.deepPurple,
'indigo': Colors.indigo,
'blue': Colors.blue,
'lightBlue': Colors.lightBlue,
'cyan': Colors.cyan,
'teal': Colors.teal,
'green': Colors.green,
'lightGreen': Colors.lightGreen,
'lime': Colors.lime,
'yellow': Colors.yellow,
'amber': Colors.amber,
'orange': Colors.orange,
'deepOrange': Colors.deepOrange,
'brown': Colors.brown,
'blueGrey': Colors.blueGrey,
};
final _registry = <String, dynamic>{
'Text': (Map<String, dynamic> attrs) {
return Text(
attrs['text'] as String? ?? '',
textAlign: attrs['textAlign'],
style: TextStyle(
color: attrs['color'],
),
);
},
'Column': (Map<String, dynamic> attrs, List<Widget> children) {
return Column(
mainAxisAlignment: attrs['mainAxisAlignment'] as MainAxisAlignment? ??
MainAxisAlignment.start,
crossAxisAlignment: attrs['crossAxisAlignment'] as CrossAxisAlignment? ??
CrossAxisAlignment.center,
children: children,
);
},
'Row': (Map<String, dynamic> attrs, List<Widget> children) {
return Row(
mainAxisAlignment: attrs['mainAxisAlignment'] as MainAxisAlignment? ??
MainAxisAlignment.start,
crossAxisAlignment: attrs['crossAxisAlignment'] as CrossAxisAlignment? ??
CrossAxisAlignment.center,
children: children,
);
},
'Card': (Map<String, dynamic> attrs, Widget? child) {
return Card(
color: attrs['color'] as Color?,
child: child,
);
},
'SizedBox': (Map<String, dynamic> attrs, Widget? child) {
return SizedBox(
width: attrs['width'] as double?,
height: attrs['height'] as double?,
child: child,
);
},
'Center': (Map<String, dynamic> attrs, Widget? child) {
return Center(child: child);
},
'Padding': (Map<String, dynamic> attrs, Widget? child) {
return Padding(
padding: attrs['padding'] != null
? EdgeInsets.all(attrs['padding'] as double)
: EdgeInsets.zero,
child: child,
);
},
'Container': (Map<String, dynamic> attrs, Widget? child) {
return Container(
color: attrs['color'] as Color?,
width: attrs['width'] as double?,
height: attrs['height'] as double?,
child: child,
);
},
'Scaffold': (Map<String, dynamic> attrs) {
return Scaffold(
appBar: attrs['appBar'] as PreferredSizeWidget?,
body: attrs['body'] as Widget?,
bottomNavigationBar: attrs['bottomNavigationBar'] as Widget?,
);
},
'AppBar': (Map<String, dynamic> attrs) {
return AppBar(
title: attrs['title'] as Widget?,
actions: attrs['actions'] as List<Widget>?,
);
},
'IconButton': (Map<String, dynamic> attrs) {
return IconButton(
onPressed: () {}, // FIXME: somehow!?
icon: Icon(attrs['icon'] as IconData),
);
},
};
typedef _InflaterLeafFunction = Widget Function(Map<String, dynamic> attrs);
typedef _InflaterSingleFunction = Widget Function(
Map<String, dynamic> attrs, Widget? child);
typedef _InflaterMultiFunction = Widget Function(
Map<String, dynamic> attrs, List<Widget> children);
<?xml version="1.0" encoding="UTF-8"?>
<Scaffold xmlns:attr="https://flutter.dev/layout/attr">
<custom class="WidgetAbc"/>
<attr:appBar>
<AppBar>
<attr:title>
<Text>Testing AppBar</Text>
</attr:title>
<attr:actions>
<IconButton icon="add" onPressed="onAdd"/>
<IconButton icon="business" onPressed="onBusiness"/>
<IconButton icon="more_vert" onPressed="onMore"/>
</attr:actions>
</AppBar>
</attr:appBar>
<attr:body>
<Container color="0xFFDEDEDE">
<Center>
<SizedBox width="320" height="320">
<Card color="indigo">
<Padding padding="24">
<Column mainAxisAlignment="center"
crossAxisAlignment="stretch">
<SizedBox height="16"/>
<Row mainAxisAlignment="spaceEvenly">
<Text>Hello</Text>
<SizedBox width="100" height="20">
<Container color="0xFF009688"/>
</SizedBox>
<Text>World</Text>
</Row>
<SizedBox height="16"/>
<Container
color="0xFF0000FF"
width="200"
height="40"/>
<SizedBox height="32"/>
<Text textAlign="start" color="white">One</Text>
<Text textAlign="center" color="cyan">Two</Text>
<Text textAlign="end" color="yellow">Three</Text>
</Column>
</Padding>
</Card>
</SizedBox>
</Center>
</Container>
</attr:body>
</Scaffold>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment