Created with <3 with dartpad.dev.
Last active
July 17, 2023 12:12
-
-
Save stephanedeluca/484c6f384f16d187fb6c1e7f082ef894 to your computer and use it in GitHub Desktop.
Yassine b13
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
import 'package:flutter/material.dart'; | |
const _buildNumber = 13; | |
// lets define the stateless part of the screen (theme, AppBar, app title, background color...) | |
class MetaDataAdScreen extends StatelessWidget { | |
@override | |
Widget build(BuildContext context) { | |
return MaterialApp( | |
debugShowCheckedModeBanner: false, | |
theme: ThemeData(useMaterial3: true, colorSchemeSeed: Colors.amber), | |
darkTheme: | |
ThemeData(useMaterial3: true, colorSchemeSeed: Colors.amber.shade100), | |
themeMode: ThemeMode.light, // Default is system | |
title: "product informations", | |
home: Scaffold( | |
appBar: AppBar( | |
title: const Text( | |
"My product caracteristics form (b$_buildNumber)", | |
style: TextStyle(color: Colors.white), | |
), | |
centerTitle: true, | |
backgroundColor: Colors.green), | |
body: ListRowsGenerator(), | |
), | |
); | |
} | |
} | |
/// A generic field widget implemented as a ListTile | |
/// TODO: I must specialize for all the types I need to support | |
class FieldInput<T> extends StatefulWidget { | |
/// Name of the field | |
final String name; | |
/// The current value | |
final T? value; | |
/// The field to edit | |
final Map<String, dynamic> field; | |
const FieldInput({ | |
super.key, | |
required this.name, | |
required this.field, | |
required this.value, | |
}); | |
@override | |
State<FieldInput<T>> createState() => _FieldInputState<T>(); | |
} | |
class _FieldInputState<T> extends State<FieldInput<T>> { | |
@override | |
String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) => | |
"FieldInput: ${widget.name}: ${widget.field['value']}"; | |
/// The TextFormField | |
Key? _key; | |
/// The initial value | |
T? value; | |
/// the controller for our text field | |
final _controller = TextEditingController(); | |
// late here means: as soon as you created the class instance, do this | |
/// First function called in the life cycle: we set everything we need here | |
@override void initState(){ | |
super.initState(); | |
//debugPrint(_controller.toString()); // Exception because _controller has not been set yet | |
/// The default value the widget got | |
final value = widget.field["value"]; | |
//_controller = TextEditingController(value); | |
// == Is transferred to the text field thru the controller | |
_controller.text = value?.toString()??""; | |
if (widget.name!="model")return; | |
debugPrint(widget.field.toString()); | |
} | |
@override | |
didUpdateWidget(FieldInput<T> oldWidget){ | |
super.didUpdateWidget( oldWidget); | |
value = widget.field["value"]; | |
_key = Key("$value"); | |
if (widget.name!="model")return; | |
debugPrint("value: $value"); | |
} | |
@override | |
Widget build(BuildContext context) { | |
value = widget.field["value"]; | |
/// Provided icon widget or null | |
final icon = widget.field['icon'] == null | |
? const Icon(Icons.circle, color: Colors.transparent) | |
: Icon(widget.field['icon']); | |
/// Provided unit widget or null | |
final unit = widget.field["unit"] == null ? null : Text(widget.field["unit"]); | |
/// Returns the ListTile with potential leading icon and trailing unit. | |
return ListTile( | |
leading: icon, | |
title: TextFormField( | |
//key: _key, | |
/// Provide initialValue of TextFormField the object that sits at key 'value' if and ONLY IF it exists, | |
/// otherwise provide null. Providing means: call toString() method only if field['value'] is not null | |
//initialValue: value?.toString(), //widget.field['value']?.toString()??"€€€€", | |
controller: _controller, | |
autovalidateMode: AutovalidateMode.onUserInteraction, | |
decoration: InputDecoration( | |
labelText: widget.name, | |
), | |
/*keyboardType: switch(T){ | |
case : 'DateTime' | |
{TextInputType.datetime(); | |
} | |
break; | |
case : Phone | |
{TextInputType.phone(); | |
} | |
break; | |
}*/ | |
onChanged: (value) { | |
debugPrint("=> Field ${widget.name}: T is of $T | value: $value"); | |
try { | |
//if (T is String) { | |
setState(() { | |
debugPrint('About to set field["value"]=$value'); | |
try { | |
// = Make sure the value is of the right type | |
if (value is T) { | |
widget.field['value'] = value; | |
} | |
else { | |
// TODO: bubble up the erro to the user even if the errir is code: Temporary user notification | |
ScaffoldMessenger.of(context).showSnackBar( | |
SnackBar( | |
content: Text( | |
'You entered a ${value.runtimeType} where I expect a $T value (Code: 34eF)', | |
style: const TextStyle(color:Colors.white)), | |
backgroundColor: Colors.red, | |
) | |
); | |
} | |
} | |
// TODO: why is the cast exception not catched right here? | |
catch (e) { | |
debugPrint("Error: $e"); | |
ScaffoldMessenger.of(context).showSnackBar( | |
SnackBar( | |
content: Text("An error occured: $e", | |
style: const TextStyle(color:Colors.white)), | |
backgroundColor: Colors.red, | |
) | |
); | |
} | |
//debugPrint('f["value"] set to ${widget.field['value']})'); | |
}); | |
//} | |
//else if (T is int) { | |
//} | |
} | |
catch(e) { | |
debugPrint("Error while copying to value: $e"); | |
} | |
}, | |
validator: (value) { | |
if (value == null || value.isEmpty) return "This field is mandatory"; | |
switch(T.runtimeType) { | |
// Proposed direction | |
case num: | |
final cast = T.tryParse(value); | |
/* | |
case int: | |
final cast = int.tryParse(value); | |
if (cast==null) return "You must enter a valid integer value"; | |
final min = field.containsKey("min"); | |
if (min!=null) { | |
if (min is int) { | |
if (cast<min) return "The minimum value is $min"; | |
} | |
else { | |
return "Min value not supported as we expect an int and we got '$min'"; | |
} | |
} | |
if (max!=null) { | |
if (max is int) { | |
if (cast<max) return "The maximum value is $min"; | |
} | |
else { | |
return "Max value not supported as we expect an int and we got '$min'"; | |
} | |
} | |
break; | |
case double: | |
// TODO: craft some code/class to help handlilng generic type and avoid | |
// code duplication | |
final cast = int.tryParse(value); | |
if (cast==null) return "You must enter a valid integer value"; | |
final min = field.containsKey("min"); | |
if (min!=null) { | |
if (min is int) { | |
if (cast<min) return "The minimum value is $min"; | |
} | |
else { | |
return "Max value not supported as we expect an int and we got '$min'"; | |
} | |
} | |
if (max!=null) { | |
if (max is int) { | |
if (cast<max) return "The maximum value is $min"; | |
} | |
else { | |
return "Max value not supported as we expect an int and we got '$min'"; | |
} | |
} | |
break; | |
*/ | |
case String: | |
} | |
return null; | |
}, | |
), | |
subtitle: Text(value?.toString()??"null"), | |
trailing: unit); | |
} | |
} | |
/// The input field that lets the user pick a value from many | |
//TODO: see how to make EnumInputField share code with other InputField<T> | |
class EnumInputField extends StatefulWidget { | |
/// The list of the enums to pick from | |
final List<String> values; | |
/// The callback that is called with the selected value or null if user cancels | |
final void Function(String?) onSelected; | |
const EnumInputField( | |
{super.key, required this.values, required this.onSelected}); | |
@override | |
State<StatefulWidget> createState() => _EnumInputField(); | |
} | |
class _EnumInputField extends State<EnumInputField> { | |
@override | |
Widget build(BuildContext context) { | |
/// We display a scroll view | |
return ListView( | |
padding: const EdgeInsets.all(20), | |
physics: const BouncingScrollPhysics(), | |
children: [ | |
// == With a title | |
Text("Pick a value", style: Theme.of(context).textTheme.titleLarge), | |
// == A separator | |
const Text(" "), | |
// == The list of the values themselves | |
...widget.values | |
.map((v) => TextButton( | |
onPressed: () { | |
debugPrint("Tap on '$v'"); | |
// == Provide the user-selected value to the parent | |
widget.onSelected(v); | |
debugPrint("Tap on '$v': called onSelected()"); | |
// == Just close the bottom sheet | |
Navigator.of(context).pop(); | |
}, | |
child: Text(v), | |
)) | |
.toList(), | |
const Text(" "), | |
TextButton( | |
onPressed: () { | |
debugPrint("Tap on cancel 'null'"); | |
// == Tell parent user's cancelled | |
widget.onSelected(null); | |
// == Just close the bottom sheet | |
Navigator.of(context).pop(); | |
}, | |
child: const Text("Cancel"), | |
), | |
], | |
); | |
} | |
} | |
// Let's create a class that takes as input the CategoryCarMetaData Map and returns a ListView widget containing in each line | |
// a representation of each element of CategoryCarMetaData. | |
class ListRowsGenerator extends StatefulWidget { | |
@override | |
State<StatefulWidget> createState() => _ListListRowsGeneratorState(); | |
} | |
class _ListListRowsGeneratorState extends State<ListRowsGenerator> { | |
Map<String, dynamic> fields = {}; | |
@override | |
void initState() { | |
super.initState(); | |
fields = getCategoryCarMetaData(); | |
} | |
/// Returns the error widget | |
Widget _errorWidget(String text, Object e, StackTrace stack) { | |
return Container( | |
color: Colors.red, | |
padding: const EdgeInsets.all(20), | |
child: Text("Error $text: $e at:\n$stack", | |
style: const TextStyle(color: Colors.white)), | |
); | |
} | |
@override | |
Widget build(BuildContext context) { | |
// == Select the right InputField generic according to the field type | |
final widgets = fields.keys | |
//.where((k) => k!='type') | |
.map<Widget>((k) { | |
//debugPrint("Field $k..."); | |
/// A shortcut on field k | |
/// diff 2 | |
final f = fields[k]; | |
final value = f["value"]; | |
//debugPrint("Field $k: type is ${f["type"]}"); | |
try { | |
switch (f["type"]) { | |
case "int": | |
try { | |
return FieldInput<int>(name: k, field: f, value: value,); | |
} catch (e, stack) { | |
return _errorWidget("FI<int>", e, stack); | |
} | |
case "enum": | |
//debugPrint("Field values: ${f["values"]}"); | |
try { | |
return ListTile( | |
leading: f['icon'] == null ? null : Icon(f['icon']), | |
title: Text(k), | |
//TODO: treat the case where there is unit and enum (where to put unit ?) | |
trailing: const Icon(Icons.arrow_drop_down), | |
subtitle: f['value'] == null | |
? const Text('pick a value') | |
: Text('${f["value"]}'), | |
onTap: () => showModalBottomSheet( | |
context: context, | |
builder: (context) => EnumInputField( | |
values: f["values"].toList(), | |
onSelected: (value) { | |
debugPrint( | |
"In onSelected: $value | fields[value]=${fields['value']} | Type of f[value] is ${fields['value'].runtimeType}"); | |
setState(() { | |
debugPrint('About to set f["value"]=$value'); | |
try { | |
f ['value'] = value; | |
//fields[k]=f; | |
} catch (e) { | |
debugPrint("Error: $e"); | |
} | |
debugPrint('f["value"] set to ${f['value']})'); | |
}); | |
}, | |
), | |
)); | |
} catch (e, stack) { | |
return _errorWidget("FI <enum>", e, stack); | |
} | |
case "string": | |
try { | |
return FieldInput<String>(name: k, field: f, value: value,); | |
} catch (e, stack) { | |
return _errorWidget("FI<String>", e, stack); | |
} | |
case "date": | |
case "dateTime": | |
try { | |
return FieldInput<DateTime>(name: k, field: f, value: value,); | |
} catch (e, stack) { | |
return _errorWidget("FI<DateTime>", e, stack); | |
} | |
default: | |
return Text("Invalid field $k: $f"); | |
} | |
} catch (e, stack) { | |
return _errorWidget('FI<{$f["type"]}>', e, stack); | |
} | |
}).toList(); | |
return ListView( | |
children: [ | |
ListView( | |
physics: const BouncingScrollPhysics(), | |
shrinkWrap: true, | |
children: widgets, | |
), | |
//Some space | |
const Text(" "), | |
//TODO:Save Button : save all data in a Map to send it to FireBase | |
Center(child: ElevatedButton(onPressed: (){debugPrint('${fields}');}, child: Text(" Save "))), | |
//space | |
const Text(" "), | |
//TODO:Cancel Button : set to deault all value of form | |
Center(child: ElevatedButton(onPressed: () { | |
///set to null all values chosen | |
setState(() { | |
for (String key in fields.keys){fields[key].remove('value');} | |
}); | |
debugPrint('${fields}'); | |
}, | |
child: Text("Discard"))), | |
Center(child: ElevatedButton( | |
onPressed: ()=> setState((){}), | |
child: const Text("setState"))), | |
], | |
); | |
} | |
} | |
void main() { | |
runApp( MetaDataAdScreen()); | |
} | |
/// Get the meta data for the `car` category | |
Map<String, dynamic> getCategoryCarMetaData() { | |
var energies = { | |
"icon": Icons.energy_savings_leaf, | |
"type": "enum", | |
"values": { | |
"electric", | |
"hybrid", | |
"reloaded hybrid", | |
"diesel", | |
"gas", | |
"hydrogen", | |
"gpl", | |
"lng", | |
"other" | |
}, | |
}; | |
var brands = { | |
"icon": Icons.branding_watermark, | |
"type": "enum", | |
"values": { | |
"mercedes-benz", | |
"bmw", | |
"renault", | |
"citroën", | |
"peugeot", | |
"opel", | |
"vw", | |
"simca", | |
"ford", | |
"gm", | |
"cadillac", | |
"other" | |
}, | |
}; | |
var types = { | |
"icon": Icons.type_specimen, | |
"type": "enum", | |
"values": {"sedan", "suv", "pickup", "truck", "semitruck", "other"}, | |
}; | |
return { | |
"type": types, | |
"energy": energies, | |
"brand": brands, | |
"model": {"type": "string"}, | |
"date": { | |
"icon": Icons.calendar_today, | |
"type": "date", | |
}, | |
"power": { | |
"icon": Icons.power, | |
"type": "int", | |
"unit": "CV", | |
"value": 12, | |
"min": "Yohann", | |
"max": 30 | |
} | |
}; | |
} | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment