I need one-time read Data from Firebase Cloud, thats why I use FutureBuilder in my project (dart/flutter). But when the application is started it reads without stopping (as stream). What should I do to fix this?
class Hello extends StatefulWidget {
#override
_HelloState createState() => _HelloState();
}
class _HelloState extends State<Hello> {
Future getPosts() async{
QuerySnapshot qn = await FirebaseFirestore.instance.collection("111").get();
return qn.docs;
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: new Text('Hello'),
),
body: FutureBuilder(
future: getPosts(),
builder: (context, snapshot){
if(snapshot.connectionState == ConnectionState.waiting){
return Center(
child: CircularProgressIndicator(),
);
}
else{
return ListView.builder(
itemCount: snapshot.data.length,
itemBuilder: (context, index){
return Text(snapshot.data[index].data()["subject"]);
},
);
}
},
),
);
}
}
From the FutureBuilder doc :
The future must have been obtained earlier, e.g. during
State.initState, State.didUpdateConfig, 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.
Example :
Future<QuerySnapshot> future;
#override
void initState() {
super.initState();
future = Firestore.instance.collection("111").getDocuments();
}
// future : future
Related
I have a collection called orders. In this I have a subcollection known as stockQuantity. Every day when new stock is added, the stock added that day along with the date are added as documents in the subcollection stockQuantity. I am trying to find the stock added on the latest date for each order and display it on a page known as StockPage, however nothing is being displayed on the page. The order must meet the criteria, orderStatus = 1.
An index has been created too.
This is my code for StockPage
import 'package:flutter/material.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
class StockPage extends StatefulWidget {
#override
State<StockPage> createState() => _StockPageState();
}
class _StockPageState extends State<StockPage> {
final Stream<QuerySnapshot> _stockStream = FirebaseFirestore.instance
.collection('orders')
.where("statusOrder", isEqualTo: 1)
.orderBy("stockQuantity.date", descending: true)
.snapshots();
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Stock Quantity"),
),
body: StreamBuilder<QuerySnapshot>(
stream: _stockStream,
builder:
(BuildContext context, AsyncSnapshot<QuerySnapshot> snapshot) {
if (snapshot.hasError) {
return Text('Something went wrong');
}
if (snapshot.connectionState == ConnectionState.waiting) {
return Text("Loading");
}
return ListView.builder(
itemCount: snapshot.data!.docs.length,
itemBuilder: (context, index) {
var stockQuantity = snapshot.data!.docs[index]["stockQuantity"].last;
return ListTile(
title: Text(stockQuantity["quantity"].toString()),
subtitle: Text(stockQuantity["date"].toString()),
leading: Text('${snapshot.data!.docs.length}'),
);
},
);
}),
);
}
}
Nothing is being displayed on the page.
I tried using print statements to find the values of different variables, but none of them showed the stock quantity
I want to use shared preferences to store a list of names of files that the user has created. Upon launching the app, I want to show a list of those file names by reading them from shared preferences. How can I read shared preferences data (from an async function) and populate the data in a ListView which is placed inside a stateful widget?
class ListCard extends StatefulWidget {
const ListCard({
Key? key,
}) : super(key: key);
#override
State<ListCard> createState() => _ListCardState();
}
class _ListCardState extends State<ListCard> {
late List _dbList;
#override
void initState() {
super.initState();
_dbList = getDBNames().split(',');
}
#override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: _dbList.length,
itemBuilder: (BuildContext context, int index) {
return Card(
shape:
RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
child: Padding(
padding: const EdgeInsets.all(12.0),
child: Text(
_dbList[index],
),
),
);
},
);
}
}
The function to fetch the file names
getDBNames() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
var dbNames = prefs.getString('dbNames') ?? '';
return dbNames.split(',');
}
This gives me the following error message
Class 'Future<dynamic>' has no instance method 'split'.
Receiver: Instance of 'Future<dynamic>'
How do I adapt my code to use Future?
On a side note, is there a way to read a list of sqlite databases on app launch and display the same in a ListView?
getDBNames is a Future method, you can use FutureBuilder
class _ListCardState extends State<ListCard> {
late final future = getDBNames();
#override
Widget build(BuildContext context) {
return FutureBuilder<String?>(
future: future,
builder: (context, snapshot) {
if (snapshot.hasData) {
if (snapshot.data == null) return Text("got null data");
final _dbList = snapshot.data!.split(",");
return ListView.builder(
itemCount: _dbList.length,
itemBuilder: (BuildContext context, int index) {
return Card(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10)),
child: Padding(
padding: const EdgeInsets.all(12.0),
child: Text(
_dbList[index],
),
),
);
},
);
}
return CircularProgressIndicator();
});
}
}
Find more about FutureBuilder. Also you can save list on sharedPreference
also you can use .then and call setState but not looks good that way.
My problem is with Futures, because they should be obtained before build() method executed, as the documentation states:
The future must be obtained earlier, because 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.
I know that Futures should be called in initstate() function before the build method executed, but my case is different.
I want to get data from api as a Future, but the request I am sending to the api needs some parameters that user should select inside the screen's build() method.
And I don't know what the parameter of the request will be until user selects in build() method, and I have to call the api in the build() method and use FutureBuilder there, but that makes FutureBuilder to get constantly called, and I don't want that.
basically, I don't want to call FutureBuilder indefinetely, and I can't put my Future inside initState() because the Future needs some parameters that user later selects when the screen is shown inside build() method.
inside the build method:
FutureBuilder<List<LatLng>>(
builder: (context, snapshot) {
if (snapshot.hasData) {
return PolylineLayer(
polylines: [
Polyline(
points: snapshot.data!,
strokeWidth: 4,
color: Colors.purple),
],
);
} else if (snapshot.hasError) {
return Text("${snapshot.error}");
} else {
return Container();
}
},
future: Provider.of<NavigationProvider>(context)
.getNavigationPoints(pointToGoTo!),
),
now if you look at the code, at the final lines, I am sending the parameter pointToGoTo to the function which calls the backend.
simply, I want to get rid of calling api and getting data back as a Future inside build method, I want to do it in initState or somewhere else that prevents the build methods calling backend indefinitely.
is there any way to fix this problem?
Thanks in advance.
Firstly, create future state variable and a nullable params and use it with conditional if while using FutureBuilder.
I will recommend checking Fixing a common FutureBuilder and StreamBuilder problem
Now you can follow this example. It is missing progressBar on API recall, StreamBuilder might be better option in cases like this.
class Foo extends StatefulWidget {
const Foo({super.key});
#override
State<Foo> createState() => _FooState();
}
class _FooState extends State<Foo> {
int? params;
Future<int> fetch(int? data) async {
await Future.delayed(Duration(seconds: 1));
return (params ?? 0) * 2;
}
late Future<int> future = fetch(params);
#override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
children: [
DropdownButton<int?>(
value: params,
items: List.generate(
12,
(index) => DropdownMenuItem(
value: index,
child: Text("$index"),
)).toList(),
onChanged: (value) {
future =
fetch(params); // this will only call api with update data
setState(() {
params = value;
});
},
),
if (params != null)
FutureBuilder<int>(
future: future,
builder: (context, snapshot) {
if (snapshot.hasData) return Text("${snapshot.data}");
return CircularProgressIndicator();
},
)
],
),
);
}
}
class Testing extends StatefulWidget {
const Testing({super.key});
#override
State<Testing> createState() => _TestingState();
}
class _TestingState extends State<Testing> {
bool isFetched = false;
#override
Widget build(BuildContext context) {
return Scaffold(
body: Consumer<SomethingProvider>(
builder: (context, prov, child) {
if (!isFetched) {
prov.getData("a", "b");
Future.delayed(const Duration(milliseconds: 200), () {
isFetched = true;
});
}
if (prov.newData.isNotEmpty) {
return Column(
// make widget tree from here
);
} else {
return const Center(
child: CircularProgressIndicator(),
);
}
},
),
);
}
}
class SomethingProvider extends ChangeNotifier {
List newData = [];
Future getData(param1, param2) async {
newData = ["testingdata"];
}
}
When I delete the item from the list, then I go back and refresh the page, RefreshIndicator seems not working(The animation is working but not refreshing the page). I have searched a lot about this problem. I tried everything I found on the web but none of them worked for me. The problem is that I have the method of _refresh to call this method onRefresh but it didn't work. I debugged the code to see whether the refresh method is being called. As far as I see it seems it is being called because I see refresh method is called on the debug console. The ListView.builder also has the physics property and it's not shrunk. I saw one more solution that suggests adding items that fill the whole screen. I added as many items as I can but it didn't work. So any suggestions? I am suspecting from the FutureBuilder that is a parent of the ListView.builder, I tried to cover the FutureBuilder too but it didn't work either.
class _DraftsState extends State<Drafts> {
final SQFLiteHelper _helper = SQFLiteHelper.instance;
#override
void initState() {
print('init state is called');
super.initState();
_helper.getForms();
}
Future<void> _refresh() async {
print('refresh method is called');
await _helper.getForms();
}
//TODO: RefreshIndicator not working.
//TODO:When the list changed nothing is happening until the draft section is rebuilt
#override
Widget build(BuildContext context) {
return Scaffold(
body: FutureBuilder<List<FormData>?>(
future: _helper.getForms(),
builder:
(BuildContext context, AsyncSnapshot<List<FormData>?> snapshot) {
if (snapshot.hasData && snapshot.data!.isEmpty) {
return const Center(
child: Text("Henüz kaydedilmiş taslak bulunmamaktadır."));
}
if (snapshot.hasError) {
return Center(
child: Text(
'Bir şeyler ters gitti.',
style: TEXT_STYLE,
));
}
if (snapshot.connectionState == ConnectionState.done) {
return RefreshIndicator(
backgroundColor: Colors.grey[700],
color: LIGHT_BUTTON_COLOR,
onRefresh: _refresh,
child: SizedBox(
height: MediaQuery.of(context).size.height,
child: ListView.builder(
physics: const AlwaysScrollableScrollPhysics(),
itemCount: snapshot.data!.length,
itemBuilder: (BuildContext context, int index) {
return CustomListTile(
formData: snapshot.data![index], index: index);
},
),
),
);
}
return const Center(
child: CircularProgressIndicator(),
);
}),
);
}
}
Future<void> _refresh() async {
print('refresh method is called');
setState(() {
await _helper.getForms();
});
}
use setState in your refresh function. coz you need to reload the build method. or I think you can use setState like this.
Future<void> _refresh() async {
print('refresh method is called');
await _helper.getForms();
setState(() { });
}
I am implementing a Logout widget that will only appear when user is logged in.
I used this DrawerLogout widget inside a Drawer with a ListView
class DrawerLogout extends StatefulWidget {
#override
_DrawerLogoutState createState() => _DrawerLogoutState();
}
class _DrawerLogoutState extends State<DrawerLogout> {
Stream authState = FirebaseAuth.instance.authStateChanges();
#override
Widget build(BuildContext context) {
return StreamBuilder(
stream: authState,
builder: (context, snapshot) {
if (snapshot.hasData) {
return ListTile(
title: Text(
'Logout',
style: TextStyle(color: Theme.of(context).errorColor),
),
onTap: () {
FirebaseAuth.instance.signOut();
},
);
}
return Container();
});
}
}
Something like this:
Scaffold(
drawer: Drawer(
child: ListView(
children:[
...
...
DrawerLogout(),
])
)
)
The problem is the logout button only shows for the first time when I open the drawer, after closing the drawer and reopen it, the logout button disappear.
This is the error code when it disappear:
[ERROR:flutter/lib/ui/ui_dart_state.cc(177)] Unhandled Exception: Bad state: Cannot add new events while doing an addStream
As discussed in comments, moving the state initialization to initState solved the issue.
The issue was caused by new events being added into a disposed stream. As per docs:
If a State's build method depends on an object that can itself change state, for example a ChangeNotifier or Stream, or some other object to which one can subscribe to receive notifications, then be sure to subscribe and unsubscribe properly in initState, didUpdateWidget, and dispose:
In initState, subscribe to the object.
In didUpdateWidget unsubscribe from the old object and subscribe to the new one if the updated widget configuration requires replacing the object.
In dispose, unsubscribe from the object.
As #JoyTerence suggested, initializing the stream in initState works!
class DrawerLogout extends StatefulWidget {
#override
_DrawerLogoutState createState() => _DrawerLogoutState();
}
class _DrawerLogoutState extends State<DrawerLogout> {
Stream authState;
#override
void initState() {
// TODO: implement initState
super.initState();
authState = FirebaseAuth.instance.authStateChanges();
}
#override
Widget build(BuildContext context) {
return StreamBuilder(
stream: authState,
builder: (context, snapshot) {
if (snapshot.hasData) {
return ListTile(
title: Text(
'Logout',
style: TextStyle(color: Theme.of(context).errorColor),
),
onTap: () {
FirebaseAuth.instance.signOut();
},
);
}
return Container();
});
}
}