I am working on a list app and want to load the categories of tasks of the home page in the dropdown menu button.
I have used SQflite to save the categories in a table.
When I tap the Dropdown Menu, the values seem not to be loaded on the menu. It's just an empty list.
Here is the code:
class _HomeState extends State<Home> {
TodoService _todoService;
var _selectedValue;
var _categories = List<DropdownMenuItem>();
List<Todo>_todoList=List<Todo>();
final GlobalKey<ScaffoldState> _globalKey=GlobalKey<ScaffoldState>();
#override
initState(){
super.initState();
getAllTodos();
}
_loadCategories() async {
var _categoryService = CategoryService();
var categories = await _categoryService.readCategory();
categories.forEach((category) {
setState(() {
_categories.add(DropdownMenuItem(
child: Text(category['name']),
value: category['name'],
));
});
});
}
getAllTodos()async{
_todoService=TodoService();
_todoList=List<Todo>();
var todos= await _todoService.readTodo();
todos.forEach((todo){
setState(() {
var model=Todo();
model.id=todo['id'];
model.title=todo['title'];
model.dueDate=todo['dueDate'];
model.category=todo['category'];
model.isFinished=todo['isFinished'];
_todoList.add(model);
});
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
key: _globalKey,
appBar: AppBar(
actions: <Widget>[
DropdownButton(
value: _selectedValue,
items: _categories,
onChanged: (value) {
setState(() {
_selectedValue = value;
_loadCategories();
});
},
),
Have you checked the service calls?
Maybe, the SQFlite database is not populated with the values you are trying to retrieve.
Try with a dummy set of values first, like:
items: <String>['One', 'Two', 'Free', 'Four']
.map<DropdownMenuItem<String>>((String value) {
return DropdownMenuItem<String>(
value: value,
child: Text(value),
);
}).toList(),
If the above works, make sure you populate the table by calling an insert query via the service call.
Or you can use the SQFlite's build-in support for doing a raw insert. This means that you can use a SQL string using rawInsert().
You forgot to map the List<Object> in the items of DropdownButton.
DropdownButton _dropdownButton(List<Object> categoriesList) {
return DropdownButton(
value: _selectedValue,
onChanged: (dynamic newValue) {
setState(() {
_selectedValue = newValue;
});
},
items: categoriesList
.map<DropdownMenuItem<Object>>((Object value) {
return DropdownMenuItem<Object>(
value: value,
child: Text(value.getTxt()), // Replace this with getter for the value you want to display
);
}).toList(),
);
}
Related
I have a user input and two select option dropdowns. I retrieved the displayName (user input value) from the state and displayed it in Firestore db, however, I can't get the selected options (selectedItemA and selectedItemB) from the state and display them in the db.
How can I get these values (selectedItemA and selectedItemB) displayed in the db?
state
class UserInfoState with ChangeNotifier {
String _displayName;
dynamic _selectedItemA;
dynamic _selectedItemB;
get displayName => _displayName;
get selectedItemA => _selectedItemA;
get selectedItemB => _selectedItemB;
set displayName(String value) {
_displayName = value;
notifyListeners();
}
set selectedItemA(dynamic value) {
_selectedItemA = value;
notifyListeners();
}
set selectedItemB(dynamic value) {
_selectedItemB = value;
notifyListeners();
}
}
Future<void> updateUserReportWithUserInfo(UserInfoState state) {
return Global.reportRef.upsert(
({
'displayName': state.displayName,
'selectedItemA': state.selectedItemA,
'selectedItemB': state.selectedItemB
}),
);
}
profile screen
#override
void initState() {
super.initState();
myFocusNode = FocusNode();
_dropdownMenuItemsA = buildDropDownMenuItemsA(_dropdownItemsA);
selectedItemA = _dropdownMenuItemsA[0].value;
_dropdownMenuItemsB = buildDropDownMenuItemsB(_dropdownItemsB);
selectedItemB = _dropdownMenuItemsB[0].value;
}
...
List<DropdownMenuItem<ContinentListItem>> _dropdownMenuItemsA;
ContinentListItem selectedItemA;
List<DropdownMenuItem<ContinentListItem>> buildDropDownMenuItemsA(
List listItems) {
List<DropdownMenuItem<ContinentListItem>> items = List();
for (ContinentListItem listItem in listItems) {
items.add(
DropdownMenuItem(
child: Text(listItem.name),
value: listItem,
),
);
}
return items;
...
floatingActionButton: FloatingActionButton(
backgroundColor: deepOrange,
onPressed: () {
myFocusNode.requestFocus();
updateUserReportWithUserInfo(state);
changeScreen(context, BottomNavBarController());
...
TextField(
focusNode: myFocusNode,
onChanged: (value) => state.displayName = value,
),
DropdownButtonHideUnderline(
child: DropdownButton(
value: selectedItemA,
items: _dropdownMenuItemsA,
onChanged: (value) {
setState(() {
selectedItemA = value;
print(selectedItemA.name);
});
}),
),
...
DropdownButtonHideUnderline(
child: DropdownButton(
value: selectedItemB,
items: _dropdownMenuItemsB,
onChanged: (value) {
setState(() {
selectedItemB = value;
print(selectedItemB.name);
});
class ContinentListItem {
int value;
String name;
ContinentListItem({this.value, this.name});
}
I solved it by adding state.selectedItemA = selectedItemA.name; as follows:
setState(() {
selectedItemA = value;
});
state.selectedItemA = selectedItemA.name;
}),
I'm new in Flutter with Firebase and I'm trying to load some arrays stored in Firebase into a DropdownButton.
This piece of code works when a I call it from a button. It returns a list of drinks that I can print on the screen:
Future<List<String>> get drinks async {
QuerySnapshot docs = await _constantes.getDocuments();
List<String> res = List();
List<Map<String, dynamic>> datos = List();
for (var d in docs.documents) {
datos.add(d.data);
}
for (var d in datos[0]['drinks'].toList()) {
res.add(d.toString());
}
return res;
}
But my problem is that I'd like to load this list into a DropdownButton, so the user could choose one of the drinks when the app shows him the form :
DropdownButtonFormField(
hint: Text('Choose a drink'),
value: _currentDrink ?? 'Water',
items: _db.drinks.then((drinks) {
List<DropdownMenuItem> datos = List();
for (var d in drinks) {
datos.add(DropdownMenuItem(
value: d,
child: Text(d),
));
}
return datos;
}),
onChanged: (val) => setState(() => _currentDrink = val),
),
But it doesn't work because the result is a Future.
How could I fix it?
Thanks.
Wrap it in a StreamBuilder. Instead of querying using _constantes.getDocuments() return the stream from _constantes.snapshots() assuming _constantes is your firebase collection:
StreamBuilder<List<DocumentSnapshot>>(
stream: _drinkStream,
builder: (context, snapshot) {
return snapshot.hasData
? DropdownButton(
onChanged: (value) {},
items: [
for (var child in snapshot.data)
DropdownMenuItem(
child: Text(
child.data['name'],
),
value: child,
),
],
)
: Container();
},
)
Assign an empty list [] to dropdown until drinks are fetched and when fetched we will assign drinks list.Drinks list will get items after our future is completed.You need to call this future in initState of your method so when page is opened it fetches drinks and then assigns it to the drinks list.Dropdown will remain empty until drinks are fetched.
(Incase you want to show progressindicator on dropdown until drinks are fetched wrap dropdown in a future builder)
List<String> drinks = List();
Future<List<String>> get drinks async {
QuerySnapshot docs = await _constantes.getDocuments();
List<String> res = List();
List<Map<String, dynamic>> datos = List();
for (var d in docs.documents) {
datos.add(d.data);
}
for (var d in datos[0]['drinks'].toList()) {
res.add(d.toString());
}
setState(() {
drinks = res;
});
return res;
}
DropdownButtonFormField(
hint: Text('Choose a drink'),
value: _currentDrink ?? 'Water',
items: drinks == null? []: drinks.map((drink) {
return DropdownMenuItem<String>(
child: Text(drink),
value: drink,
);
}).toList(),
onChanged: (val) => setState(() => _currentDrink = val),
),
you can build drop down widget after you get data from your firebase and if you want to make show water before data is load or not data found then following solutions could be best for you.
in method comment part is for your case and un comment part is for demo.
class DeleteWidget extends StatefulWidget {
const DeleteWidget({Key key}) : super(key: key);
#override
_DeleteWidgetState createState() => _DeleteWidgetState();
}
class _DeleteWidgetState extends State<DeleteWidget> {
String initdata = 'water';
List<String> _getdata = List();
#override
void initState() {
super.initState();
getdatafromAPI();
}
void getdatafromAPI() async {
/*
_db.drinks.then((drinks){
setState((){
_getdata.addAll(drinks);
initdata = _getdata[0];
});
});
*/
await Future.delayed(Duration(seconds: 1));
setState(() {
_getdata.addAll(['coffee', 'tea', 'greentea']);
initdata = _getdata[0];
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('test app'),
),
body: Container(
child: Center(
child: DropdownButton(
items: _getdata.length > 0
? _getdata.map((e) {
return DropdownMenuItem<String>(
child: Text(e.toString()),
value: e.toString(),
);
}).toList()
: [
DropdownMenuItem<String>(
child: Text("water"),
value: 'water',
)
],
value: initdata,
onChanged: (value) {
setState(() {
initdata = value;
});
},
),
),
),
);
}
}
I want to read some txts and store their text in an array. But because I need this array for my GUI it should wait until all is done.
Future<String> getFileData(String path) async {
return await rootBundle.loadString(path);
}
int topicNr = 3;
int finished = 0;
for (int topic = 1; topic <= topicNr; topic++) {
getFileData('assets/topic' + topic.toString() + '.txt').then(
(text) {
topics.add(text);
},
).whenComplete(() {
finished++;
});
}
while (finished < topicNr)
But when I run this code, finished won't update (I think because it is because the while loop runs on the main thread and so the async funtion can't run at the same time)
I could do this by just waiting, but this isn't really a good solution:
Future.delayed(const Duration(milliseconds: 10), () {
runApp(MaterialApp(
title: 'Navigation Basics',
home: MainMenu(),
));
});
How can I now just wait until all of those async Funtions have finished?
(sorry, I am new to Flutter)
One thing you could do is use a stateful widget and a loading modal. When the page is initialized, you set the view to be the loading modal and then call the function that gets the data and populate the data using set state. When you are done/when you are sure the final data has been loaded then you set the loading to false. See the example below:
class Page extends StatefulWidget {
page();
#override
State<StatefulWidget> createState() => new _Page();
}
class _Page extends State<Page>{
bool _loading = true; //used to show if the page is loading or not
#override
void initState() {
getFileData(path); //Call the method to get the data
super.initState();
}
Future<String> getFileData(String path) async {
return await rootBundle.loadString(path).then((onValue){
setState(() { //Call the data and then set loading to false when you are done
data = on value.data;
_loading = false;
});
})
}
//You could also use this widget if you want the loading modal ontop your page.
Widget IsloadingWidget() {
if (_loading) {
return Stack(
children: [
new Opacity(
opacity: 0.3,
child: const ModalBarrier(
dismissible: false,
color: Colors.grey,
),
),
new Center(
child: new CircularProgressIndicator(
valueColor:
new AlwaysStoppedAnimation<Color>(Colors.green),
strokeWidth: 4.0,
),
),
],
);
} else {
return Container();
}
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Stack(
children: <Widget>[
//If loading, return a loading widget, else return the page.
_loading ?
Container(
child: Center(
child: CircularProgressIndicator(
valueColor: new AlwaysStoppedAnimation<Color>(
Colors.blue))))
:Column(
children:<Widget>[
//Rest of your page.
]
)
]))
}
}
You could also set the fields of the initial data to empty values and the use set state to give them their actual values when you get the data.
so for example
string myvalue = " ";
#override
void initState() {
getFileData(path); //Call the method to get the data
super.initState();
}
//then
Future<String> getFileData(String path) async {
return await rootBundle.loadString(path).then((onValue){
setState(() { //Call the data and then set loading to false when you are done
data = on value.data;
myValue = onValue.data['val'];
_loading = false;
});
})
}
Let me know if this helps.
Use the FutureBuilder to wait for the API call to complete before building the widget.
See this example: https://flutter.dev/docs/cookbook/networking/fetch-data
runApp(MaterialApp(
title: 'Navigation Basics',
home: FutureBuilder(
future: getFileData(),
builder: (context, snapshot) {
if (snapshot.hasData) {
return MainMenu()
} else {
return CircularProgressIndicator();
}
));
I am trying to set up a toggle switch button from flutter to firestore. I have already set up the dependencies in my flutter project, however, I do not know how to connect the switch with the firestore.
I am trying to make an on/off switch which can be used to control light; I have tried giving it some values but, even then, I am not sure how to connect with firestore.
class _HomeState extends State<Home> {
bool _value = false;
void _onChanged(bool value) {
setState(() {
_value = value;
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Home ${widget.user.email}'),
),
body: new Container(
padding: new EdgeInsets.all(32.0),
child: new Column(
children: <Widget>[
new SwitchListTile.adaptive(
title: new Text('Bedroom light'),
activeColor: Colors.red,
secondary: const Icon(Icons.lightbulb_outline),
value: _value,
onChanged: (bool value) {
_onChanged(value);
})
],
),
),
);
}
}
This is the code I have so far. I know that we have to use StreamBuilder but I would like to know how.
You have to create a database reference first in firestore say-
databaseReference = Firestore.instance.collection('Switches').where('switch','==',/*ANY NAME*/);
And then run a transition to update the value of value
Firestore.instance.runTransaction((transaction) async {
await transaction.update(
documentReference, _value);
};
Just make sure that in the firestore, the field which will take the value of _value isboolean
new TextFormField(
validator: (value) async{
if (value.isEmpty) {
return 'Username is required.';
}
if (await checkUser()) {
return 'Username is already taken.';
}
},
controller: userNameController,
decoration: InputDecoration(hintText: 'Username'),
),
I have a form for user, and I want to check if the user already exists in the firestore datebase.
Future checkUser() async {
var user = await Firestore.instance
.collection('users')
.document(userNameController.text)
.get();
return user.exists;
}
This is my function to check if the user document already exists in the database.
But validator gives me this error.
[dart] The argument type '(String) → Future' can't be assigned to the parameter type '(String) → String'.
How should I fix this issue?
At this time I think that you can't associate a Future to a validator.
What you can do is this verifying the data on a button click or in another way and set the state on the validator response var.
#override
Widget build(BuildContext context) {
// TODO: implement build
return Scaffold(
body: Form(
key: _formKey,
child: Column(children: [
new TextFormField(
validator: (value) {
return usernameValidator;
},
decoration: InputDecoration(hintText: 'Username')),
RaisedButton(
onPressed: () async {
var response = await checkUser();
setState(() {
this.usernameValidator = response;
});
if (_formKey.currentState.validate()) {}
},
child: Text('Submit'),
)
])));
}
I needed to do this for username validation recently (to check if a username already exists in firebase) and this is how I achieved async validation on a TextFormField ( without installation of any additional packages). I have a "users" collection where the document name is the unique username ( Firebase can't have duplicate document names in a collection but watch out for case sensitivity)
//In my state class
class _MyFormState extends State<MyForm> {
final _usernameFormFieldKey = GlobalKey<FormFieldState>();
//Create a focus node
FocusNode _usernameFocusNode;
//Create a controller
final TextEditingController _usernameController = new TextEditingController();
bool _isUsernameTaken = false;
String _usernameErrorString;
#override
void initState() {
super.initState();
_usernameFocusNode = FocusNode();
//set up focus node listeners
_usernameFocusNode.addListener(_onUsernameFocusChange);
}
#override
void dispose() {
_usernameFocusNode.dispose();
_usernameController.dispose();
super.dispose();
}
}
Then in my TextFormField widget
TextFormField(
keyboardType: TextInputType.text,
focusNode: _usernameFocusNode,
textInputAction: TextInputAction.next,
controller: _usernameController,
key: _usernameFormFieldKey,
onEditingComplete: _usernameEditingComplete,
validator: (value) => _isUsernameTaken ? "Username already taken" : _usernameErrorString,)
Listen for focus changes on the widget i.e when it loses focus. You can also do something similar for "onEditingComplete" method
void _onUsernameFocusChange() {
if (!_usernameFocusNode.hasFocus) {
String message = UsernameValidator.validate(_usernameController.text.trim());
//First make sure username is in valid format, if it is then check firebase
if (message == null) {
Firestore.instance.collection("my_users").document(_usernameController.text.trim()).get().then((doc) {
if (doc.exists) {
setState(() {
_isUsernameTaken = true;
_usernameErrorString = null;
});
} else {
setState(() {
_isUsernameTaken = false;
_usernameErrorString = null;
});
}
_usernameFormFieldKey.currentState.validate();
}).catchError((onError) {
setState(() {
_isUsernameTaken = false;
_usernameErrorString = "Having trouble verifying username. Please try again";
});
_usernameFormFieldKey.currentState.validate();
});
} else {
setState(() {
_usernameErrorString = message;
});
_usernameFormFieldKey.currentState.validate();
}
}
}
For completeness, this is my username validator class
class UsernameValidator {
static String validate(String value) {
final regexUsername = RegExp(r"^[a-zA-Z0-9_]{3,20}$");
String trimmedValue = value.trim();
if (trimmedValue.isEmpty) {
return "Username can't be empty";
}
if (trimmedValue.length < 3) {
return "Username min is 3 characters";
}
if (!regexUsername.hasMatch(trimmedValue)) {
return "Usernames should be a maximum of 20 characters with letters, numbers or underscores only. Thanks!";
}
return null;
}
}
I had the same problem while using Firebase's Realtime Database but I found a pretty good solution similar to Zroq's solution. This function creates a simple popup form to have the user input a name. Essentially, I was trying to see if a particular name for a specific user was already in the database and show a validation error if true. I created a local variable called 'duplicate' that is changed anytime the user clicks the ok button to finish. Then I can call the validator again if there is an error, and the validator will display it.
void add(BuildContext context, String email) {
String _name;
bool duplicate = false;
showDialog(
context: context,
builder: (_) {
final key = GlobalKey<FormState>();
return GestureDetector(
onTap: () => FocusScope.of(context).requestFocus(new FocusNode()),
child: AlertDialog(
title: Text("Add a Workspace"),
content: Form(
key: key,
child: TextFormField(
autocorrect: true,
autofocus: false,
decoration: const InputDecoration(
labelText: 'Title',
),
enableInteractiveSelection: true,
textCapitalization: TextCapitalization.sentences,
onSaved: (value) => _name = value.trim(),
validator: (value) {
final validCharacters =
RegExp(r'^[a-zA-Z0-9]+( [a-zA-Z0-9]+)*$');
if (!validCharacters.hasMatch(value.trim())) {
return 'Alphanumeric characters only.';
} else if (duplicate) {
return 'Workspace already exists for this user';
}
return null;
},
),
),
actions: <Widget>[
FlatButton(
child: const Text("Ok"),
onPressed: () async {
duplicate = false;
if (key.currentState.validate()) {
key.currentState.save();
if (await addToDatabase(_name, email) == false) {
duplicate = true;
key.currentState.validate();
} else {
Navigator.of(context).pop(true);
}
}
},
),
FlatButton(
child: const Text('Cancel'),
onPressed: () {
Navigator.of(context).pop(false);
},
),
],
),
);
});
}