Flutter - Lazy Load When Scrolling Up (show previous data) - firebase

I'm working on a chat room where the new data will be at the bottom of a ListView and as a user scrolls up, previous chat messages will need to load (until they reach the end or original message).
By default, the user will start at the bottom of the list so they can see the new message(s).
The issue I am having is that when I reach the top of my ListView, I stay there. So when new data comes in I'm always at position 0. I'd like to lazy load or prefetch the data so that I get an infinite scroll feel and will stay at the position I stop scrolling at.
Here's some of the logic, hopefully, it will help. Please let me know if there is any other piece of the logic that would help.
void _scrollListener() {
_firstAutoscrollExecuted = true;
if (_scrollController.hasClients &&
_scrollController.position.pixels ==
_scrollController.position.maxScrollExtent) {
_shouldAutoscroll = true;
} else {
_shouldAutoscroll = false;
}
if (_scrollController.position.pixels == 0) {
isAtTopOfTable = true;
} else {
isAtTopOfTable = false;
}
}
void _goToElement(double index) {
_scrollController.animateTo(index,
duration: const Duration(milliseconds: 300), curve: Curves.easeOut);
}
_buildMessagesListView(
List<Message> messages, int numberOfChatsToDisplay, abcState state) {
final abcBloc = BlocProvider.of<abcBloc>(context);
// I've tried something like this as well to animate back to a specific position when the view refreshes but doesn't do anything
if (isAtTopOfTable) {
_goToElement(25);
}
// set our length
length = numberOfChatsToDisplay;
final isPreviousPageLoadingState =
state is PreviousPageLoadingMessagesState;
final isPreviousPageErrorState = state is PreviousPageErrorMessagesState;
final isFinishedState = state is FinishedMessagesState;
if (isPreviousPageLoadingState || isPreviousPageErrorState) {
length = length + 50;
}
return SafeArea(
bottom: false,
minimum: EdgeInsets.only(
bottom: 20.0,
),
child: Column(
children: [
Expanded(
child: StreamBuilder<Event>(
stream: itemRef
.orderByKey()
.limitToLast(numberOfChatsToDisplay)
.onValue,
builder: (context, snapshot) {
if (snapshot.hasData &&
snapshot.connectionState == ConnectionState.active) {
WidgetsBinding.instance?.addPostFrameCallback((_) {
if (_scrollController.hasClients && _shouldAutoscroll) {
_scrollToBottom();
}
if (!_firstAutoscrollExecuted &&
_scrollController.hasClients) {
_scrollToBottom();
}
});
final Event? event = snapshot.data;
final Map<dynamic, dynamic> collection =
event?.snapshot.value as Map<dynamic, dynamic>;
if (collection != null) {
return NotificationListener<ScrollEndNotification>(
child: Container(
height: MediaQuery.of(context).size.height * 0.7,
child: ListView.builder(
padding: EdgeInsets.all(16.0),
controller: _scrollController,
key: PageStorageKey(50), // Tested this but doesn't work
shrinkWrap: true,
reverse: false,
physics: BouncingScrollPhysics(),
itemCount: messages.length,
itemBuilder: (BuildContext context, int index) {
savedIndex = index;
if (index >= numberOfChatsToDisplay) {
return const Offstage();
}
return CustomListTile();
},
),
),
onNotification: (notification) {
if (isPreviousPageLoadingState ||
isPreviousPageErrorState ||
isFinishedState) {
return true;
}
if (_scrollController.position.pixels == 0) {
// call my BLoC logic to load in new messages
}
return true;
},
);
}
return SizedBox.shrink();
} else {
return Container();
}
},
),
),
// Custom widget for adding a new message
],
),
);
}

Related

Flutter / Dart: How to add an extra widget at the bottom of a ListView?

I need to add a Text Widget at the end of a listview.
I came up with this code snippet based on my need. This code was based on this and this codes.
The Issue with this code is, it is not scrollable. How can I fix this? what is the best way to add a widget to the end of a ListView?
List<Widget> listItems = [];
int listItemCount = 0;
listItems.addAll(snapshot.data!.docs.map((DocumentSnapshot document) {
Map<String, dynamic> data = document.data()! as Map<String, dynamic>;
if (data['status'] == true) {
listItemCount++;
return ListTile(
title: Text(data['full_name']),
subtitle: Text(data['company']),
);
} else {
return SizedBox();
}
}).toList());
return ListView(children: <Widget>[
ListView(
shrinkWrap: true,
children: listItems,
),
(listItemCount > 0) ? Text('All Finish') : Text('Not available'),
]);
use ListView.separated
ListView.separated(
itemCount: listItems.length,
separatorBuilder: (BuildContext context, int index) {
if(index == ListItems.length-1){
return Container(height: 50, color: Colors.red);
}
else {
return SizedBox();
}
},
itemBuilder: (BuildContext context, int index) {
Why don't you add a tile to the end of listItems? Something like this:
...
const finalTile = ListTile(
title: Text((listItemCount > 0) ? Text('All Finish') : Text('Not available')),
);
listItems.add(finalTile)
return ListView(children: <Widget>[
ListView(
shrinkWrap: true,
children: listItems,
),
,
]);
Its simple.. Check this code snippet
return ListView.builder
(
itemCount: listItems.length + 1, // here is the trick, we are adding +1 for extra widget at bottom
itemBuilder: (BuildContext ctxt, int index) {
if (index < listItems.size - 1)
return new Text("List Item");
else
return new Text("Last Extra widget"); // This will be the extra widget at last
}
)

How to compare multiple fields in a query from Firestore?

I'm trying to find a way to compare the location of all users and show nearby people as a result. In this question, Frank explains how it should be done. But I have no idea how to do the third step.
That's what I've achieved so far:
double _userLatitude;
double _userLongitude;
_getUserLocation() async {
Position position = await Geolocator.getCurrentPosition(
desiredAccuracy: LocationAccuracy.high);
_userLatitude = position.latitude;
_userLongitude = position.longitude;
final firebaseUser = await FirebaseAuth.instance.currentUser();
if (firebaseUser != null)
await usersRef.document(firebaseUser.uid).updateData({
"location": "active",
"latitude": _userLatitude,
"longitude": _userLongitude,
});
}
_getNearbyUsers() {
usersRef.where('location', isEqualTo: "active").getDocuments();
Geolocator.distanceBetween(
_userLatitude, _userLongitude, endLatitude, endLongitude);
}
StreamBuilder(
stream: _getNearbyUsers(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return Center(
child: Text(
"Loading...",
),
);
} else if (snapshot.data.documents.length == 1) {
return Center(
child: Text(
":/",
),
);
}
return ListView.builder(
padding: EdgeInsets.only(top: 10),
scrollDirection: Axis.vertical,
itemCount: snapshot.data.documents.length,
itemBuilder: (context, index) =>
_buildListUsers(context, snapshot.data.documents[index]));
},
),
Widget _buildListUsers(BuildContext context, DocumentSnapshot document) {
List<Users> usersList = [];
var data = document.data;
if (data["id"] != _userId) {
Users user = Users();
user.id = document["id"];
usersList.add(user);
return ListTile(
onTap: () {
Navigator.pushNamed(context, "/users", arguments: user);
},
title: Text(
document['id'],
),
);
}
return SizedBox();
}
I have no idea what to do next in _getNearbyUsers().
The Geolocator.distanceBetween function returns the distance in meters. With the distance you should apply the logic for your application, like performing an action if the distance is smaller than 500m, for example.
Something like this:
double distanceInMeters = Geolocator.distanceBetween(_userLatitude, _userLongitude, endLatitude, endLongitude);
if (distanceInMeters < 500) {
// Your custom logic goes here
}

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.

waiting for async funtion to finish

I want to read some txts and store their text in an array. But because I need this array for my GUI it should wait until all is done.
Future<String> getFileData(String path) async {
return await rootBundle.loadString(path);
}
int topicNr = 3;
int finished = 0;
for (int topic = 1; topic <= topicNr; topic++) {
getFileData('assets/topic' + topic.toString() + '.txt').then(
(text) {
topics.add(text);
},
).whenComplete(() {
finished++;
});
}
while (finished < topicNr)
But when I run this code, finished won't update (I think because it is because the while loop runs on the main thread and so the async funtion can't run at the same time)
I could do this by just waiting, but this isn't really a good solution:
Future.delayed(const Duration(milliseconds: 10), () {
runApp(MaterialApp(
title: 'Navigation Basics',
home: MainMenu(),
));
});
How can I now just wait until all of those async Funtions have finished?
(sorry, I am new to Flutter)
One thing you could do is use a stateful widget and a loading modal. When the page is initialized, you set the view to be the loading modal and then call the function that gets the data and populate the data using set state. When you are done/when you are sure the final data has been loaded then you set the loading to false. See the example below:
class Page extends StatefulWidget {
page();
#override
State<StatefulWidget> createState() => new _Page();
}
class _Page extends State<Page>{
bool _loading = true; //used to show if the page is loading or not
#override
void initState() {
getFileData(path); //Call the method to get the data
super.initState();
}
Future<String> getFileData(String path) async {
return await rootBundle.loadString(path).then((onValue){
setState(() { //Call the data and then set loading to false when you are done
data = on value.data;
_loading = false;
});
})
}
//You could also use this widget if you want the loading modal ontop your page.
Widget IsloadingWidget() {
if (_loading) {
return Stack(
children: [
new Opacity(
opacity: 0.3,
child: const ModalBarrier(
dismissible: false,
color: Colors.grey,
),
),
new Center(
child: new CircularProgressIndicator(
valueColor:
new AlwaysStoppedAnimation<Color>(Colors.green),
strokeWidth: 4.0,
),
),
],
);
} else {
return Container();
}
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Stack(
children: <Widget>[
//If loading, return a loading widget, else return the page.
_loading ?
Container(
child: Center(
child: CircularProgressIndicator(
valueColor: new AlwaysStoppedAnimation<Color>(
Colors.blue))))
:Column(
children:<Widget>[
//Rest of your page.
]
)
]))
}
}
You could also set the fields of the initial data to empty values and the use set state to give them their actual values when you get the data.
so for example
string myvalue = " ";
#override
void initState() {
getFileData(path); //Call the method to get the data
super.initState();
}
//then
Future<String> getFileData(String path) async {
return await rootBundle.loadString(path).then((onValue){
setState(() { //Call the data and then set loading to false when you are done
data = on value.data;
myValue = onValue.data['val'];
_loading = false;
});
})
}
Let me know if this helps.
Use the FutureBuilder to wait for the API call to complete before building the widget.
See this example: https://flutter.dev/docs/cookbook/networking/fetch-data
runApp(MaterialApp(
title: 'Navigation Basics',
home: FutureBuilder(
future: getFileData(),
builder: (context, snapshot) {
if (snapshot.hasData) {
return MainMenu()
} else {
return CircularProgressIndicator();
}
));

Window is Full error in Flutter

When loading data from Firestore into a listview, i receive this warning
W/CursorWindow(15035): Window is full: requested allocation 483 bytes, free space 274 bytes, window size 2097152 bytes
I am suing scoped model pattern and getting user profile data in the model class. I saved all the user data into an array in the model class instead of using an streambuilder in the widget tree itself so that it is easier to page through the data, and it's frankly easier to read. However, going through the list, i receive a Window is Full warning, i understand that too much space is being allocated for this operation of storing the user profiles, however is there an alternative approach i can take to this problem?
class _ExploreScreenState extends State<ExploreScreen>
with SingleTickerProviderStateMixin {
Query _query;
AnimationController controller;
Animation<double> animation;
int limitNum = 4;
bool startAfter = false;
User _lastUser;
#override
Widget build(BuildContext context) {
return Container(
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Expanded(
child: new ScopedModel<ExploreModel>(
model: widget.model,
child: new ScopedModelDescendant<ExploreModel> (
builder: (context, child, model) =>
model.users.length != 0 ? GridView.builder(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
childAspectRatio: 0.8,
mainAxisSpacing: 5.0,
crossAxisSpacing: 5.0
),
itemCount: model.users.length,
itemBuilder: (BuildContext context, int index) {
if(index == model.users.length-1){
model.loadUsers();
}
return BrowseTile(model.users[index]);
}
) : Text('loading...'),
)
),
),
],
)
);
}
class ExploreModel extends Model {
List<User> _users;
Query _query;
User _currentUser;
int limitNum = 20;
List<User> get users => _users;
ExploreModel(this._currentUser) {
_users = new List();
loadUsers();
}
void loadUsers() {
_query = Firestore.instance.collection('Users').where(
'gender', isEqualTo: _currentUser.prefGender)
.limit(limitNum)
.orderBy('firstName')
.orderBy('lastName');
if (_users.length > 0) _query = _query.startAfter(
[_users[_users.length - 1].firstName, _users[_users.length - 1].lastName
]);
print('Array Size: ');
print(_users.length);
_query.snapshots().listen((snapshot) {
snapshot.documents.forEach((ds) {
User user = User.fromMap(ds.data, ds.documentID);
if(user.id != _currentUser.id){
bool _added = false;
_users.forEach((userEach) {
for(var i=0; i<_users.length; i++){
if(_users[i].id == user.id){
_users.remove(_users[i]);
_users.insert(i, user);
_added = true;
notifyListeners();
}
}
});
if (!_added) {
_users.add(user);
notifyListeners();
}
}
});
});
}
}
This seems to be a known issue as mentioned in this GitHub issue ticket, but this should have been resolved as of May 2020. I suggest upgrading the Flutter SDK version that you're using and try it again.

Resources