How to get firestore data in another widget - firebase

What the app can do so far: So far, I have an app that shows markers on a Google Map that get the coordinates from Firestore.
What I want: I want that when the user presses a marker, he comes to another screen on which some more data is presented (the data is also on the firestore).
The problem: I don't know how to get the data (for example the name) on the screen on which the details are to be displayed.
I have a onTap function, which calls goToDetailPage()
That`s the goToDetailPage() function:
void goToDetailPage() {
Navigator.of(context).push(new MaterialPageRoute(
builder: (BuildContext context) => new detailPage()));
}
That's the code for "drawing" the marker:
void initMarker(specify, specifyId) async {
var markerIdVal = specifyId;
final MarkerId markerId = MarkerId(markerIdVal);
final Marker marker = Marker(
markerId: markerId,
position:
LatLng(specify['location'].latitude, specify['location'].longitude),
infoWindow: InfoWindow(title: specify['name'], snippet: 'Shop'),
onTap: () {
goToDetailPage();
});
print(specify['location'].latitude);
nameTest = specify['name'];
setState(() {
markers[markerId] = marker;
print(markerId.toString() + '__________________________');
});
}
That's the code from the detail Page:
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Detail Page'),
),
body: Column(children: <Widget>[Text(specify['name'])]),
);
The problem is that the specify from the specify['name'] in the detail page is red underlined and I don`t know what's the reason for that.

In the second page:
class Tester extends StatefulWidget {
String dataFromLastPage;
Tester({Key key, this.dataFromLastPage}) : super(key: key);
#override
_TesterState createState() => _TesterState();
}
class _TesterState extends State<Tester> {
#override
Widget build(BuildContext context) {
return Text(widget.dataFromLastPage); //"thisWillGoToTheNextPage!"
}
}
When navigating to that page, send data like this:
Navigator.of(context).push(new MaterialPageRoute(
builder: (BuildContext context) => new Tester(dataFromLastPage: 'thisWillGoToTheNextPage!')));

Try and go accoording to what the docs say : https://firebase.flutter.dev/docs/firestore/usage/#realtime-changes
However , there are several things the doc doesn't specify like
To use firestore first , you need to initialize you firestore app , for that you just write a specific piece of code in the main method of your dart file
void main() async{
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
runApp(MyApp());
}
To connect to your firestore database , simply type in your widget build function
CollectionReference <Variable name> = FirebaseFirestore.instance.collection(<Name of collection as string>);
Now if you are creating a list , you can simply use a StreamBuilder function and get the data in your document via (initialize snapshot as given in the documentation)
snapshot.data.docs[index].data()[<Identifier of your document such as
'Name' or 'location'>]

Related

Flutter Streambuilder stream inconsistent FirebaseFirestore snapshot data

Retrieving shopping cart items my snapshot data is inconsistent. When one item is in cart I get this correctly formatted result:
{1111111111111: 1, PriceSmart: 540.0}
When two items in cart, and second item is also "PriceSmart", I get an error because returns this result:
{1111111111111: 1, PriceSmart: 300.0, 5555555555555: 1}
and should be:
{1111111111111: 1, PriceSmart: 540.0, 5555555555555: 1, PriceSmart: 300.0}
This is my firebase data structure:
First cart item:
Second cart item:
Basically is combining the "seller" (PriceSmart), when I need to return complete data from each cart item, otherwise I get an error as soon as I have more than one item in cart and seller is the same.
Please check the Stream in my code and see what is wrong with this implementation:
class PriceUpdaterWidget extends StatefulWidget {
const PriceUpdaterWidget({
Key? key,
required this.loginService,
required this.code,
required this.itemSubCategory,
}) : super(key: key);
final LoginService loginService;
final String? code;
final SubCategory? itemSubCategory;
_PriceUpdaterWidgetState createState() => _PriceUpdaterWidgetState();
}
class _PriceUpdaterWidgetState extends State<PriceUpdaterWidget> {
#override
Widget build(BuildContext context) {
CategorySelectionService catSelection =
Provider.of<CategorySelectionService>(context, listen: false);
Stream<DocumentSnapshot> priceDocStream = FirebaseFirestore.instance
.collection('shoppers')
.doc(widget.loginService.loggedInUserModel!.uid)
.collection("cartItems")
.doc(widget.code)
.snapshots();
return StreamBuilder<DocumentSnapshot>(
stream: priceDocStream,
builder:
(BuildContext context, AsyncSnapshot<DocumentSnapshot> snapshot) {
SellerNameService isSellerName =
Provider.of<SellerNameService>(context, listen: false);
var sellerName = isSellerName.isSellerName;
if (snapshot.data != null) {
return Text(
snapshot.data![sellerName].toStringAsFixed(2),
textAlign: TextAlign.center,
);
} else {
return Text('No Data');
}
});
}
}
Without seeing more of your code, it's hard to make an example to exactly fit your specification, and the error you're getting doesn't match the code you posted, but, broadly, you need to:
move the Stream outside your build function and into initState
process each snapshot one at a time
stay away from processing the data in your build widget
class PriceUpdaterWidget extends StatefulWidget {
final String login, code;
const PriceUpdaterWidget(this.login, this.code);
#override
_PriceUpdaterWidgetState createState() => _PriceUpdaterWidgetState ();
}
class _PriceUpdaterWidgetState extends State<PriceUpdaterWidget> {
Stream<DocumentSnapshot> priceStream; // only one stream per widget
#override
void initState() {
super.initState();
priceStream = FirebaseFirestore.instance // set the stream once
.collection("shoppers")
.doc(widget.login)
.collection("cartItems")
.doc(widget.code)
.snapshots();
}
#override
Widget build(BuildContext context) => StreamBuilder<DocumentSnapshot>(
stream: priceStream,
builder: (context, snapshot) {
const String sellerName = "PriceSmart";
return snapshot.data == null
? const Text("No data")
: Text(
snapshot.data[sellerName].toStringAsFixed(2),
textAlign: TextAlign.center,
);
}
);
}

Firestore one-time read using Flutter, i got to printing document data in console want to output it in UI

In my Firestore DB, inside 'location' collection i have 2 docs,(named as Europe,Australia) having a field 'name' with their string values (same as their document names).
I have worked with StreamBuilder and Streams before, but this time i dont want real-time calls, but just once.
I wanna print that 'name' field data of all the docs inside location collection.
This is what my UI code looks like:
class HomePage extends StatefulWidget {
#override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
double spaceInBetween = 25;
#override
Widget build(BuildContext context) {
DatabaseService().getData();
return Scaffold(
body: Container(
child: Text("data here")
);
}
I wanna print all that documents data, with all their names using ListView.builder() on the HomePage.
This is my DatabaseService class (using the official FlutterFire Docs https://firebase.flutter.dev/docs/firestore/usage/ but didnt find what i was looking for)
class DatabaseService {
final locationCollection = FirebaseFirestore.instance.collection("location");
getData() async {
await locationCollection.get().then((QuerySnapshot querySnapshot) {
querySnapshot.docs.forEach((doc) {
print(doc['name']);
});
});
}
}
Also wanted to know if there's any other way i could do this, using FutureBuilder or anything else, just wanna get field data from all docs in a collection from Firestore and print it (im still learning).
Thank you :)
I think the answer is FutureBuilder. You can create a Future method which is going to get datas from Firebase servers and return it. After that you just create a FutureBuilder which is going to help you to show datas and if something wrong with the server or the internet connection you will not get any error messages because FutureBuilder will show an CircularProgressIndicator.
I made a demo code for you to demostrate FutureBuilder.
class MyHomePage extends StatefulWidget {
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
final locationCollection = FirebaseFirestore.instance.collection("location");
#override
void initState() {
super.initState();
}
Future<List<String>> getData() async {
List<String> name = [];
await locationCollection.get().then((QuerySnapshot querySnapshot) {
querySnapshot.docs.forEach((doc) {
name = doc['name'];
});
});
return name;
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
child: Center(
child: FutureBuilder<List<String>>(
future: getData(), // call getData method
builder: (context, snapshot) {
List<String> nameList = snapshot.data ?? []; // create a local variable which is storing data from the getData method
if (snapshot.hasError) print(snapshot.error);
return snapshot.hasData
? ListView.builder( // if getData method give datas listviewbuilder is going to show datas
itemCount: nameList.length,
itemBuilder: (BuildContext context, int index) {
return ListTile(
title: Text(nameList[index]),
);
},
)
: Center(child: CircularProgressIndicator()); // if something wrong with the server or with the internet you will see a CircularProgressIndicator
}),
),
),
);
}
}
In order to ensure you only get the data once, you can use a FutureBuilder and ensure you define the future outside the build method (for example in the initState) so that it doesn't get called again whenever the build method is called.
FutureBuilder
...
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.
https://api.flutter.dev/flutter/widgets/FutureBuilder-class.html
Update the getData method of your DatabaseService class to this below:
Future<List<String>> getData() async {
final QuerySnapshot locationDataSnapshot = await locationCollection.get();
final List<String> listOfNames = locationDataSnapshot.docs
.map((QueryDocumentSnapshot documentSnapshot) =>
documentSnapshot.data()['name'] as String)
.toList();
return listOfNames;
}
This code above fetches the list of documents from the location collection and maps them to a list of names, which is then returned.
You can then get define the future object to get this data in your initState and use it in your FutureBuilder like shown below:
class MyHomePage extends StatefulWidget {
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
Future<List<String>> _nameListFuture;
#override
void initState() {
super.initState();
_nameListFuture = DatabaseService().getData();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: FutureBuilder<List<String>>(
future: _nameListFuture,
builder: (context, snapshot) {
if (snapshot.data == null) {
return Center(child: CircularProgressIndicator());
}
final List<String> nameList = snapshot.data;
return ListView.builder(
itemCount: nameList.length,
itemBuilder: (context, index) => Text(nameList[index]),
);
},
),
);
}
}

How do I execute FutureBuilder only once in a list that gets reinitialized?

I have created a Flutter project that has a home page with a bottom navigation bar. I used an IndexedStack as the body.
I'm trying to make my CustomList() a feed which shows the most recent documents.
I intend to use pagination too.
class Home extends StatefulWidget {
#override
_HomeState createState() => _HomeState();
}
class _HomeState extends State<Home> {
final widgetOptions = [
CustomList(),
Page2(),
Page3(),
Page4(),
];
int _selectedItemPosition = 0;
#override
Widget build(BuildContext context) {
return Scaffold(
bottomNavigationBar: BottomNavigationBar(
//
currentIndex: _selectedItemPosition,
onPositionChanged: (index) => setState(() {
_selectedItemPosition = index;
}),
items: [
BottomNavigationBarItem(),
BottomNavigationBarItem(),
BottomNavigationBarItem(),
BottomNavigationBarItem()
],
),
body: IndexedStack(
index: _selectedItemPosition,
children: widgetOptions,
),
);
}
}
This is the code of my CustomList():
class CustomList extends StatefulWidget {
#override
_CustomListState createState() => _CustomListState();
}
class _CustomListState extends State<CustomList> {
#override
Widget build(BuildContext context) {
Future<Object> getData()
{
//get Data from server
}
return FutureBuilder<Object>(
future: getData(),
builder: (context, snapshot) {
if(snapshot.data != null)
{
if(snapshot.hasData)
{
//get Documents
}
return ListView.builder(
//
itemBuilder: (context , index) {
//return a widget that uses the data received from the snapshot
},
);
}
}
);
}
}
The issue is that every time I change the page using the bottom navigation bar, whenever I come back to my default page with the CustomList(), the FutureBuilder is fired again resulting in my list having duplicates. This is due to the CustomList() being initialized again.
How do I structure my code so that the FutureBuilder is executed only once and isn't fired repeatedly when I use the BottomNavigationBar to change the page?
This is because you get a new future every time build is called, because you pass a function call to the FutureBuilder and not a reference that stays the same.
There are several easy options to solve this.
You can store a reference to the future and pass this reference to the FutureBuilder
You can use an AsyncMemoizer from the async package to only run the future once https://api.flutter.dev/flutter/package-async_async/AsyncMemoizer-class.html
You can use the FutureProvider from the provider package https://pub.dev/documentation/provider/latest/provider/FutureProvider-class.html

How to merge multiple streams inside a stream

I'm trying to use the tab bar and tab bar view to appear some elements of the fire base. First, I used stream builder to get the text of the tabs in the tab bar:
class HomePage extends StatelessWidget {
final FirebaseUser user;
HomePage({this.user});
#override
Widget build(BuildContext context) {
return new StreamBuilder<QuerySnapshot>(
stream: Firestore.instance.collection("places").snapshots(),
builder: (BuildContext context,AsyncSnapshot<QuerySnapshot> snapshot){
if (!snapshot.hasData){
return Center(child: CircularProgressIndicator());
}
else{
return DefaultTabController(
length: 20,
child: Scaffold(
appBar: AppBar(
title: Text("Home Page"),
bottom: TabBar( isScrollable: true,
tabs: new List.generate(snapshot.data.documents.length, (index) {
return new Tab(child: Text(snapshot.data.documents[index]['name'].toString().toUpperCase()));
}),)),
Then I want from the fire store to get a stream builder of collection named "temps" which has documents inside of it, every document id represents a document id in another collection named "users". In every document in users, i have a field named place. I already made the tabs and it works but,What I can't do is:
wanna get the document id of every document in collection temps, and get this document id and use it to access the documents which has the same id in "users" collection and check if the field place has the same value of the name in the tab bar i wanna appear it in the tab bar view!
How can i do this?
If I understood correctly, one solution would be creating a StatefulWidget, inside its State, using a local StreamController and pointing your StreamBuilder to it.
Separately, consume both Streams and add these items to your StreamController.
It would look a bit like that:
class YourClass extends StatefulWidget {
... createState() ...
}
class _YourClassState extends State<YourClass> {
StreamController<YourItem> _places;
#override
void initState() {
super.initState();
// the unified stream
_places = new StreamController();
// listening to changes of the first reference
CollectionReference places1Ref = Firestore.instance.collection("places1");
places1Ref.listen((snapshopt) {
// posting item to the unified streamController
_places.add(item);
});
// listening to changes of the second reference
CollectionReference places2Ref = Firestore.instance.collection("places2");
places2Ref.listen((snapshopt) {
// posting item to the unified streamController
_places.add(item);
});
}
#override
Widget build(BuildContext context) {
return StreamBuilder<YourItem>(
stream: _places.stream, // using here only the unified stream
builder: (context, snapshot) {
return YourWidgets();
}
);
}
}
This mockup is using YourItem as unified object, but you can use something else, including dynamic itself.

How do I use an async method to build a List Object?

I am getting an error that says that the method .length is calling on a null object _genreList.
I am using an async method to get data from a local asset sqlite database to which is a list of genre's. Which then I use ListView.builder in order to display that list on the screen. This is the code to obtain the data...
Future getGenreData() async {
Directory documentsDirectory = await getApplicationDocumentsDirectory();
String path = join(documentsDirectory.path, "asset_sample_sqlite.db");
ByteData data = await rootBundle.load(join("assets", "sample_sqlite.db"));
List<int> bytes = data.buffer.asUint8List(data.offsetInBytes, data.lengthInBytes);
await new File(path).writeAsBytes(bytes);
Database db = await openDatabase(path);
_genreList = await db.rawQuery('SELECT genre_name[] FROM tbl_genres');
print(_genreList);
await db.close();
}
How do I use this method inside the build Widget method so that I can access the _genreList when I use ListView.builder? like so..
#override
Widget build(BuildContext context) {
return Scaffold(
body: new ListView.builder(
itemCount: _genreList.length, //need to access the genreList here
itemBuilder: (BuildContext context, int index) {
return new Card(
child: new ListTile(
title: new Text("${_genreList[index]}"),
onTap: () {
Navigator.push(context,
MaterialPageRoute(
builder: (context) => BookPage(id: index),
),
);
}
),
);
}
),
);
}
The end goal here is to display a list of genres (from the tbl_genres in my sqlite database) that will be able to pass through data to the next page to show a list of books (from the tbl_books in my sqlite database) related to that genre.
The whole point of programming asynchronously is that your user interface can stay alive while you are doing time consuming work in the background. So you need (and want) to display something like a CircularProgressIndicator or even a blank page (e.g. a Container), while the application is loading.
There are at least these two ways of doing that:
Make the widget stateful and introduce a state field loading, that you initialize to true and set to false when your data (in another field) is ready. Your code would look like that:
import 'package:flutter/material.dart';
class GenresPage extends StatefulWidget {
#override
_GenresPageState createState() => _GenresPageState();
}
class _GenresPageState extends State<GenresPage> {
bool loading;
List<String> genreNames;
#override
void initState() {
super.initState();
loading = true;
getGenreData();
}
Future getGenreData() async {
final genreData = await actuallyGetThoseNames();
setState(() {
genreNames = genreData;
loading = false;
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: !loading ? new ListView.builder(
itemCount: genreNames.length,
itemBuilder: (context, index) {
return new Card(
child: new ListTile(
title: new Text("${genreNames[index]}"),
),
);
},
) : CircularProgressIndicator(), // or Container()
);
}
}
Use a FutureBuilder. Therefore you would need to refactor your getGenreData method to return the list as a Future<List<String>>.

Resources