How to paginate using Future Builder widget and flutter firestore? - flutter

import 'package:flutter/cupertino.dart';
class FarmplaceScreen extends StatefulWidget {
const FarmplaceScreen({Key key}) : super(key: key);
#override
_FarmplaceScreenState createState() => _FarmplaceScreenState();
}
class _FarmplaceScreenState extends State<FarmplaceScreen>
with AutomaticKeepAliveClientMixin {
final _nativeAdController = NativeAdmobController();
int limit = 15;
DocumentSnapshot lastVisible;
bool _hasNext = true;
bool _isFetching = false;
bool needMore = false;
final List<DocumentSnapshot> allProducts = [];
var productFuture;
var _getProductFuture;
ScrollController _scrollController = new ScrollController();
#override
void initState() {
super.initState();
if(lastVisible == null) productFuture =getUsers();
_scrollController.addListener(() {
if(_scrollController.offset >= _scrollController.position.maxScrollExtent){
if(_hasNext){
productFuture = getUsers();
setState(() {
_isFetching = true;
});
}
}
});
}
Future <QuerySnapshot> getUsers() {
if(_isFetching) return Future.value();
final refUsers = FirebaseFirestore.instance.collection('product').orderBy('publishedDate').limit(15);
Future.value(refUsers.startAfterDocument(allProducts.last).get());
if(lastVisible == null){
return Future.value(refUsers.get());
}
else{
return Future.value(refUsers.startAfterDocument(lastVisible).get());
}
}
#override
void dispose() {
_scrollController.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
super.build(context);
return Container(
child: FutureBuilder<QuerySnapshot>(
future: productFuture,
builder: (BuildContext context, AsyncSnapshot<QuerySnapshot> snapshot) {
if (snapshot.hasError) {
return ErrorDisplay();
}
if (snapshot.connectionState == ConnectionState.waiting) {
return Container(
child: Center(child: circularProgress()),
);
}
lastVisible = snapshot.data.docs[snapshot.data.docs.length-1];
if (snapshot.data.docs.length < 15){
_hasNext = false;
}
if (snapshot.connectionState == ConnectionState.waiting){
_isFetching = true;
}
if (snapshot.connectionState == ConnectionState.done){
_isFetching = false;
}
allProducts.addAll(snapshot.data.docs);
return new GridView.countBuilder();
},
)
);
}
}
#override
bool get wantKeepAlive => true;
}
Hello Folks,
I am trying to achieve pagination using flutter Future builder widget.
Situation:
I am able to load first 15 products using the method stated above.
The problem occurs when I try to load the next 15 products.
I get the next next 15 products in line but, the future builder widget rebuilds. Now, to avoid the rebuild I have tried to initialize the future (productFuture) in the initState, but it dosen't solve the problem.
I tried setting _getProductFuture = productFuture in the initstate and then using _getProductFuture as the future in the FutureBuilder widget. In this case the widget doesn't rebuild but, the first 15 products are repeated everytime I scroll to the bottom of the screen.
Please suggest how I can stop this unnecessary rebuild of the FutureBuilder widget.
FYI: AbdulRahmanAlHamali's solution on GitHub dosen't work in this case.

Related

How to wait until datas are loaded?

I try to learn flutter and i face an issue with data loading.
I get information from sqlite database to display them in my homepage.
When starting my app, i have an error :
LateInitializationError: Field 'child' has not been initialized.
late MonneyServices monneyServices;
late ChildDAO childDAO;
late Child child;
void initState() {
super.initState();
this.monneyServices = MonneyServices();
monneyServices.getChild().then((Child child) {
this.child = child;
setState(() {});
});
the getChild method is async
Future<Child> getChild() async {
//return Child(1, 'Alice2', 100);
Child child = Child(1, 'A', 1);
this.childDAO.insertChild(Child(1, "Alice", 10));
List<Child> childList = await this.childDAO.getChilds();
child = childList.first;
print(childList.first);
return child;
}
I use so datas in
#override
Widget build(BuildContext context)
How can i wait until datas are loaded ?
Thanks for your help
You could use FutureBuilder.
It lets you to await for a future to complete and return a different widget according to the future status.
In your case you should use it in the build method and not in initState.
You should use it more or less like so:
Widget build(BuildContext context) {
return FutureBuilder<Widget>(context, snapshot){
if(snapshot.hasData){ //If the future has completed
return snapshot.data; //You return the widget it completed to
} else {
return CircularProgressIndicator(); //Otherwise, return a progress indicator
}
}
}
you can use a boolean variable be sure the data is loaded and reflect this in the build
late MonneyServices monneyServices;
late ChildDAO childDAO;
late Child child;
bool isLoading = true; // <--
void initState() {
super.initState();
this.monneyServices = MonneyServices();
monneyServices.getChild().then((Child child) {
this.child = child;
isLoading = false; // <--
setState(() {});
});
and in the build:
#override
Widget build(BuildContext context) {
if(isLoading) {
return Text('loading...');
}
return child;
}

StreamBuilder / ChangeNotifierProvider- setState() or markNeedsBuild() called during build

Streambuilder, ChangeNotifier and Consumer cannot figure out how to use correctly. Flutter
I've tried and tried and tried, I've searched a lot but I cannot figure this out:
I'm using a Streambuilder this should update a ChangeNotifier that should trigger rebuild in my Consumer widget. Supposedly...
but even if I call the provider with the (listen: false) option I've got this error
The following assertion was thrown while dispatching notifications for
HealthCheckDataNotifier: setState() or markNeedsBuild() called during
build. the widget which was currently being built when the offending call was made was:
StreamBuilder<List>
Important: I cannot create the stream sooner because I need to collect other informations before reading firebase, see (userMember: userMember)
Widget build(BuildContext context) {
return MultiProvider(
providers: [
/// I have other provider...
ChangeNotifierProvider<HealthCheckDataNotifier>(create: (context) => HealthCheckDataNotifier())
],
child: MaterialApp(...
then my Change notifier look like this
class HealthCheckDataNotifier extends ChangeNotifier {
HealthCheckData healthCheckData = HealthCheckData(
nonCrewMember: false,
dateTime: DateTime.now(),
cleared: false,
);
void upDate(HealthCheckData _healthCheckData) {
healthCheckData = _healthCheckData;
notifyListeners();
}
}
then the Streambuilder
return StreamBuilder<List<HealthCheckData>>(
stream: HeathCheckService(userMember: userMember).healthCheckData,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.active) {
if (snapshot.hasData) {
if (snapshot.data!.isNotEmpty) {
healthCheckData = snapshot.data?.first;
}
if (healthCheckData != null) {
timeDifference = healthCheckData!.dateTime.difference(DateTime.now()).inHours;
_cleared = healthCheckData!.cleared;
if (timeDifference < -12) {
healthCheckData!.cleared = false;
_cleared = false;
}
///The problem is here but don't know where to put this or how should be done
Provider.of<HealthCheckDataNotifier>(context, listen: false).upDate(healthCheckData!);
}
}
return Builder(builder: (context) {
return Provider<HealthCheckData?>.value(
value: healthCheckData,
builder: (BuildContext context, _) {
return const HealthButton();
},
);
});
} else {
return const Text('checking health'); //Scaffold(body: Loading(message: 'checking...'));
}
});
and finally the Consumer (note: the consumer is on another Route)
return Consumer<HealthCheckDataNotifier>(
builder: (context, hN, _) {
if (hN.healthCheckData.cleared) {
_cleared = true;
return Container(
color: _cleared ? Colors.green : Colors.amber[900],
Hope is enough clear,
Thank you so very much for your time!
it is not possible to setState(or anything that trigger rerender) in the builder callback
just like you don't setState in React render
const A =()=>{
const [state, setState] = useState([])
return (
<div>
{setState([])}
<p>will not work</p>
</div>
)
}
it will not work for obvious reason, render --> setState --> render --> setState --> (infinite loop)
so the solution is similar to how we do it in React, move them to useEffect
(example using firebase onAuthChange)
class _MyAppState extends Stateful<MyApp> {
StreamSubscription<User?>? _userStream;
var _waiting = true;
User? _user;
#override
void initState() {
super.initState();
_userStream = FirebaseAuth.instance.authStateChanges().listen((user) async {
setState(() {
_waiting = false;
_user = user;
});
}, onError: (error) {
setState(() {
_waiting = false;
});
});
}
#override
void dispose() {
super.dispose();
_userStream?.cancel();
}
#override
Widget build(context) {
return Container()
}
}

Flutter: Stateful Widget does not update

Imagine two Widgets: Main that manages a tabbar and therefore holds several Widgets - and Dashboard.
On Main Constructor I create a first Instance of Dashboard and the other tabbar Widgets with some dummy data (they are getting fetched in the meanwhile in initState). I build these with Futurebuilder. Once the data arrived I want to create a new Instance of Dashboard, but it won't change.
class _MainState extends State<HomePage> {
var _tabs = <Widget>[];
Future<dynamic> futureData;
_MainState() {
_tabs.add(Dashboard(null));
}
#override
void initState() {
super.initState();
futureData = _getData();
}
#override
Widget build(BuildContext context) {
return FutureBuilder(
future: futureData,
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (snapshot.data != null) {
tabs[0] = Dashboard(snapshot.data);
} else {
return CircularProgressIndicator();
}
});
}
}
class DashboardScreen extends StatefulWidget {
final data;
DashboardScreen(this.data,
{Key key})
: super(key: key) {
print('Dashboard Constructor: ' + data.toString());
}
#override
_DashboardScreenState createState() => _DashboardScreenState(data);
}
class _DashboardScreenState extends State<DashboardScreen> {
var data;
_DashboardScreenState(this.data);
#override
void initState() {
super.initState();
print('InitState: ' + data.toString());
}
#override
void didUpdateWidget(Widget oldWidget) {
super.didUpdateWidget(oldWidget);
print('didUpdateWidget');
}
#override
void didChangeDependencies() {
super.didChangeDependencies();
print('didChangeDependencies' + data.toString());
}
#override
Widget build(BuildContext context) {
return Text(data.toString());
}
}
When I print on several available methods it comes clear that the DasboardScreenState is not recreated. Only the DashboardScreen Constructor is called again when the data arrived, but not it's state...
flutter: MainConstructor: null
flutter: Dashboard Constructor: null
flutter: InitState: null
flutter: didChangeDependencies: null
flutter: Dashboard Constructor: MachineStatus.Manual <- Here the data arrived in futureBuilder
How can I force the State to recreate? I tried to use the key parameter with UniqueKey(), but that didn't worked. Also inherrited widget seems not to be the solution either, despite the fact that i don't know how to use it in my use case, because the child is only available in the ..ScreenState but not the updated data..
I could imagine to inform dashboardScreenState by using Stream: listen to messages and then call setState() - I think, but that's only a workaround.
Can anyone help me please :)?
I know I have had issues with the if statement before, try:
return FutureBuilder(
future: futureData,
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (snapshot.hasData) { //use hasData
DataType data = snapshot.data; //Declare Values first
tabs[0] = Dashboard(data);
} else {
return CircularProgressIndicator();
}
});

How to reference variable in method in FutureBuilder (builder:)?

I want to use the variable dbRef in inputData() in future Builder builder: you can see the variable in between asterisk .
void inputData() async {
FirebaseUser user = await FirebaseAuth.instance.currentUser();
final uid = user.uid;
final **dbRef** = FirebaseDatabase.instance.reference().child("Add Job Details").child(uid).child("Favorites");
}
#override
Widget build(BuildContext context) {
return FutureBuilder (
future: **dbRef**.once(),
builder: (context, AsyncSnapshot<DataSnapshot> snapshot) {
if (snapshot.hasData) {
List<Map<dynamic, dynamic>> list = [];
for (String key in snapshot.data.value.keys) {
list.add(snapshot.data.value[key]);
}
This is one more approach to tackle the problem.
The idea is to use a variable _loading and set it to true initially.
Now, after in your inputData() function, you can set it to false once you get the dbref.
Store dbref, the way I stored _myFuture in the code below i.e., globally within the class.
Use your _loading variable to return a progress bar if its true else return FutureBuilder with your dbref.once() in place. Now, that you have loaded it, it should be available at this point.
class MyWidget extends StatefulWidget {
#override
createState() => _MyWidgetState();
}
class _MyWidgetState extends State<MyWidget> {
// Is the future being loaded?
bool _loading;
// This is the future we will be using in our FutureBuilder.
// It is currently null and we will assign it in _loadMyFuture function.
// Until assigned, we will keep the _loading variable as true.
Future<String> _myFuture;
// Load the _myFuture with the future we are going to use in FutureBuilder
Future<void> _loadMyFuture() async {
// Fake the wait for 2 seconds
await Future.delayed(const Duration(seconds: 2));
// Our fake future that will take 2 seconds to return "Hello"
_myFuture = Future(() async {
await Future.delayed(const Duration(seconds: 2));
return "Hello";
});
}
// We initialize stuff here. Remember, initState is called once in the beginning so hot-reload wont make flutter call it again
#override
initState() {
super.initState();
_loading = true; // Start loading
_loadMyFuture().then((x) => setState(() => _loading = false)); // Set loading = false when the future is loaded
}
#override
Widget build(BuildContext context) {
// If loading, show loading bar
return _loading?_loader():FutureBuilder<String>(
future: _myFuture,
builder: (context, snapshot) {
if(!snapshot.hasData) return _loader(); // still loading but now it's due to the delay in _myFuture
else return Text(snapshot.data);
},
);
}
// A simple loading widget
Widget _loader() {
return Container(
child: CircularProgressIndicator(),
width: 30,
height: 30
);
}
}
Here is the output of this approach
This does the job but, you might need to do it for every class where you require your uid.
========================================
Here is the approach I described in the comments.
// Create a User Manager like this
class UserManager {
static String _uid;
static String get uid => _uid;
static Future<void> loadUID() async {
// Your loading code
await Future.delayed(const Duration(seconds: 5));
_uid = '1234'; // Let's assign it directly for the sake of this example
}
}
In your welcome screen:
class MyWidget extends StatefulWidget {
#override
createState() => _MyWidgetState();
}
class _MyWidgetState extends State<MyWidget> {
bool _loading = true;
#override
void initState() {
super.initState();
UserManager.loadUID().then((x) => setState(() => _loading = false));
}
#override
Widget build(BuildContext context) {
return _loading ? _loader() : Text('Welcome User ${UserManager.uid}!');
}
// A simple loading widget
Widget _loader() {
return Container(child: CircularProgressIndicator(), width: 30, height: 30);
}
}
The advantage of this method is that once you have loaded the uid, You can directly access it like this:
String uid = UserManager.uid;
thus eliminating use of futures.
Hope this helps!

Get data from future function to text widget

I am trying to implement a Column with a Text:
Column(
children: <Widget>[
Text('data from future function')
],
),
I can't get the data from initState() cause initState() it's only void
If I get data directly from the function
Text(function)
I get
instance of function
The function:
Future<double> calculate(int index) async {
LocData _getUser = await getLoc();
double uLat = _getUser.latitude;
double uLng = _getUser.latitude;
double pLat = parks[data].loc.lat;
double pLng = parks[data].loc.lng;
double dis = await Geolocator()
.distanceBetween(uLat , uLng, uLng , pLat );
return dis ;
}
Any idea what can i do to get this data from the function directly to the text wigdet?
There 2 ways to get data from a future.
Option #1:
(with 2 suboptions)
class MyWidgetState extends State<MyWidget> {
String _someAsyncData;
#override
void initState() {
super.initState();
// opt 1.
aDataFuture.then((val) {
setState(() {
_someAsyncdata = val;
});
});
// opt 2.
_setAsyncData(aDataFuture);
}
void _setAsyncData(Future<String> someFuture) async {
// void + async is considered a "fire and forget" call
// part of opt. 2
_someAsyncData = await someFuture;
// must trigger rebuild with setState
setState((){});
}
Widget build(BuildContext context) {
return _someAsyncData == null ? Container() : Text('$_someAsyncData');
}
}
Option #2
Use FutureBuilder
class MyWidget extends StatelessWidget {
Widget build(BuildContext context) {
return FutureBuilder<String>(
future: _someFuture,
builder: (ctx, snapshot) {
// can also check for snapshot.hasData or snapshot.hasError to
// provide more user feedback eg.
if(snapshot.connectionState == ConnectionState.done)
return Text('${snapshot.data}');
return Text('No data available yet...');
}
);
}
}
Here is the full working code.
class _InfoPageState extends State<InfoPage> {
String _text = "";
#override
void initState() {
super.initState();
calculate(10).then((value) {
setState(() {
_text = value.toString();
});
});
}
Future<double> calculate(int index) async {
LocData _getUser = await getLoc();
double uLat = _getUser.latitude;
double uLng = _getUser.latitude;
double pLat = parks[data].loc.lat;
double pLng = parks[data].loc.lng;
double dis = await Geolocator().distanceBetween(uLat, userLng, uLng, pLat);
return dis;
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Column(children: <Widget>[Text(_text)]),
);
}
}