Skip to content

Instantly share code, notes, and snippets.

@stephanedeluca
Last active July 17, 2023 12:12
Show Gist options
  • Save stephanedeluca/484c6f384f16d187fb6c1e7f082ef894 to your computer and use it in GitHub Desktop.
Save stephanedeluca/484c6f384f16d187fb6c1e7f082ef894 to your computer and use it in GitHub Desktop.
Yassine b13
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