Last active
December 6, 2023 22:52
-
-
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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