Created with <3 with dartpad.dev.
Last active
July 5, 2023 23:47
-
-
Save stephanedeluca/599fd35f9c04c6234ca0f24cebed16d3 to your computer and use it in GitHub Desktop.
Shokaze Yassine b2
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'; | |
/// The buid number | |
const _buildNumber = 8; | |
// lets define the stateless part of the screen (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: Text( | |
"My vehicle caracteristics form (b$_buildNumber)", | |
style: TextStyle(color: Colors.white), | |
), | |
centerTitle: true, | |
backgroundColor: Colors.red | |
), | |
body: ListRowsGenerator(), | |
), | |
); | |
} | |
} | |
/// A generic field widget implemented as a ListTile | |
/// TODO: I must specilize for all the type I need to support | |
class FieldInput<T> extends StatelessWidget { | |
/// Name of the field | |
final String name; | |
/// The field to edit | |
final Map<String,dynamic> field; | |
//T value; | |
const FieldInput({ | |
super.key, | |
required this.name, | |
required this.field, | |
}); | |
@override String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) => "FieldInput: $name: ${field['value']}"; | |
@override Widget build(BuildContext context) { | |
/// Providionned icon widget or null | |
final icon = field['icon']==null?null:Icon( field['icon']); | |
// Returns the lit tile with potential leading icon and triling unit. | |
return ListTile( | |
leading: icon, | |
title: TextFormField( | |
initialValue: field['value']?.toString(), | |
// == Always fires the validator | |
autovalidateMode:AutovalidateMode.always, | |
// 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 | |
decoration: InputDecoration( | |
labelText: name, | |
), | |
keyboardType: | |
T is int ? | |
TextInputType.number:T is double? | |
const TextInputType.numberWithOptions(decimal:true) | |
:null | |
, | |
/// Called whenever the user strike a key. The current valye (as a string) sits in value (btw, which could be null if nothing was stoke) | |
onChanged: (value) { | |
field['value'] = value; | |
//setStte() | |
//debugPrint("Field $name: $value"); | |
debugPrint("Field is now: $field"); | |
}, | |
validator:(value){ | |
if (value==null || value.isEmpty) return "This field is mandatory"; | |
if (value.length<4) return 'You must provide at least 4 charcaters'; | |
return null; | |
} | |
), | |
trailing:field["unit"] == null ? null: Text(field["unit"]), | |
); | |
} | |
} | |
/// The input field that let the user pick a value from many | |
//TODO: see how to make EnumInputField shar code with other InputField<T> | |
class EnumInputField extends StatelessWidget { | |
/// The list of the enums To pick from | |
final List<String> values; | |
/// The call back 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 Widget build(BuildContext context) { | |
/// We display a scrollin 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 (Note that I use a stanard text instead of | |
// a metrics, so that everything will stand in shape whatever | |
// the the used) | |
const Text(""), // const SizedBox(10), | |
// == The list of the values themseleves | |
...values.map((v)=> | |
TextButton(onPressed:() { | |
debugPrint("tap on $v"); | |
// == Provide the user-seleted value to the parent | |
onSelected(v); | |
// == Just close the bottom sheet | |
Navigator.of(context).pop(); | |
}, | |
child:Text(v))).toList(), // Operator `...` explodes a list into individuel items => "a", "b", "c", etc. | |
const Text(""), | |
TextButton(onPressed:(){ | |
// == Tell parent user's cancelled | |
onSelected(null); | |
// == Just close the bottom sheet | |
Navigator.of(context).pop(); | |
}, child:Text("Cancel")), | |
] | |
); | |
} | |
} | |
/* | |
/// The form definition from the server | |
final definition = { | |
"age": { | |
"type": "int", | |
} | |
}; | |
/// Once the user filled in, we get: | |
final definition = { | |
"age": { | |
"type": "int", | |
"value": 43, // What the user filled in! | |
} | |
}; | |
*/ | |
/* | |
/// A person model | |
class Person extends Object { | |
/// The person's firstname | |
final String name; | |
/// The Weight of the person | |
final double? weight; | |
/// The ID of the instance | |
late final _id; // private | |
Person({required this.name, this.weight}) { | |
// TODO: Computes a statisically unique ID | |
// FIXME: This bug is referred to as http://atlassian.com/342432ZERZERZ | |
_id = "12"; | |
} | |
@override String toString() => | |
'Person "name" weights ${weight}Kg'; | |
// Replaces [instane@12432ZERZZERZER] of Person] by Person "Yassine" weights 80Kg] | |
} | |
/// The large person (aka over 200lbs) | |
/// My new class is LArge Person and I reach any of its memeber via `this`. | |
/// As it extends Person, you can reach any accessible from PErson via `super`. | |
class LargePerson extends Person { | |
/// Create instances of larg person, with default name as Demis Roussos | |
LargePerson.withDefault({super.name="Demis Roussos", super.weight}); | |
/// Create intances of large person, with sealed name to Demis Roussos | |
LargePerson.onlyWeight({super.weight}):super(name:"Demis Roussos"); | |
} | |
final g = LargePerson.withDefault(weight:250); //Kg | |
final h = LargePerson.withDefault(name:"toto", weight:250); | |
final i = LargePerson.onlyWeight(weight:230); | |
//final j = LargePerson.onlyWeight(name:"toto", weight:250);// Could not provide name | |
/// The widget that draws the user's profile | |
class YassineProfile extends StatelessWidget { | |
/// The person's name to display | |
final String name; | |
const YassineProfile({super.key, required this.name}); | |
@override Widget build(BuildContext context) { | |
return Text("Name: $name"); | |
} | |
} | |
void explainingHowTypesWork() { | |
/// TextFormField: A (Text) form field => String | |
dynamic s = "Hello"; | |
s = 12; | |
// Molded object (instance) : mold (class) | |
s = YassineProfile(name:"Yassine"); | |
final yassine = Person(name:"Yassine"); | |
s = yassine; | |
s = null; | |
//Map<dynamic, dynamic> s1; | |
//Map<int,String> s2; | |
//Map<Widget,double> s3; | |
Map<String, dynamic> dict = { | |
"age": 12, // int | |
"weight": 82.3, // double | |
"fistname": "Yassine", // String | |
"definition": { // Map<String, dynamic> | |
"type": "int", | |
"value": 12, | |
}, | |
}; | |
dict["int"] = 123; | |
// blah blah | |
final v = dict["definition"]; // could be null if something gone wrong | |
if (v is Map<String, dynamic>) { // Make sure v is of the expected type, aka. mateches specs, hourray! | |
//TODO: | |
} | |
// == | |
else { | |
throw Exception("Error: Form definiton is wrong: expecting Map>String, dynamic> but got $v"); | |
} | |
final s0 = "Hello $v"; //==> s0 = "Hello "+v.toString()+""; | |
} | |
/// Reads the form definition the server just sent to us. Warning could rethrow whenever something goes wrong. | |
void readFormDefinitionFromServer() { | |
/// The stream I use to update blah blah | |
//late final stream; | |
// Run the reader | |
try { | |
//stream = Stream(); | |
explainingHowTypesWork(); | |
} | |
catch(e) { | |
rethrow; | |
} | |
/// Finally is the code that is runf whatever the branches the code took: eitheir try or catch. | |
finally { | |
//stream.dispose(); | |
debugPrint("Leaving readFormDefinitonFromServer()"); | |
} | |
} | |
*/ | |
//let's create a class that take as input the the CategoryCarMetaData Map and return a listView widget containing in each line | |
//a represetation of each element of CategoryCarMetaData. | |
class ListRowsGenerator extends StatefulWidget{ | |
@override | |
State<StatefulWidget> createState() =>ListListRowsGeneratorState(); | |
} | |
class ListListRowsGeneratorState extends State { | |
String ? valueChosen; | |
@override | |
Widget build(BuildContext context) { | |
final fields = getCategoryCarMetaData(); | |
//final k = fields.keys.first; | |
//return FieldInput(name: k, field: fields[k]); | |
// == Selet the right Inputfiel generic according to the fiel type | |
final widgets = fields.keys.map<Widget>((k) { | |
debugPrint("field $k…"); | |
/// A shortcut on field k | |
final f = fields[k]; | |
debugPrint("field $k: type is ${f["type"]}"); | |
switch(f["type"]) { | |
case "int": return FieldInput<int>(name: k, field: f); | |
case "enum": | |
assert(f["values"] is Set<String>,"field values is not a set of string"); | |
assert(f["values"]!=null, "field values is null"); | |
assert(f["values"].length>1,"field values is one length"); | |
debugPrint("field values: ${f["values"]}"); | |
return ListTile( | |
title:Text(k), | |
subtitle:f["value"]==null?null:Text(f["value"]), | |
trailing: | |
TextButton(onPressed:() { | |
showModalBottomSheet(context:context, builder:(context)=> | |
EnumInputField( | |
values: f["values"].toList(), | |
onSelected:(value) { | |
setState((){ | |
debugPrint('f["value"]=${f["value"]}'); | |
debugPrint('f["value"] set to $value'); | |
f["value"] = value; | |
}); | |
}, | |
), | |
); | |
}, | |
child: const Icon(Icons.edit)), | |
); | |
default: return FieldInput(name: k,field: f); | |
} | |
} | |
).toList(); | |
//debugPrint("Field widgets: $widgets"); | |
final w= EnumInputField(values:const ["A","b","c","d","e","f","g","h"],onSelected:(value){ | |
//TODO: make something useful of user-picked value | |
// == User's cancelled? do nothing | |
if (value==null) return; | |
// == Store the value the user picked to the form | |
debugPrint("User picked $value"); | |
setState((){ | |
//field["value"]=value; | |
}); | |
}); | |
return | |
Column( | |
mainAxisSize: MainAxisSize.min, | |
children:[ | |
ListView( | |
physics: const BouncingScrollPhysics(), | |
shrinkWrap:true, | |
children: widgets, | |
), | |
// == Allow the user to pick from an enum | |
ElevatedButton(onPressed:() { | |
showModalBottomSheet(context:context, builder:(context)=>w); | |
},child:Text("Pick a letter")), | |
]) | |
; | |
/* | |
List <Widget>ListViewRows=[]; //empty list of widgets that will contain the listview elements | |
for (String key in getCategoryCarMetaData().keys){ //for loop to navigate the CategoryCarMetaData and extract data and put it as widget in the ListViewRows | |
switch (getCategoryCarMetaData()[key]["type"]){ //depending on type of data, widget that represent it is different | |
case "enum": { //if the type is enum, we will use DropDownButton to display all values allowed so the customer can choose. | |
final List itemsList=getCategoryCarMetaData()[key]["values"].toList(); | |
ListViewRows.add(Center(child: Text(key))); | |
ListViewRows.add(DropdownButton<String>( value: valueChosen, | |
isExpanded: true, | |
onChanged: (String ?newValue){setState(() {valueChosen=newValue;});}, | |
items : itemsList.map((e) =>DropdownMenuItem<String>( | |
value: e, | |
child: Text(e))).toList())); | |
} break; | |
case "string": { //if the type is String, we will use a TextFormField to let the user Add values with keyboard | |
ListViewRows.add(Center(child: Text(key))); | |
ListViewRows.add(TextFormField()); | |
} break; | |
case "date": { //if the type is enum, we will use a TextFormField to Add Date Value | |
ListViewRows.add(Center(child: Text(key))); | |
ListViewRows.add(TextFormField()); | |
} break; | |
case "int": { //if the type is int, we will use a TextFormField to Add this Value | |
ListViewRows.add(Center(child: Text(key))); | |
ListViewRows.add(TextFormField()); | |
} break; | |
case "double": { //if the type is int, we will use a TextFormField to Add this Value | |
ListViewRows.add(Center(child: Text(key))); | |
ListViewRows.add(TextFormField()); | |
} break; | |
default: { | |
ListViewRows.add(Center(child: Text(key))); | |
ListViewRows.add(TextFormField()); | |
} | |
break; | |
} | |
} | |
return(ListView( | |
padding: const EdgeInsets.all(20), | |
children: ListViewRows)); | |
*/ | |
} | |
} | |
/// Get the meta data for the `car` category | |
Map<String, dynamic> getCategoryCarMetaData() { | |
const energies = { | |
"icon": Icons.energy_savings_leaf, | |
"type": "enum", | |
"values": { | |
"electric", | |
"hybrid", | |
"reloaded hybrid", | |
"diesel", | |
"gas", | |
"hydrogen", | |
"gpl", | |
"lng", | |
"other" | |
}, | |
}; | |
const brands = { | |
"icon": Icons.branding_watermark, | |
"type": "enum", | |
"values": { | |
"mercedes-benz", | |
"bmw", | |
"renault", | |
"citroën", | |
"peugeot", | |
"opel", | |
"vw", | |
"simca", | |
"ford", | |
"gm", | |
"cadillac", | |
"other" | |
}, | |
}; | |
const 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,} | |
}; | |
} | |
/* DropDown Button Template | |
string ? valueChosen; | |
final List valueChosen=["tap1","tap2","tap3"]; | |
DropdownButton<String>( value: valueChosen, | |
onChanged: (newValue){setState(() {valueChosen=newValue;});}, | |
items : itemsList.map((e) =>DropdownMenuItem<String>(value: e, | |
child: Text(e))).toList()) | |
*/ | |
/* | |
Intro | |
It's about creating a simple application, which allows a user to feed a form allowing him to create the description data relating to the car he wants to sell 🚗 . | |
The information will be returned to elastic search from the application.Then you'll build a screen allowing him to build a search filter, run it, and finally display the results on the screen. He can then tap on it to view all the data. | |
The peculiarity here is that the form is not known in advance: it will be downloaded from a remote server: outside the scope of this work, you will hard code the description in the code meanwhile as I show in the wiki . | |
You are asked to deliver: | |
Step 1: Create ClassifiedAdMetaDataFormScreen screen that takes the category metadata from a Map<String, dynamic> and presents the related form built on the fly; | |
Step 2: Make the final evolution of the previous screen that also stores the data in the Firestore collection testClassifiedAd. | |
Step 3: Create ClassifiedAdSearchEngineScreen screen that enable the user to build a filter interactively according to the category metadata; | |
Step 4: Make the final evolution of the previous screen to actually build and run the request from the generated filter and create the ClassifiedAdSearchEngineResultsScreen screen that displays the results. | |
Have fun! | |
for the solution explanation go to : README.md | |
*/ | |
void main() { | |
runApp( MetaDataAdScreen()); | |
} | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment