Flutter App Performance Issues with Long List - firebase

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

Related

I am trying to make a grocery app using flutter and firebase, everything is working but when I press the checkbox it Checks all of them

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)

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.

How to remove loading time between images from firebase in carousel slider in flutter

I am viewing images from firebase in a carousel slider in flutter. But there is a small loading gap everytime I swipe to the next image.
Is it possible to save the images in some way to remove the loading time between the images?
Here is my code:
class ViewImages extends StatelessWidget {
Future getCarouselWidget() async {
var firestore = Firestore.instance;
QuerySnapshot qn = await firestore.collection("images").getDocuments();
return qn.documents;
}
#override
Widget build(BuildContext context) {
var idx = 1;
return Container(
child: FutureBuilder(
future: getCarouselWidget(),
builder: (context, AsyncSnapshot snapshot) {
List<NetworkImage> list = new List<NetworkImage>();
if (snapshot.connectionState == ConnectionState.waiting) {
return new CircularProgressIndicator();
} else {
if (snapshot.hasError) {
return new Text("fetch error");
} else {
//Create for loop and store the urls in the list
for(int i = 0; i < snapshot.data[0].data.length; i++ ) {
debugPrint("Index is " + idx.toString());
list.add(NetworkImage(snapshot.data[0].data["img_"+idx.toString()]));
idx++;
}
return new Container(
height: MediaQuery.of(context).size.width,
width: MediaQuery.of(context).size.width,
child: new Carousel(
boxFit: BoxFit.cover,
images: list,
autoplay: false,
autoplayDuration: Duration(seconds: 5),
dotSize: 4.0,
indicatorBgPadding: 16.0,
animationCurve: Curves.fastOutSlowIn,
animationDuration: Duration(milliseconds: 2000),
dotIncreasedColor: Colors.white,
dotColor: Colors.grey,
dotBgColor: Colors.transparent,
));
}
}
}),
);
}}
What you are looking for is CachedNetworkImage, CachedNetworkImage, as the name suggests will only fetch the image once and the cache it, so the next time you build the widget, It won't be called again.

Search database (SQLite) using a textform in Flutter

I am trying to search my sqlite database, right now it returns all the members, even when text is input to the text form. I have a ListView builder in the memberList constructor that creates cards for each member. What I want it to do is display just the cards that match the users input.
i.e. if a user inputs J it would show only the members that either have first or last name with the letters J.
I can see the query is working properly as I have it printing the count in the dbHelper class and it updates each time I make a change to the textform's text. What I need it to do is essentially refresh the body of the Scaffold onChange of the textform's text, which is not working.
Any suggestions on how I can do this?
I prefer to have the textform in the appbar if at all possible.
Below is my code:
import 'package:flutter/material.dart';
import 'package:troop_mobile_app/MemberFiles/Member.dart';
import 'package:troop_mobile_app/MemberFiles/MemberList.dart';
import 'package:troop_mobile_app/DatabaseFiles/DBHelper.dart';
Future<List<Member>> search(String search) async {
var dbHelper = DBHelper();
Future<List<Member>> members = dbHelper.searchScouts(search);
return members;
}
class SearchFunction extends StatefulWidget {
#override
_SearchFunctionState createState() => _SearchFunctionState();
}
class _SearchFunctionState extends State<SearchFunction> {
TextEditingController controller = TextEditingController();
String searchText = "";
_searchResults(String text) {
return new FutureBuilder<List<Member>>(
future: search(text),
builder: (context, snapshot) {
if (snapshot.hasData) {
return MemberList(snapshot.data);
}
return Container(
alignment: AlignmentDirectional.center,
child: new CircularProgressIndicator(
strokeWidth: 7,
));
});
}
Widget build(BuildContext context) {
//Page Creation returning the UI Home Page Display
return Scaffold(
//Top 'Menu Bar' (AppBar) Creation
appBar: AppBar(
leading: IconButton(
icon: Icon(Icons.arrow_back),
onPressed: () {
Navigator.pop(context);
},
padding: EdgeInsets.fromLTRB(
20 /*left*/, 0 /*top*/, 20 /*right*/, 0 /*bottom*/),
),
title: TextField(
//initialValue: 'Search...',
style: TextStyle(color: Colors.black),
decoration: InputDecoration(
//fillColor: Colors.white,
//filled: true,
//border:
//OutlineInputBorder(borderRadius: BorderRadius.circular(12.0)),
labelText: 'Search...',
contentPadding: EdgeInsets.fromLTRB(10, 6, 0, 6),
prefixIcon: Icon(Icons.search),
),
onChanged: (text) async {
_searchResults(text);
searchText = text;
},
controller: controller,
),
),
//End Top 'Menu Bar' Creation
//Main Body Creation
body: Container(
child: new FutureBuilder<List<Member>> (
future: search(searchText),
builder: (context, snapshot) {
if (snapshot.hasData) {
return MemberList(snapshot.data);
}
return Container(
alignment: AlignmentDirectional.center,
child: new CircularProgressIndicator(
strokeWidth: 7,
));
}),
)
//End Main Body Creation
);
}
}
MemberList:
import 'package:flutter/material.dart';
import 'MemberCards.dart';
import 'package:troop_mobile_app/MemberFiles/Member.dart';
class MemberList extends StatelessWidget {
final List<Member> members;
MemberList(this.members);
#override
Widget build(BuildContext context) {
return _buildList(context);
}
ListView _buildList(context) {
return ListView.builder(
itemCount: members.length,
itemBuilder: (context, int) {
return MemberCards(members[int], );
},
);
}
}
DBHelper:
Future<List<Map<String, dynamic>>> searchScoutsMap(String search) async {
Database db = await this.database;
print("This works? $db");
var result = await db.rawQuery("SELECT * FROM $memberTable WHERE adult = 'N' AND ($colFirstName Like '%$search%' OR $colLastName Like '%$search%') ORDER BY $colFirstName ASC, $colLastName ASC");
print("result is working? $result");
print(result.length);
return result;
}
Future<List<Member>> searchScouts(String search) async {
var searchResults = await searchScoutsMap(search); // Get 'Map List' from database
print(searchResults.length);
print(searchResults.toString());
int count = searchResults.length; // Count the number of map entries in db table
List<Member> memberList = List<Member>();
// For loop to create a 'Member List' from a 'Map List'
for (int i = 0; i < count; i++) {
print("for loop working: ${i+1}");
memberList.add(Member.fromMapObject(searchResults[i]));
}
print("completed for loop");
return memberList;
}
I was able to solve my mistake after hours of frustrating work...
Here is was my fix:
In the first code snippet I was missing the setState()
I had to wrap the return new FutureBuilder... with setState()
_searchResults(String text) {
return new FutureBuilder<List<Member>>(
future: search(text),
builder: (context, snapshot) {
if (snapshot.hasData) {
return MemberList(snapshot.data);
}
return Container(
alignment: AlignmentDirectional.center,
child: new CircularProgressIndicator(
strokeWidth: 7,
));
});
}
New code snippet shown below:
I hope this helps anyone else out there that runs into a similar issue.
_searchResults(String text) {
setState(() {
return new FutureBuilder<List<Member>>(
future: search(text),
builder: (context, snapshot) {
if (snapshot.hasData) {
return MemberList(snapshot.data);
}
return Container(
alignment: AlignmentDirectional.center,
child: new CircularProgressIndicator(
strokeWidth: 7,
));
});
});
}

How to fix issue with firestore collection snapshot

I'm trying to order my messages collection from Firebase from newest to oldest. But my messages display randomly once after the last and once before the last.
Despite my final who order my collection by reverse
I've tried to find any subject to orders by realtime but I can't find anything about this on Flutter and Firestore. (Maybe I'm blind or I'm not looking well)
class MessagesStream extends StatelessWidget {
#override
Widget build(BuildContext context) {
return StreamBuilder<QuerySnapshot>(
stream: _fireStore.collection('messages').snapshots(),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return Center(
child: CircularProgressIndicator(
backgroundColor: Colors.lightBlueAccent,
),
);
}
final messages = snapshot.data.documents.reversed;
List<MessageBubble> messageBubbles = [];
for (var message in messages) {
final messageSender = message.data['sender'];
final messageText = message.data['text'];
final currentUser = loggedInUser.email;
final messageBubble = MessageBubble(
sender: messageSender,
text: messageText,
isMe: currentUser == messageSender,
);
messageBubbles.add(messageBubble);
}
return Expanded(
child: ListView(
reverse: true,
padding: EdgeInsets.symmetric(horizontal: 10.0, vertical: 20.0),
children: messageBubbles,
),
);
},
);
}
}
I add my data into this widget :
FlatButton(
onPressed: () {
messageTextController.clear();
_fireStore.collection('messages').add(
{'text': messageText, 'sender': loggedInUser.email});
},
child: Text(
'Send',
style: kSendButtonTextStyle,
),
),
while adding data you can add a timestamp /server timestamp to your message doc and then use it to arrange your data example "timestamp": Timestamp.now(), then query your data .orderby('timestamp' ,descending :true)

Resources