i am new to flutter, i'm trying to invisible button when there is no data in Firebase.
To get data i'm using StreamBuilder, if snapshot.data!.docs is null i want to invisible CustomButton which is outside of StreamBuilder.
StreamBuilder:
bool _isVisible = true; //variable
#override
Widget build(BuildContext context) {
final size = MediaQuery.of(context).size;
return Scaffold(
key: _scaffoldKey,
appBar: _appBar(context),
body: CommonRefreshIndicator(
child: StreamBuilder(
stream: FirebaseFirestore.instance
.collection('users')
.doc(_currentUser!.uid)
.collection('favourites')
.snapshots(),
builder:
(BuildContext context, AsyncSnapshot<QuerySnapshot> snapshot) {
if (snapshot.hasError) {
return const CustomProgressIndicator();
}
if (snapshot.connectionState == ConnectionState.waiting) {
return const CustomProgressIndicator();
}
final data = snapshot.data!.docs;
allData = snapshot.data!.docs;
if (data.isNotEmpty) { //update based on data
_isVisible = true;
} else {
_isVisible = false;
}
return data.isNotEmpty
? _favItemListView(data)
: const Center(
child: Text('No data found'),
);
},
),
),
bottomNavigationBar: _addAllToFavButton(size),
);
}
CustomButton:
Padding _addAllToFavButton(Size size) => Padding(
padding: kSymmetricPaddingHor,
child: Visibility(
visible: _isVisible,
child: CustomButton(
label: 'Add all to my cart',
onPressed: () {},
),
),
);
i have tried with Visibility widget and its work but whenever i'm deleting all data CustomButton is still visible, to invisivle CustomButton every time need to do hot reload.
NOTE: setState is also not working its giving me error.
if any one can help me! Thanks.
If you want to hide your CustomButton when there is no data you can try this:
Put your _favItemListView(data) & _addAllToFavButton(size) inside Stack and give Positioned to your CustomButton with its bottom : 1 property.
StreamBuilder
return data.isNotEmpty
? Stack(
children: [
_favItemListView(data),
_addAllToFavButton(size),
],
)
: const Center(
child: Text('No data found'),
);
CustomButton:
Positioned _addAllToFavButton(Size size) => Positioned(
width: size.width,
bottom: 1, //bottom property
child: Padding(
padding: kSymmetricPaddingHor,
child: CustomButton(
label: 'Add all to my cart',
onPressed: () {}
},
),
),
);
You can check if the snapshot has any data by using snapshot.data!.data()!.isNotEmpty
then
if(snapshot.data!.data()!.isNotEmpty){
//show your data widget
// using a variable might require you to call a setstate but since
//the widget is building here you might get some errors, its safe to just show your widget if data exists
}else{
///no data here
}
Also to get the .data() you need to tell your stream that its a of type <DocumentSnapshot<Map<String, dynamic>>> as . .snapshots() returns Stream<DocumentSnapshot<Map<String, dynamic>>>
StreamBuilder<DocumentSnapshot<Map<String, dynamic>>>(
stream: FirebaseFirestore.instance..
Related
I made a floatingactionbutton and every time you press it it adds an item, and each item has a checkbox next to it but when I check off one item it checks all of them, I've spent a lot of time trying to figure out how to fix this but I can't. I could really use your help.
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
runApp(FireApp());
}
class FireApp extends StatefulWidget {
#override
_FireAppState createState() => _FireAppState();
}
bool isChecked = false;
class _FireAppState extends State<FireApp> {
final TextController = TextEditingController();
#override
Widget build(BuildContext context) {
CollectionReference groceries =
FirebaseFirestore.instance.collection('groceries');
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: TextField(
controller: TextController,
),
),
body: Center(
child: StreamBuilder(
stream: groceries.orderBy('name').snapshots(),
builder: (context, AsyncSnapshot<QuerySnapshot> snapshot) {
return ListView(
children: snapshot.data!.docs.map((grocery) {
return Center(
child: Row(
children: [
Container(color: Colors.red,height: 50,child: Text(grocery['name'])),
Checkbox(
materialTapTargetSize: MaterialTapTargetSize.padded,
value: isChecked,
activeColor: Colors.black,
checkColor: Colors.greenAccent,
onChanged: (bool) {
setState(() {
isChecked = !isChecked;
});
}
)],
),
);
}).toList(),
);
},
),
),
floatingActionButton: FloatingActionButton(onPressed: () {
groceries.add({
'name': TextController.text,
});
},),
),
);
}
}
You are using the same variable for all your checkboxes (isChecked) but you ougth to have one per data, you could add that attribute to your firebase document so its synced or you could create it locally but each time your stream updates you will need to compare what grocery correspond to a checkbox value which can be hard.
UPDATE
The easiest way is to have a bool parameter in your Firestore document
Then just push an update any time the user tap
return ListView(
children: snapshot.data!.docs.map((grocery) {
return Center(
child: Row(
children: [
Container(color: Colors.red,height: 50,child: Text(grocery['name'])),
Checkbox(
materialTapTargetSize: MaterialTapTargetSize.padded,
value: grocery['checked'],
activeColor: Colors.black,
checkColor: Colors.greenAccent,
onChanged: (val) async {
final data = grocery.data();
data['checked'] = val;
await grocery.reference.update(data);
}
)],
),
);
}).toList(),
);
For now this is sufficient to answer your question, you will see later that this incurs in more Firestore calls, unnecesary rebuild of all widgets in the list and so on and you will have to think another way to optimize resources, like watching the stream somewhere else to have a local List of bools that keeps in sync all values of the groceries so you only update locally with an setState and once in the cloud at the end (a save button perhaps)
I have a flutter app where user can add items to a list which are stored in firebase. User can add up to 1000 items at once. Initially this is no issue but with a growing number of list items the app gets slower and slower until when adding multiple items at once after roughly 1000 items are in the list it crashes the app due to the memory use -
thread #10, name = 'io.flutter.1.ui', stop reason = EXC_RESOURCE RESOURCE_TYPE_MEMORY (limit=1450 MB, unused=0x0)
How can I improve the code so the performance improves. I would like to keep the setup with the Stream since it lets me dynamically filter the list on the fly. One information here as well is that WidgetA and WidgetB also both use the Stream Data to display the number of list items in the list.
Here is my code a bit simplified for ease of reading:
Main Screen Class:
Widget content(context) {
double h = MediaQuery.of(context).size.height; //screen height
double w = MediaQuery.of(context).size.width; //screen width
return StreamProvider<List<Activity>>.value(
catchError: (_, __) => null,
value: DatabaseService().activities(widget.uid),
builder: (context, snapshot) {
return SafeArea(
child: Container(
//color: Theme.of(context).backgroundColor, //SkyHookTheme.background,
child: Scaffold(
backgroundColor: Colors.transparent,
body: NotificationListener<ScrollNotification>(
onNotification: _handleScrollNotification,
child: Stack(children: [
ListView(
controller: _scrollController,
children: <Widget>[
Column(
children: <Widget>[
WidgetA(),
WidgetB(),
ActivityList(), //List of User Activities
],
)
],
),
]),
),
),
),
);
});
}
ActivityList Class Listview Building:
ListView buildList(List<Activity> acts){
items = ListView.builder(
shrinkWrap: true,
physics: ClampingScrollPhysics(),
scrollDirection: Axis.vertical,
itemCount: len,
itemBuilder: (context, index) {
return ActivityTile(activity: acts[index], number: acts.length - (index));
},
);
return items;
}
Any Tips / Hints how I can improve this would be highly appreciated.
Thanks!
You have to pagination to achieve smooth perform
And just load 10 documents in one time and with
Help of scrollcontroller check you are end of the list
And then load next 10 documents that’s would be
Efficient manner .
Instead of "listview" use sliversList widget.
See the Example of sliversList and sliverscomponents here
I think #AmitSingh's suggestion is best but if you want to load data in once then you can get data in pagination but not when the user scrolls but when you got the first bunch of data.
yeah you should use pagination or lazy-loading! reading and rendering 1000 document at once is too much work for most mobile devices.
instead you should load you documents likes this
import 'package:cloud_firestore/cloud_firestore.dart';
Firestore firestore = Firestore.instance
class LongList extends StatefulWidget {
#override
_LongListState createState() => _LongListState();
}
class _LongListState extends State<LongList> {
List<DocumentSnapshot> products = []; // stores fetched products
bool isLoading = false; // track if products fetching
bool hasMore = true; // flag for more products available or not
int documentLimit = 10; // documents to be fetched per request
DocumentSnapshot lastDocument; // flag for last document from where next 10 records to be fetched
ScrollController _scrollController = ScrollController(); // listener for listview scrolling
getProducts() async {
if (!hasMore) {
print('No More Products');
return;
}
if (isLoading) {
return;
}
setState(() {
isLoading = true;
});
QuerySnapshot querySnapshot;
if (lastDocument == null) {
querySnapshot = await firestore
.collection('products')
.orderBy('name')
.limit(documentLimit)
.getDocuments();
} else {
querySnapshot = await firestore
.collection('products')
.orderBy('name')
.startAfterDocument(lastDocument)
.limit(documentLimit)
.getDocuments();
print(1);
}
if (querySnapshot.documents.length < documentLimit) {
hasMore = false;
}
lastDocument = querySnapshot.documents[querySnapshot.documents.length - 1];
products.addAll(querySnapshot.documents);
setState(() {
isLoading = false;
});
}
void initState(){
getProducts();
_scrollController.addListener(() {
double maxScroll = _scrollController.position.maxScrollExtent;
double currentScroll = _scrollController.position.pixels;
double delta = MediaQuery.of(context).size.height * 0.20;
if (maxScroll - currentScroll <= delta) {
getProducts();
}
});
_pageManager = PageManager();
super.initState();
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Flutter Pagination with Firestore'),
),
body: Column(children: [
Expanded(
child: products.length == 0
? Center(
child: Text('No Data...'),
)
: ListView.builder(
controller: _scrollController,
itemCount: products.length,
itemBuilder: (context, index) {
return ListTile(
contentPadding: EdgeInsets.all(5),
title: Text(products[index]['name']),
subtitle: Text(products[index] ['short_desc']),
);
},
),
),
isLoading
? Container(
width: MediaQuery.of(context).size.width,
padding: EdgeInsets.all(5),
color: Colors.yellowAccent,
child: Text(
'Loading',
textAlign: TextAlign.center,
style: TextStyle(
fontWeight: FontWeight.bold,
),
),
)
: Container()
]),
);
}
}
I am using future builder and stream builder to fetch data from firebase and show them on screen.
I have favourite button as well. when I click on favourite_borderLine iconButton. It fetch data from firebase then change the state to favourite_border iconButton.
It also change the state of every other listview.Builder what I want is just to change the icon state on every click not fetching the whole data from database.
This is the initial state
when I tap on favourite icon, Suppose I tapped on first icon then it start loading.
and then all the icons are changed :(
I just want to change the clicked icon state not all icons and do not want the fetch data on click just change the state of button.Here is code.
class TalentScreen1 extends StatefulWidget {
#override
_TalentScreen1State createState() => _TalentScreen1State();
}
class _TalentScreen1State extends State<TalentScreen1> {
bool toggle = false;
#override
Widget build(BuildContext context) {
return BlocProvider<TalentFavCubit>(
create: (context) => TalentFavCubit(),
child: SafeArea(
child: Scaffold(
body: Padding(
padding: const EdgeInsets.all(20.0),
child: SingleChildScrollView(
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[
Text('Talent Screen 1 home search'),
_retriveAllDocs,
],
),
),
),
),
),
);
}
Widget get _retriveAllDocs => FutureBuilder<QuerySnapshot>(
future: FirebaseRepo.instance.fetchWorkerFormFieldsData(),
builder: (BuildContext context, AsyncSnapshot<QuerySnapshot> snapshot) {
if (snapshot.connectionState == ConnectionState.waiting)
return CircularProgressIndicator();
if (snapshot.hasError) {
return Text("Something went wrong");
}
if (!snapshot.hasData) {
return Text("Nothing to show");
}
if (snapshot.connectionState == ConnectionState.done) {
final List<DocumentSnapshot> data = snapshot.data.docs;
return theUserInfo(data);
}
return Text("loading");
});
Widget theUserInfo(List<DocumentSnapshot> data) {
return ListView.builder(
shrinkWrap: true,
itemCount: data.length,
itemBuilder: (context, index) {
return FutureBuilder<DocumentSnapshot>(
future: fetch(data[index]['uid']),
builder: (BuildContext context,
AsyncSnapshot<DocumentSnapshot> snapshot) {
if (snapshot.hasError) {
return Text("Something went wrong");
}
if (snapshot.connectionState == ConnectionState.done) {
TalentHireFavModel userData = TalentHireFavModel.fromMap(
data[index].data(), snapshot.data.data());
return Card(
child: Column(
children: <Widget>[
Text(userData.name),
Text(userData.categories),
Text(userData.skills),
Text(userData.country),
Text(userData.phoneNo),
Text(userData.hourlyRate),
Text(userData.professionalOverview),
Text(userData.skills),
Text(userData.expert),
Text(userData.createdAt),
IconButton(
icon: toggle
? Icon(Icons.favorite_border)
: Icon(
Icons.favorite,
),
onPressed: () {
setState(() {
// Here we changing the icon.
toggle = !toggle;
});
}),
],
),
);
}
return Container();
});
});
}
//TODO: Implementation Fix Error
Widget _iconButton(uid) {
return StreamBuilder<QuerySnapshot>(
stream: FirebaseRepo.instance.fetchCurrentUserFavourites().snapshots(),
builder: (BuildContext context, AsyncSnapshot<QuerySnapshot> snapshot) {
var data = snapshot.data.docs;
// print(snapshot.data.get('uid'));
if (snapshot.hasError) {
return Text('Something went wrong');
}
return IconButton(
icon: data.isEmpty == uid
? Icon(Icons.favorite)
: Icon(Icons.favorite_border),
onPressed: () =>
BlocProvider.of<TalentFavCubit>(context).addTalentFav(uid));
},
);
}
Future<DocumentSnapshot> fetch(data) async =>
await FirebaseRepo.instance.fetchWorkerUserData(data);
}
This is your broken line of code:
future: FirebaseRepo.instance.fetchWorkerFormFieldsData(),
The FutureBuilder documentation starts with:
The future must have been obtained earlier, e.g. during State.initState, State.didUpdateWidget, or State.didChangeDependencies. It must not be created during the State.build or StatelessWidget.build method call when constructing the FutureBuilder. If the future is created at the same time as the FutureBuilder, then every time the FutureBuilder's parent is rebuilt, the asynchronous task will be restarted.
A general guideline is to assume that every build method could get called every frame, and to treat omitted calls as an optimization.
And you broke the contract. I have a video that illustrates this in detail. https://www.youtube.com/watch?v=sqE-J8YJnpg
Do what the docs say. TL;DR: Do not create the Future in the parameter to FutureBuilder.
I'm trying to figure out how to render a list from a specific collection in firebase and to change that list when selecting options from dropdown menu. I could get the list rendered on 1 collection, but when I add my dropdown menu, with the default value being 'lost', nothing is displayed. Here's what I have so far that works, but not entirely what I want.
class _ListPageState extends State<ListPage>{
List<String> _type = ['lost', 'found'];
String _selectedView = 'lost';
//this getData pulls from 'lost' collection, since I set _selectedView to lost by default
Future getData() async{
var firestore = Firestore.instance;
QuerySnapshot qn = await firestore.collection(_selectedView).getDocuments();
return qn.documents;
}
navigateToDetail(DocumentSnapshot post){
Navigator.push(context, MaterialPageRoute(builder: (context) => DetailPage(post: post,)));
}
Widget _viewType() {
return new DropdownButtonFormField(
value: _selectedView,
onChanged: (newValue) {
setState(() {
_selectedView = newValue;
});
},
items: _type.map((view) {
return new DropdownMenuItem(
child: new Text(view),
value: view,
);
}).toList(),
);
}
#override
Widget build(BuildContext context){
return ListView(
children: <Widget>[
_viewType(),
FutureBuilder(//it's not rendering any of this when adding the dropdown above it
future: getData(),
builder: (_, snapshot){
if(snapshot.connectionState == ConnectionState.waiting){
return Center(
child: Text("Loading"),
);
}
else{
return ListView.builder(
itemCount: snapshot.data.length,
itemBuilder: (_, index){
return ListTile(
title: Text(snapshot.data[index].data["Title"]),
onTap: () => navigateToDetail(snapshot.data[index]),
);
});
}
}),]
);
}
}
Thanks in advance for any help.
Please let me know if there's any more code you'd like to see.
I this I have to wrap part of it with setState(), but I'm not quite sure where.
Thanks for the fast clarification.
What is happening here is that you have put a ListView inside a ListView. You should use a Column.
By default (as mentioned in the documentation):
The Column widget does not scroll (and in general it is considered an error to have more children in a Column than will fit in the available room). If you have a line of widgets and want them to be able to scroll if there is insufficient room, consider using a ListView.
In your case, you want to place a ListView that will overflow the Column that can't scroll. To avoid that, consider using an Expanded
to take the remaining space so that the height is somehow constrained and the ListView knows its limits and work properly.
class _ListPageState extends State<ListPage> {
List<String> _type = ['lost', 'found'];
String _selectedView = 'lost';
//this getData pulls from 'lost' collection, since I set _selectedView to lost by default
Future getData() async {
var firestore = Firestore.instance;
QuerySnapshot qn = await firestore.collection(_selectedView).getDocuments();
return qn.documents;
}
navigateToDetail(DocumentSnapshot post) {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => DetailPage(
post: post,
)));
}
Widget _viewType() {
return new DropdownButtonFormField(
value: _selectedView,
onChanged: (newValue) {
setState(() {
_selectedView = newValue;
});
},
items: _type.map((view) {
return new DropdownMenuItem(
child: new Text(view),
value: view,
);
}).toList(),
);
}
#override
Widget build(BuildContext context) {
return Column(
children: <Widget>[
_viewType(),
Expanded(
child: FutureBuilder(
//it's not rendering any of this when adding the dropdown above it
future: getData(),
builder: (_, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return Center(
child: Text("Loading"),
);
} else {
return ListView.builder(
itemCount: snapshot.data.length,
itemBuilder: (_, index) {
return ListTile(
title: Text(snapshot.data[index].data["Title"]),
onTap: () => navigateToDetail(snapshot.data[index]),
);
},
);
}
},
),
),
],
);
}
}
I upload an image FirebaseStorage with FutureBuilder and when I press upload button I want to show a waiting dialog. Image upload successfully on FireStorage but nothing shows up. Whats wrong my codes? I think FutureBuilder return Widget and press button void. Maybe it is the problem. Or I am calling FutureBuilder wrong way. Do you have any tips or suggestions for my wrong code?
Here is my code;
Widget buildBody(BuildContext context) {
return new SingleChildScrollView(
child: new Column(
children: [
new SizedBox(
width: double.infinity,
child: new RaisedButton(
child: new Text('Upload'),
onPressed: () {
futureBuilder();
},
),
),
],
),
);
}
Future mediaUpload() async {
final fileName = DateTime.now().millisecondsSinceEpoch.toString() + '.jpg';
final StorageReference storage = FirebaseStorage.instance.ref().child(fileName);
final StorageUploadTask task = storage.putFile(_image);
return task.future;
}
Widget futureBuilder() {
return new FutureBuilder(
future: mediaUpload(),
builder: (BuildContext context, AsyncSnapshot snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.none:
print('ConnectionState.none');
return new Text('Press button to start.');
case ConnectionState.active:
print('ConnectionState.active');
return new Text('');
case ConnectionState.waiting:
waitingDialog(context);
return waitingDialog(context);
case ConnectionState.done:
print('ConnectionState.done');
if (snapshot.hasError) return new Text('Error: ${snapshot.error}');
print(snapshot.data.downloadUrl);
Navigator.of(context).pushReplacementNamed('home');
return new Text('Result: ${snapshot.data.downloadUrl}');
}
},
);
}
waitingDialog(BuildContext context) {
return showDialog(
context: context,
barrierDismissible: false,
builder: (BuildContext context) {
return new Center(
child: new SizedBox(
width: 40.0,
height: 40.0,
child: const CircularProgressIndicator(
value: null,
strokeWidth: 2.0,
),
),
);
},
);
}
I am not 100% sure why FutureBuilder is not working. But I have a guess. In the above example, FutureBuilder is not attached to the widgetTree(screen). Not attached means value returned from FutureBuilder is not displayed in screen. In this case, Text is returned based on snapshot.connectionState which is not attached to screen.
Work around: (I tested and it worked, can you please verify)
futureBuilder(BuildContext context) {
mediaUpload().then((task) { // fire the upload
Navigator.of(context).maybePop(); // remove the dialog on success upload
}); // we can use task(which returned from
// mediaUpload()) to get the values like downloadUrl.
waitingDialog(context); // show the spinner dialog
}