Flutter: Saving the state of a page even after navigating between screens - firebase

I have a simple thing to achieve. I have 2 screens in my app, on the main screen i have a button which navigates me to a new page called Interests. The Interests page is a list of checkboxes(for which i have to use listview.builder only) and a button to submit the data(which navigates me back to the main screen). The thing i want to achieve is this:
checkboxes should work properly.
when i navigate from Interests page to the main page and again navigate back to the Interests page, the selected checkboxes should remain checked. In short the state of the page should be saved.
I have written a function "applyInterestChanges" to save the data in database. I have to retrieve the same data to display the selected checkboxes(which we were doing by passing the data via constructor).
Any help would be appreciated!!
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: MyHomePage(),
debugShowCheckedModeBanner: false,
);
}
}
class MyHomePage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
child: Center(
child: RaisedButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => Interests(),
),
);
},
child: Text("Click here!!"),
),
),
),
);
}
}
class Interests extends StatefulWidget {
final List<dynamic> selectedList;
final void Function(List<dynamic>) callback;
Interests(this.selectedList, this.callback);
#override
_InterestsState createState() => _InterestsState();
}
class _InterestsState extends State<Interests> {
Map<String, dynamic> _categories = {
"responseCode": "1",
"responseText": "List categories.",
"responseBody": [
{"category_id": "1", "category_name": "Movies"},
{"category_id": "2", "category_name": "Sports"},
{"category_id": "3", "category_name": "Food"},
{"category_id": "4", "category_name": "Music"},
{"category_id": "5", "category_name": "Others"},
],
"responseTotalResult": 5
};
void _onCategorySelected(bool selected, categoryName) {
if (selected == true) {
setState(() {
widget.selectedList.add(categoryName);
});
} else {
setState(() {
widget.selectedList.remove(categoryName);
});
}
widget.callback(widget.selectedList);
}
applyInterestChanges() { //function to save the changes in database.
Firestore.instance
.collection('my_users')
.document(currentUserModel.id)
.updateData({
"interests": widget.selectedList,
});
} //this code is working properly. Need to similar function to retrieve the data and display the updated interests list.
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text("Interests"),
),
body: SingleChildScrollView(
child: Column(
children: [
Text(
"Select your interests: ",
style: TextStyle(fontSize: 20.0, fontWeight: FontWeight.bold),
),
ListView.builder(
physics: NeverScrollableScrollPhysics(),
scrollDirection: Axis.vertical,
shrinkWrap: true,
itemCount: _categories['responseTotalResult'],
itemBuilder: (BuildContext context, int index) {
return CheckboxListTile(
controlAffinity: ListTileControlAffinity.leading,
value: widget.selectedList.contains(
_categories['responseBody'][index]['category_name']),
onChanged: (bool selected) {
_onCategorySelected(selected,
_categories['responseBody'][index]['category_name']);
},
title:
Text(_categories['responseBody'][index]['category_name']),
);
},
),
MaterialButton(
onPressed: () {
Navigator.pop(context);
applyInterestChanges();
},
child: Text("Submit"),
),
],
),
),
);
}
}

You can pass an empty list from the parent widget MyHomeWidget & update this list via callback from the Interests widget.
Next time, whenever you go back & navigate again to Interests widget, we will pass this updated list which saves the state of Interests widget. Hence, the checkboxes will be checked depending upon their values in the list.
Here is the implementation:
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: MyHomePage(),
debugShowCheckedModeBanner: false,
);
}
}
class MyHomePage extends StatefulWidget {
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
List<dynamic> selectedList = [];
#override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
child: Center(
child: RaisedButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => Interests(
selectedList,
(List<dynamic> updatedList) {
setState(() {
selectedList = updatedList;
});
}
),
),
);
},
child: Text("Click here!!"),
),
),
),
);
}
}
class Interests extends StatefulWidget {
Interests(this.selectedList, this.callback);
// Passing the list from parent widget i.e, MyHomeWidget
// Initially the list will be empty
// We will update the list in parent whenever checkboxes change
final List<dynamic> selectedList;
// Creating a callback function to save state(update list) in
// MyHomeWidget
final void Function(List<dynamic>) callback;
#override
_InterestsState createState() => _InterestsState();
}
class _InterestsState extends State<Interests> {
Map<String, dynamic> _categories = {
"responseCode": "1",
"responseText": "List categories.",
"responseBody": [
{"category_id": "1", "category_name": "Movies"},
{"category_id": "2", "category_name": "Sports"},
{"category_id": "3", "category_name": "Food"},
{"category_id": "4", "category_name": "Music"},
{"category_id": "5", "category_name": "Others"},
],
"responseTotalResult": 5
};
void _onCategorySelected(bool selected, categoryId) {
if (selected == true) {
setState(() {
widget.selectedList.add(categoryId);
});
} else {
setState(() {
widget.selectedList.remove(categoryId);
});
}
// Callback to save the updated selectedList to MyHomeWidget list
widget.callback(widget.selectedList);
}
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text("Interests"),
),
body: SingleChildScrollView(
child: Column(
children: [
Text(
"Select your interests: ",
style: TextStyle(fontSize: 20.0, fontWeight: FontWeight.bold),
),
ListView.builder(
physics: NeverScrollableScrollPhysics(),
scrollDirection: Axis.vertical,
shrinkWrap: true,
itemCount: _categories['responseTotalResult'],
itemBuilder: (BuildContext context, int index) {
return CheckboxListTile(
value: widget.selectedList.contains(
_categories['responseBody'][index]['category_id']),
onChanged: (bool selected) {
_onCategorySelected(selected,
_categories['responseBody'][index]['category_id']);
},
title:
Text(_categories['responseBody'][index]['category_name']),
);
},
),
RaisedButton(
onPressed: () {
Navigator.pop(context);
},
child: Text("Go back!!"),
),
],
),
),
);
}
}
Here is the method that you wanted to fetch from Firebase. I have used the updated class FirebaseFirestore. If you are using the older version of Firebase, then simply replace FirebaseFirestore with Firebase.
Future<void> fetchInterestChanges() async { //function to get the changes in database.
final DocumentSnapshot doc = await FirebaseFirestore.instance
.collection('my_users')
.document(currentUserModel.id)
.get();
final updatedList = doc.data();
print(updatedList);
}

You should mixin with class AutomaticKeepAliveClientMixin to save old widgets, for example: https://github.com/diegoveloper/flutter-samples/blob/master/lib/persistent_tabbar/page2.dart.
Or when not work:
You must save data checkbox to cache, for example in shared presences, etc.
When reopening the page, you call data from the cache
And when checked/not resave data to cache

Related

The argument type ‘Widget’ can’t be assigned to the parameter type ‘String’?

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.)

No MediaQuery widget ancestor found. All the answers on the service did not help (((

Making a list with adding items to the database. After switching from main.dart to the page with a list, it does not open, it writes an error.enter image description here
import 'package:flutter/material.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_core/firebase_core.dart';
class Home extends StatefulWidget {
const Home({Key? key}) : super(key: key);
#override
State<Home> createState() => _HomeState();
}
class _HomeState extends State<Home> {
late String _userToDo;
List todoList = [];
void initFirebase() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
runApp(Home());
}
#override
void initState() {
super.initState();
initFirebase();
todoList.addAll(['Milk', 'Car', 'Sugar']);
}
void _menuOpen() {
Navigator.of(context).push(
MaterialPageRoute(builder: (BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Menu'),),
body: Row(
children: [
Padding(padding: EdgeInsets.only(left: 15)),
ElevatedButton(onPressed: () {
Navigator.pop(context);
Navigator.pushNamedAndRemoveUntil(context, '/', (route) => false);
},
child: Text('Home')),
Padding(padding: EdgeInsets.only(left: 15)),
Text('Home old')
],
)
);
})
);
}
#override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.grey[900],
appBar: AppBar(
title: Text('Список дел'),
actions: [
IconButton(onPressed: _menuOpen,
icon: Icon(Icons.menu_outlined),
)
],
),
body: ListView.builder(
itemCount: todoList.length,
itemBuilder: (BuildContext context, int index){
return Dismissible(
key: Key(todoList[index]),
child: Card(
child: ListTile(
title: Text(todoList[index]),
trailing: IconButton(
icon: Icon(Icons.delete_sweep,
color: Colors.redAccent,
), onPressed: () {
setState(() {
todoList.removeAt(index);
});
},
)
),
),
onDismissed: (direction) {
// if(direction == DismissDirection.startToEnd)
setState(() {
todoList.removeAt(index);
});
},
);
}
),
floatingActionButton: FloatingActionButton(
backgroundColor: Colors.green,
onPressed: () {
showDialog(context: context, builder: (BuildContext context){
return AlertDialog(
title: Text('Добавить'),
content: TextField(
onChanged: (String value){
_userToDo = value;
},
),
actions: [
ElevatedButton(onPressed: (){
FirebaseFirestore.instance.collection('items').add({'item': _userToDo});
Navigator.of(context).pop();
}, child: Text('Добавить')
)
],
);
});
},
child: Icon(Icons.add_comment_outlined,
color: Colors.white,
),
),
);
}
}
Everyone knows the error.
The following assertion was thrown building Home(state:
_HomeState#17f50): No MediaQuery widget ancestor found.
Scaffold widgets require a MediaQuery widget ancestor. The specific
widget that could not find a MediaQuery ancestor was: Scaffold dirty
state: ScaffoldState#4d9ee(lifecycle state: initialized, tickers:
tracking 2 tickers) The ownership chain for the affected widget is:
"Scaffold ← Home ← [root]"
No MediaQuery ancestor could be found starting from the context that
was passed to MediaQuery.of(). This can happen because you have not
added a WidgetsApp, CupertinoApp, or MaterialApp widget (those widgets
introduce a MediaQuery), or it can happen if the context you use comes
from a widget above those widgets.
Set according to your advice. Navigation and pop-up window stopped working.
import 'package:flutter/material.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_core/firebase_core.dart';
class Home extends StatefulWidget {
const Home({Key? key}) : super(key: key);
#override
State<Home> createState() => _HomeState();
}
class _HomeState extends State<Home> {
late String _userToDo;
List todoList = [];
void initFirebase() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
runApp(Home());
}
#override
void initState() {
super.initState();
initFirebase();
todoList.addAll(['Milk', 'Car', 'Sugar']);
}
void _menuOpen() {
Navigator.of(context).push(
MaterialPageRoute(builder: (BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Menu'),),
body: Row(
children: [
Padding(padding: EdgeInsets.only(left: 15)),
ElevatedButton(onPressed: () {
Navigator.pop(context);
Navigator.pushNamedAndRemoveUntil(context, '/', (route) => false);
},
child: Text('Home')),
Padding(padding: EdgeInsets.only(left: 15)),
Text('Home old')
],
)
);
})
);
}
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
backgroundColor: Colors.grey[900],
appBar: AppBar(
title: Text('Список дел'),
actions: [
IconButton(onPressed: _menuOpen,
icon: Icon(Icons.menu_outlined),
)
],
),
body: ListView.builder(
itemCount: todoList.length,
itemBuilder: (BuildContext context, int index){
return Dismissible(
key: Key(todoList[index]),
child: Card(
child: ListTile(
title: Text(todoList[index]),
trailing: IconButton(
icon: Icon(Icons.delete_sweep,
color: Colors.redAccent,
), onPressed: () {
setState(() {
todoList.removeAt(index);
});
},
)
),
),
onDismissed: (direction) {
// if(direction == DismissDirection.startToEnd)
setState(() {
todoList.removeAt(index);
});
},
);
}
),
floatingActionButton: FloatingActionButton(
backgroundColor: Colors.green,
onPressed: () {
showDialog(context: context, builder: (BuildContext context){
return AlertDialog(
title: Text('Добавить'),
content: TextField(
onChanged: (String value){
_userToDo = value;
},
),
actions: [
ElevatedButton(onPressed: (){
FirebaseFirestore.instance.collection('items').add({'item': _userToDo});
Navigator.of(context).pop();
}, child: Text('Добавить')
)
],
);
});
},
child: Icon(Icons.add_comment_outlined,
color: Colors.white,
),
),
),
);
}
}
The following assertion was thrown while handling a gesture: No
MaterialLocalizations found.
Home widgets require MaterialLocalizations to be provided by a
Localizations widget ancestor. The material library uses Localizations
to generate messages, labels, and abbreviations.
To introduce a MaterialLocalizations, either use a MaterialApp at the
root of your application to include them automatically, or add a
Localization widget with a MaterialLocalizations delegate.
The specific widget that could not find a MaterialLocalizations
ancestor was: Home state: _HomeState#8899d The ancestors of this
widget were: : [root]
renderObject: RenderView#1dbbb
void initFirebase() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
runApp(Home());
}
With that runApp call, you are removing your entire widget tree and replacing it with a tree rooted at a Home widget. This means that you are unable to access the MaterialApp widget that is presumably built by your App widget elsewhere in your app.
To fix this, move the first two lines of this method to your main method before runApp, and remove the entire method from the Home widget.
Part of the error says: This can happen because you have not added a WidgetsApp, CupertinoApp, or MaterialApp widget.
So in your Build method, you can wrap your Scaffold with a MaterialApp() and it should work.
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(...),
);
}

Getting ExpansionPanelList to work inside Streambuilder in flutter

I'm trying to arrange some data streamed from Firebase with an ExpansionPanellist. The panelList is placed inside a StreamBuilder, and above the StreamBuilder i have a SingleChildScrollView.
I am able to get the list showing with the headers, but i can't get the expand/collapse function to work, so I am not able to see the body-text.
screenshot of the list
The expanding/collapinsg function worked outside the Streambuilder, but I was not able to access the data from Firebase then.
Any help will be much appreciated! If this is the wrong way of doing this, I will also be grateful for any pointers to alternative ways of achieving this. (There won't be any data added to the server while looking at past climbs and graphs, so a streambuilder might not be necessary if there are easier/better ways).
-Kristian
class Graphs extends StatefulWidget {
static String id = 'graphs_screen';
#override
_GraphsState createState() => _GraphsState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
body: DefaultTabController(
length: 3,
child: Scaffold(
appBar: AppBar(
bottom: TabBar(tabs: [
Tab(text: 'Graphs'),
Tab(text: 'Stats'),
Tab(text: 'Climbs'),
]),
),
body: TabBarView(
children: [
//Image.asset('assets/images/line_graph.png'),
Expanded(child: NumericComboLinePointChart.withSampleData()),
Container(
child: Text(''),
),
SingleChildScrollView(
child: DataStream(),
),
],
)),
),
);
}
}
class DataStream extends StatefulWidget {
#override
_DataStreamState createState() => _DataStreamState();
}
class _DataStreamState extends State<DataStream> {
#override
Widget build(BuildContext context) {
return StreamBuilder<QuerySnapshot>(
stream: _firestore
.collection('climbs')
.orderBy('Date', descending: true)
.snapshots(),
builder: (context, snapshot) {
List<ExpansionItem> expansionList = <ExpansionItem>[];
if (snapshot.hasData) {
final alldata = snapshot.data.docs;
for (var data in alldata) {
final dataFunction = data.data();
final grades = dataFunction['gradeScore'];
final climbDate = dataFunction['Date'];
final climbDateT = DateTime.fromMicrosecondsSinceEpoch(
climbDate.microsecondsSinceEpoch);
String climbDateString =
"${climbDateT.year.toString()}-${climbDateT.month.toString().padLeft(2, '0')}-${climbDateT.day.toString().padLeft(2, '0')} ${climbDateT.hour.toString()}-${climbDateT.minute.toString()}";
final climber = dataFunction['sender'];
final currentUSer = loggedInUser.email;
if (climber == loggedInUser.email) {
expansionList.add(ExpansionItem(
dateTimeHeader: climbDateString,
climbs: grades.toString()));
}
}
}
return ExpansionPanelList(
expansionCallback: (int index, bool isExpanded) {
setState(() {
print('tap registered');
expansionList[index].isExpanded = !isExpanded;
});
},
children: expansionList.map((ExpansionItem item) {
return ExpansionPanel(
headerBuilder: (BuildContext context, bool isExpanded) {
return Container(
child: Text(item.dateTimeHeader),
);
},
body: Container(
child: Text(item.climbs),
),
isExpanded: item.isExpanded,
);
}).toList(),
);
});
}
}
class ExpansionItem {
ExpansionItem({this.isExpanded: false, this.dateTimeHeader, this.climbs});
bool isExpanded;
final String dateTimeHeader;
final String climbs;
}
I also ran into this issue and managed to develop a "work-around" (sorry in advance, it's a bit messy).
The reason your expansion tiles are not expanding is due to the nature of expansionCallback function. Once you press the expand button it also causes your StreamBuilder to rebuild. Therefore, since you're initializing "expansionList" within the StreamBuilder it will reset "isExpanded" back to false no matter how many times you press it. So your best option is to initialize the expansionList outside of the StreamBuilder and modify it from within. Check below for my solution but I welcome anyone to optimize it and/or share a better one.
class ExpansionItem {
String headerValue;
bool isExpanded;
SplitObject item;
ExpansionItem({this.item, this.headerValue, this.isExpanded = false});
}
class MyExample extends StatefulWidget{
#override
_MyExampleState createState() => _MyExampleState();
}
class _MyExampleState extends State<MyExample> {
//Pick a number as large as you see fit to always be more than necessary.
List<ExpansionItem> expansionItems = List<ExpansionItem>.generate('anyNumber', (int index)=> ExpansionItem(isExpanded: false,));
#override
Widget build(BuildContext context) {
return GestureDetector(
child: Scaffold(
appBar: AppBar(
title: Text('Split',style: Theme.of(context).textTheme.headline3,),
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(8),
child: Container(
child: StreamBuilder(
builder: (context, streamData){
if(streamData.hasData){
List<SplitObject> items = streamData.data;
//Save data to Expansion list by iterating through it.
for (var i = 0; i < items.length; i++){
try {
expansionItems[i].item =items[i];
expansionItems[i].headerValue =items[i].itemName;
} catch (e) {
// Catch any range errors after trimming list.
if(e.toString().contains('RangeError')) {
expansionItems.add(ExpansionItem(
item: items[i], headerValue: items[i].itemName));
}
}
}
// Trim list
expansionItems = expansionItems.getRange(0, items.length).toList();
return _buildListPanel(expansionItems);
} else {
return ListTile(
title: Text('No items to split.'),
);
}
},
stream: DatabaseService().splitItemData,
),
),
),
)
);
}
Widget _buildListPanel(List<ExpansionItem> expansionItems){
// print(expansionItems[0].isExpanded);
return ExpansionPanelList(
expansionCallback: (int index, bool isExpanded){
setState(() {
expansionItems[index].isExpanded = !isExpanded;
// print(expansionItems[index].isExpanded);
});
},
children: expansionItems.map<ExpansionPanel>((ExpansionItem item){
return ExpansionPanel(
headerBuilder: (BuildContext context, bool isExpanded){
print(item.isExpanded);
return Padding(
padding: const EdgeInsets.all(8.0),
child: Column(),
);
},
body: Container(),
isExpanded: item.isExpanded,
);
}).toList(),
);
}
}
You should create a class where your Expansion List will be, then your Stream builder must call it. Doing it this way Expansion Panel List callback will function just normal.
Look:
class _ExpansionPanelClass extends State<ExpansionPanelClass> {
#override
Widget build(BuildContext context) {
return ExpansionPanelList(
elevation: 3,
expansionCallback: (index, isExpanded) {
setState(() {
widget.product[index]['isExpanded'] = !isExpanded;
});
},
animationDuration: const Duration(milliseconds: 600),
children: widget.product
.map(
(item) => ExpansionPanel(
canTapOnHeader: true,
backgroundColor:
item['isExpanded'] == true ? Colors.cyan[100] : Colors.white,
headerBuilder: (_, isExpanded) => Container(
padding:
const EdgeInsets.symmetric(vertical: 15, horizontal: 30),
child: Text(
item['title'],
style: const TextStyle(fontSize: 20),
)),
body: Container(
padding:
const EdgeInsets.symmetric(vertical: 15, horizontal: 30),
child: Text(item['description']),
),
isExpanded: item['isExpanded'],
),
)
.toList(),
);
}
}
Then from your StreamBuilder:
StreamBuilder<QuerySnapshot>(
stream: dbProducts
.collection(ids[i])
.orderBy('order', descending: false)
.snapshots(),
builder: (context, snapshot) {
if (snapshot.hasData) {
List<Map<String, dynamic>> _items = [];
for (var document in snapshot.data!.docs) {
Map data = Map.from(document.data() as Map);
if (data['hide'] == false) {
Map<String, dynamic> map = {
'id': _items.length,
'title': data['name'],
'description': data['ingredients'],
'isExpanded': false
};
_items.add(map);
}
}
return ExpansionPanelClass(product: _items);
} else {
return const Center(
child: CircularProgressIndicator(
color: Colors.brown,
),
);
}
},
),
That's all.

Saving dialog content before closing it

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'),
)
],
),
)));
}
}

Flutter - Fetch Data from firestore and display it in a dropdown list

I'm trying to fetch data from firestore and display it in a dropdown menu. I tried declaring the list like the following:   List makes = [''] but I can’t view the data until I click on another field and the dropdown gets populated at multiple occasions. I have it in a method because eventually, I would like to create a second dropdown where there’s a condition in the database query.
ex. If Toyota is selected display all the models for that particular make.
new StreamBuilder<QuerySnapshot>(
stream: Firestore.instance.collection("makesModels").snapshots(),
builder: (context, snapshot) {
if (!snapshot.hasData) return new Text("Please wait");
return new DropdownButton(
items: snapshot.data.documents.map((DocumentSnapshot document) {
return DropdownMenuItem(
value: document.data["make"],
child: new Text(document.data["make"]));
}).toList(),
value: category,
onChanged: (value) {
setState(() {
category = value;
});
},
hint: new Text("Makes"),
style: TextStyle(color: Colors.black),
);
}),
new StreamBuilder<QuerySnapshot>(
stream: Firestore.instance.collection("makesModels").where('make', isEqualTo: category).snapshots(),
builder: (context, snapshot) {
if (!snapshot.hasData) return new Text("Please wait");
return new DropdownButton(
items: snapshot.data.documents.map((DocumentSnapshot document) {
for(int i = 0; i < document.data['models'].length; i++){
print(document.data['models'][i]);
return new DropdownMenuItem(
value: document.data['models'][i],
child: new Text(document.data['models'][i].toString()),
);
}
}).toList(),
value: models,
onChanged: (value) {
print(value);
setState(() {
models = value;
});
},
hint: new Text("Models"),
style: TextStyle(color: Colors.black),
);
}),
Checking the snippet you've provided, it seems that the app displays two DropdownButton. To display a default selected item on the dropdown, an item should be set on value. In my approach, I've set a boolean to check if there's a need to set a default value.
The Firestore data used in this sample app accesses two Firestore collections: carMake and cars (contains 'makeModel').
carMake collection
cars collection containing 'makeModel'
Complete app
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/material.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
// Initialize Firebase
await Firebase.initializeApp();
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
var carMake, carMakeModel;
var setDefaultMake = true, setDefaultMakeModel = true;
#override
void initState() {
super.initState();
}
#override
Widget build(BuildContext context) {
debugPrint('carMake: $carMake');
debugPrint('carMakeModel: $carMakeModel');
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Column(
children: [
Expanded(
flex: 1,
child: Center(
child: StreamBuilder<QuerySnapshot>(
stream: FirebaseFirestore.instance
.collection('carMake')
.orderBy('name')
.snapshots(),
builder: (BuildContext context,
AsyncSnapshot<QuerySnapshot> snapshot) {
// Safety check to ensure that snapshot contains data
// without this safety check, StreamBuilder dirty state warnings will be thrown
if (!snapshot.hasData) return Container();
// Set this value for default,
// setDefault will change if an item was selected
// First item from the List will be displayed
if (setDefaultMake) {
carMake = snapshot.data.docs[0].get('name');
debugPrint('setDefault make: $carMake');
}
return DropdownButton(
isExpanded: false,
value: carMake,
items: snapshot.data.docs.map((value) {
return DropdownMenuItem(
value: value.get('name'),
child: Text('${value.get('name')}'),
);
}).toList(),
onChanged: (value) {
debugPrint('selected onchange: $value');
setState(
() {
debugPrint('make selected: $value');
// Selected value will be stored
carMake = value;
// Default dropdown value won't be displayed anymore
setDefaultMake = false;
// Set makeModel to true to display first car from list
setDefaultMakeModel = true;
},
);
},
);
},
),
),
),
Expanded(
flex: 1,
child: Center(
child: carMake != null
? StreamBuilder<QuerySnapshot>(
stream: FirebaseFirestore.instance
.collection('cars')
.where('make', isEqualTo: carMake)
.orderBy("makeModel").snapshots(),
builder: (BuildContext context,
AsyncSnapshot<QuerySnapshot> snapshot) {
if (!snapshot.hasData) {
debugPrint('snapshot status: ${snapshot.error}');
return Container(
child:
Text(
'snapshot empty carMake: $carMake makeModel: $carMakeModel'),
);
}
if (setDefaultMakeModel) {
carMakeModel = snapshot.data.docs[0].get('makeModel');
debugPrint('setDefault makeModel: $carMakeModel');
}
return DropdownButton(
isExpanded: false,
value: carMakeModel,
items: snapshot.data.docs.map((value) {
debugPrint('makeModel: ${value.get('makeModel')}');
return DropdownMenuItem(
value: value.get('makeModel'),
child: Text(
'${value.get('makeModel')}',
overflow: TextOverflow.ellipsis,
),
);
}).toList(),
onChanged: (value) {
debugPrint('makeModel selected: $value');
setState(
() {
// Selected value will be stored
carMakeModel = value;
// Default dropdown value won't be displayed anymore
setDefaultMakeModel = false;
},
);
},
);
},
)
: Container(
child: Text('carMake null carMake: $carMake makeModel: $carMakeModel'),
),
),
),
],
),
);
}
}
Demo

Resources