I'm making a dialog containing a list of items, each of which includes an editable text field.
I'd like to save the contents of edited text fields to a SQLite database on dialog close.
How would I do that? There seems to be no such thing as an onClose listener in Flutter and once the dialog is closed, I won't be able to retrieve the text from text fields.
As You have not shared any code - so i share a minimal example of what you intend to do.
Data can be passed with the use of Navigator.
class DemoApp extends StatefulWidget {
#override
DemoAppState createState() {
return new DemoAppState();
}
}
class DemoAppState extends State<DemoApp> {
String val = 'Empty';
TextEditingController cntrl = TextEditingController();
#override
void dispose() {
cntrl.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text('Value is -- $val'),
RaisedButton(
onPressed: () async {
val = await showDialog(
context: context,
builder: (context) {
cntrl.clear();
return AlertDialog(
title: Text('Enter Value'),
content: TextField(
controller: cntrl,
),
actions: <Widget>[
FlatButton(
onPressed: () {
Navigator.pop(context, cntrl.text);
},
child: Text('Save')),
],
);
});
setState(() {});
},
child: Text('Edit Value'),
)
],
),
)));
}
}
Related
I'm trying to get documents from a collection with the method FireBaseFirestore.instance.collection("users").where("name", isEqualTo : "something").get() which used to have a return type of QuerySnapshot.
My goal is to make a ListView or anything that can display like a ListView the result(s) of this request.
I have these functions :
This one is to get the documents with the where method
class DataBaseMeth {
getUserByUsername(String username) async{
return fsInstance.collection("users").where("name", isEqualTo: username).get();
}
}
This one is the widget with the result :
class SearchResultTile extends StatelessWidget {
final String username;
const SearchResultTile({
Key? key,
required this.username,
}) : super(key: key);
#override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(vertical: 10.0, horizontal: 25.0),
child: Row(
children: [
Column(
children: [
Text(
username,
),//username
],
),
)
],
),
);
}
}
And finally the class of the page :
class SearchPage extends StatefulWidget {...}
class _SearchPageState extends State<SearchPage> {
DataBaseMeth dataBaseMethods = DataBaseMeth();
TextEditingController usernameSearchController = TextEditingController();
QuerySnapshot searchSnapshot; //the only way the code run is to replace the type by dynamic
initSearch(){
dataBaseMethods.getUserByUsername(usernameSearchController.text)
.then((result){
setState((){
searchSnapshot = result;
print("result : $searchSnapshot");
//print("result : ${searchSnapshot.docs[1].data.toString()}");
});
});
}
Widget searchList(){
return searchSnapshot != null ?
ListView.builder(
shrinkWrap: true,
itemCount: searchSnapshot.docs.length,
itemBuilder: (context, index) {
return SearchResultTile(
username: searchSnapshot.docs[index].data.toString(),
personalMessage: "personalMessage");
}
)
:
Container();
}
#override
void initState() {
searchList();
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: const MainAppBar(titleText: 'Search truc', mainPage: true),
body: Container(
padding: const EdgeInsets.symmetric(horizontal: 15.0, vertical: 10.0),
child: Column(
children: [
Row(
children: [
Expanded(
child: TextField(
controller: usernameSearchController,
decoration: textFieldInputDecoration("search username..."),
style: whiteText(),
),
),
IconButton(
onPressed: () {
initSearch();
},
icon: const Icon(Icons.search_outlined),
color: const Color(0xFFFFFFFF),
highlightColor: Colors.deepPurple,
splashColor: const Color(0xFF3A206B),
tooltip: "Search",
),
],
),
searchList()
],
),
),
);
}
}
The result of the print of searchSnapshot (when I put it on dynamic) is :
I/flutter (31401): result : Instance of '_JsonQuerySnapshot'
And nothing appears when I tap on the button.
Your fsInstance.collection("users").where("name", isEqualTo: username).get() returns a Future<QuerySnapshot> not a QuerySnapshot, so that's why you can't assign it to QuerySnapshot searchSnapshot. You can assign it to Future<QuerySnapshot> searchSnapshot though.
That also means that if you want to use it in your UI you'll have to either wrap it in a FutureBuilder or pass it to setState().
How do I use my custom widget Notes? I unfortunately can't use the full code in the AddNoteScreen.
I got this error when I changed a few things from the class I'm taking. Below I've pasted the instructors code, with my custom widget included. I'll comment below with the other changes I tried that lead me to this error.
Custom widget down to bare bones:
class Notes extends StatelessWidget {
TextEditingController notesController = TextEditingController();
#override
Widget build(BuildContext context) {
return TextField(
controller: notesController,
);
}
}
class AddNoteScreen extends StatefulWidget {
User user;
AddNoteScreen({
required this.user,
});
#override
State<AddNoteScreen> createState() => _AddNoteScreenState();
}
class _AddNoteScreenState extends State<AddNoteScreen> {
TextEditingController titleController = TextEditingController();
TextEditingController notesController = TextEditingController();
bool loading = false;
#override
void initState(){
super.initState(
);
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor:Color (0xFF162242),
elevation: 0,
),
body: GestureDetector(
onTap: () {
FocusScope.of(context).unfocus();
new TextEditingController().clear();
},
child: SingleChildScrollView(
child: Padding(
padding: EdgeInsets.all(20),
child: Column(children: [
Text("Title", style: TextStyle(
color: Colors.white,
),
),
SizedBox(
height: 15,
),
Container(
height: 60,
color: Colors.white,
child: TextField(
style: TextStyle(
color: Color(0xFF192A4F),
),
controller: titleController,
),
),
Notes(), // My Custom Widget
SizedBox(height: 50,),
loading ? Center (child: CircularProgressIndicator(),) : Container(
height: 50,
width: MediaQuery.of(context).size.width,
child: ElevatedButton(
onPressed: ()async{
if (
titleController.text == "" || notesController.text == "") // HERE
{
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text("All fields are required")));
} else {
setState(() {
loading = true;
});
await FirestoreService().insertNote(titleController.text, notesController.text, widget.user.uid); // HERE
setState(() {
loading = false;
});
Navigator.pop(context);
}
}, child: Text("Add Note"),
),),
]),),
),
),
);
}
}
^ above I changed notesController.text == "" to Notes == "" and then notesController.text to Notes()
class FirestoreService{
FirebaseFirestore firestore = FirebaseFirestore.instance;
Future insertNote(String title, String notes, String userId)async{
try{
await firestore.collection('notes').add({
"title":title,
"notes":notes,
"userId": userId
});
} catch (e) {}
}
}
^ above I changed String to Widget for notes
class NoteModel {
String id;
String title;
String notes;
String userId;
NoteModel({
required this.id,
required this.title,
required this.notes,
required this.userId
});
factory NoteModel.fromJson(DocumentSnapshot snapshot){
return NoteModel(
id: snapshot.id,
title: snapshot['title'],
notes: snapshot['notes'],
userId: snapshot['userId']
);
}
}
^ above I changed String to Widget for notes
class HomeScreen extends StatefulWidget {
#override
State<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
final user = FirebaseAuth.instance.currentUser!;
FirebaseFirestore firestore = FirebaseFirestore.instance;
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Notes'),
centerTitle: true,
backgroundColor: Color (0xFF162242),
actions: [
TextButton(onPressed: () => FirebaseAuth.instance.signOut(), child: Text("Sign Out", style: TextStyle(color: Colors.white),),),
],
),
body: StreamBuilder(
stream: FirebaseFirestore.instance.collection("notes").where('userId', isEqualTo: user.uid).snapshots(),
builder: (context, AsyncSnapshot snapshot){
if (snapshot.hasData){
if(snapshot.data.docs.length > 0){
return ListView.builder(
itemCount: snapshot.data.docs.length,
itemBuilder: (context,index) {
NoteModel note = NoteModel.fromJson(snapshot.data.docs[index]);
return Card(
margin: EdgeInsets.only(top: 16, left: 10, right: 10, bottom: 16),
child: Column(
children: [
ListTile(
title: Center(child: Text(note.title, style: TextStyle(fontSize: 30, fontWeight: FontWeight.bold),
),
),
onTap: () {
Navigator.of(context).push(MaterialPageRoute(builder: (context) => EditNoteScreen(),));},
),
ListTile(title: Center(child:
Container(
height: 300,
child:
Text(note.notes),),), // HERE
),
]),
);
}
);
}else Center(child: Text("No notes available", style: TextStyle(color: Colors.white),),);
}
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircularProgressIndicator(),
],
),
);
}),
floatingActionButton: FloatingActionButton(
onPressed: () {
Navigator.of(context).push(MaterialPageRoute(builder: (context) => AddNoteScreen(user: user)));
},
backgroundColor: Color (0xFF162242),
child: Icon(Icons.add),
),
);
}
}
^ Text(note.notes) is where I get the error.
I don't really know what I'm doing but can something like this work ? Totally different answer is okay too!
I'm sorry that's a lot of code. Any help is appreciated.
Also link to the class if anyone is interested https://skl.sh/3wxeMVF
Assumptions
Based on the code and comments I guess the actual class NoteModel and Notes are looking something like this:
class NoteModel {
Notes notes;
...
}
class Notes extends StatelessWidget {
TextEditingController notesController = TextEditingController();
...
}
Problem
This explains the error message The argument type ‘Widget’ can’t be assigned to the parameter type ‘String’?:
Text(note.notes) expects note.notes to be a String. Whereas you changed note.notes to be the Widget Notes.
Solution 1
The widget Text() expects Strings, not another Widget. Thus,
change notes back to a String:
class NoteModel {
String notes;
...
}
Build the rest of your code around this NoteModel, do not change it.
Solution 2
If you want to use
class NoteModel {
Notes notes;
...
}
then the Text widget would be called something like this:
Text(note.notes.notesController.text)
However, this is NOT recommended, as a NoteModel is a data model. And data models should never hold Widgets. A Widget is meant for showing data, not for holding it. A data model and a Widget serve different functions. Keep them separated.
Firebase
Note, that one cannot store whole Widgets (like Notes) in in Firebase but only Strings, Numbers etc.
(Please always post your current code, not code that is indirectly related related to the issue. Otherwise, people will find it very difficult to spot the problem.)
Good day,
I need to change the command from back button located on Navigation Bar on Android cellphones, like the imagem bellow?
I need to change the button to appear a message, "Do you really want to quit the application?".To confirm the user leave the program.
Anyone can help?
Thanks.
Use the WillPopScope widget to handle the back button action, example :
class TestingWidget extends StatefulWidget {
#override
TestingWidgetState createState() {
return new TestingWidgetState();
}
}
class TestingWidgetState extends State<TestingWidget> {
Future<bool> _onBackPressed(){
final alertDialog = AlertDialog(
content: Text("Do you really want to quit the application?"),
actions: <Widget>[
FlatButton(
child: Text('Yes'),
onPressed: () => Navigator.of(context).pop(),
),
FlatButton(
child: Text('No'),
onPressed: () => Navigator.of(context).pop(),
)
],
);
showDialog(
barrierDismissible: false,
context: context,
builder: (context) => alertDialog);
}
#override
Widget build(BuildContext context) {
return WillPopScope(
onWillPop: _onBackPressed,
child: Scaffold(
appBar: AppBar(),
body: Center(child: Text("Hello world"),),
),
);
}
}
How do we add a TextField inside a StreamBuilder?
I have a TextField / TextFormField as one of the widgets inside the builder function of either a StreamBuilder or FutureBuilder, whenever we try to interact with the textfield it just refreshes the entire builder widget and calls the stream/future again.
body: StreamBuilder(
stream: getClientProfile().snapshots(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.active) {
print(snapshot.data.data);
Client tempClient = Client.from(snapshot.data);
print('details = ${tempClient.representative.email} ${tempClient
.address.location} ${tempClient.businessDescription}');
return Container(
child: Column(
children: <Widget>[
TextFormField(
)
],
),
);
} else if (snapshot.connectionState == ConnectionState.waiting) {
return Center(child: CircularProgressIndicator());
} else {
return Center(
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Padding(
padding: const EdgeInsets.all(8.0),
child: Icon(Icons.error),
),
Text('Error loading data')
],
),
);
}
}),
and firestore function
DocumentReference getClientProfile() {
return _firestore.collection(SELLERS_COLLECTION).document(_uid);
}
What I want to achieve, is to have a form with pre-filled data from firestore document, basically an edit form. Is there any other way I could achieve the same or am I doing something wrong structurally ?
EDIT:
code after suggested edits.
import 'package:flutter/material.dart';
import 'Utils/globalStore.dart';
import 'models/client_model.dart';
import 'dart:async';
class EditProfileInformation extends StatefulWidget {
#override
EditProfileInformationState createState() {
return new EditProfileInformationState();
}
}
class EditProfileInformationState extends State<EditProfileInformation> {
Stream dbCall;
final myController = TextEditingController();
#override
void initState() {
// TODO: implement initState
super.initState();
dbCall = getClientProfile().snapshots();
myController.addListener(_printLatestValue);
}
_printLatestValue() {
print("Second text field: ${myController.text}");
}
#override
void dispose() {
myController.removeListener(_printLatestValue);
myController.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
// key: _scaffoldKey,
appBar: AppBar(
title: Text(
'Edit profile',
style: TextStyle(),
),
),
body: StreamBuilder(
stream: dbCall,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.active) {
print(snapshot.data.data);
Client tempClient = Client.from(snapshot.data);
print('details = ${tempClient.representative.email} ${tempClient
.address.location} ${tempClient.businessDescription}');
return Container(
child: Column(
children: <Widget>[
Padding(
padding: const EdgeInsets.all(8.0),
child: TextField(
controller: myController,
),
)
],
),
);
} else if (snapshot.connectionState == ConnectionState.waiting) {
return Center(child: CircularProgressIndicator());
} else {
return Center(
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Padding(
padding: const EdgeInsets.all(8.0),
child: Icon(Icons.error),
),
Text('Error loading data')
],
),
);
}
}),
floatingActionButton: FloatingActionButton(
onPressed: () {
},
child: Icon(Icons.done),
),
);
}
}
In order to use a StreamBuilder correctly you must ensure that the stream you are using is cached on a State object. While StreamBuilder can correctly handle getting new events from a stream, receiving an entirely new Stream will force it to completely rebuild. In your case, getClientProfile().snapshots() will create an entirely new Stream when it is called, destroying all of the state of your text fields.
class Example extends StatefulWidget {
#override
State createState() => new ExampleState();
}
class ExampleState extends State<Example> {
Stream<SomeType> _stream;
#override
void initState() {
// Only create the stream once
_stream = _firestore.collection(collection).document(id);
super.initState();
}
#override
Widget build(BuildContext context) {
return new StreamBuilder(
stream: _stream,
builder: (context, snapshot) {
...
},
);
}
}
EDIT: it sounds like there are other problems which I cannot diagnose from the code snippet you provided.
I am developing a Flutter app and I am using the cloud_firestore plugin. I have a collection of submissions and I am using the StreamBuilder to display them (which I am assuming will update when the stream changes). I literally took the example from the plugin examples as there is not much documentation on how to do things using the plugin. When I added a record, the list of documents that I am displaying gets longer, but it seems to be copying one of the submissions instead of inserting the new submission. The new submission does not show after it is added. Here is the code for how I am displaying the list:
// At the top of the class home.dart.
final submissions = Firestore.instance.collection('submissions');
// This is in submission-list.dart and the above submissions
// is passed in to the contructor
Widget build(BuildContext context) {
return new StreamBuilder<QuerySnapshot>(
stream: submissions
.where('owner_uid', isEqualTo: this.user.uid)
.orderBy('timestamp', descending: true)
.snapshots,
builder: (BuildContext context, AsyncSnapshot<QuerySnapshot> snapshot) {
if (!snapshot.hasData) return new Text('Loading...');
return new ListView(
children: snapshot.data.documents.map((DocumentSnapshot document) {
var date = _formatDate(document['timestamp']);
String body = _constructCardBody(document['weight'],
bodyFat: document['bodyFat']);
String id = document.documentID;
return new SubmissionCard(id: id, title: date, body: body, submissions: submissions);
}).toList(),
);
},
);
}
Here is submission-card.dart in full:
import 'dart:async';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/material.dart';
import '../utils/logger.dart';
import './block-button.dart';
class SubmissionCard extends StatefulWidget {
final String id;
final String title;
final String body;
final CollectionReference submissions;
SubmissionCard({this.id, this.title, this.body, this.submissions});
#override
State<StatefulWidget> createState() =>
new _SubmissionCardState(id: this.id, title: this.title, body: this.body, submissions: this.submissions);
}
class _SubmissionCardState extends State<SubmissionCard> {
final String id;
final String title;
final String body;
bool showActionButtons = false;
final CollectionReference submissions;
_SubmissionCardState({this.id, this.title, this.body, this.submissions});
void _showEditScreen() {}
void _showActionButtons() {
setState(() {
showActionButtons = true;
});
}
void _hideActionButtons() {
setState(() {
showActionButtons = false;
});
}
Future<Null> _deleteSubmission() async {
try {
await submissions.document(id).delete();
await Logger.log('error', 'stackTrace');
} catch (error, stackTrace) {
await Logger.log(error, stackTrace);
}
}
void _closeDialog() {
Navigator.of(context).pop();
_hideActionButtons();
}
Future<Null> _warnAboutDeletion() async {
return showDialog(
context: context,
child: new SimpleDialog(
title: new Text('Are you sure?'),
children: <Widget>[
new SimpleDialogOption(
onPressed: () {
this._deleteSubmission();
this._closeDialog();
},
child: new Text("I'm sure. Delete it."),
),
new SimpleDialogOption(
onPressed: _closeDialog,
child: new Text("Nope. Take me back."),
),
],
)
);
}
#override
Widget build(BuildContext context) {
return new GestureDetector(
onLongPress: _showActionButtons,
onTap: _hideActionButtons,
child: new Card(
elevation: showActionButtons ? 8.0 : 2.0,
key: new GlobalKey(),
child: new Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
new ListTile(
trailing: showActionButtons
? new Row(
children: <Widget>[
new IconButton(
padding: const EdgeInsets.all(0.0),
icon: const Icon(Icons.edit),
onPressed: _showEditScreen,
color: Colors.black12,
splashColor: Colors.black26,
highlightColor: Colors.black12,
),
new IconButton(
padding: const EdgeInsets.all(0.0),
icon: const Icon(Icons.delete),
onPressed: _warnAboutDeletion,
color: Colors.redAccent,
splashColor: Colors.black26,
highlightColor: Colors.black12,
),
],
)
: new Container(),
isThreeLine: true,
title: new Text(title),
subtitle: new Text(
body,
style: new TextStyle(height: 3.0),
),
),
],
),
),
);
}
}
Link to repo: https://github.com/dericgw/bodwatch
Before, when I have worked with Firebase, this collection would automatically update. I have never seen this weird behavior before. Now, I am new to Flutter and Dart, so I could be missing something for sure.
You need to add the indexing in firebase console.
In your case, you need to a multiple indexes.
1. owner_uid, ascending
2. timestamp, descending
And the problem should solve.