How can I convert FutureBuilder code to StreamBuilder? - flutter

I am trying to get data from Firestore and pass that data to screen using stream. I have done this using FutureBuilder, this solution works as followed, but I need to use StreamBuilder Can anyone help me find the problem?
Future<List<Business>> list(FirebaseFirestore _firesore) async {
CollectionReference _col = _firesore.collection('Buisiness');
var _result = await _col.get();
var _docs = _result.docs;
return List.generate(_docs.length, (index) {
var satir = _docs[index].data();
return Business.fromMap(satir as Map<String, dynamic>);
});
}
This Code works in FutureBuilder but not StreamBuilder
StreamBuilder<List<Business>>(
stream: _firestorelist.list(_firestore), // Error Here
builder: (context, snapshot) {
if (snapshot.hasData) {
List<Business>? data = snapshot.data;
return ListView.builder(
itemCount: data!.length,
itemBuilder: (context, index) {
var result = data[index];
return ListTile(
title: Text(result.nereden),
subtitle: Text(result.nereye),
trailing: Text(result.fiyat),
);
},
);
} else {
return CircularProgressIndicator();
}
},
)```

You can write your data source method as
Stream<List<Business>> list(FirebaseFirestore _firesore) {
CollectionReference _col = _firesore.collection('Buisiness');
final _snap = _col.snapshots();
return _snap.map((event) => event.docs
.map<Business>((e) => Business.fromMap(e.data() as Map<String, dynamic>))
.toList());
}

The current method is a One-time Read method, You can get snapshots from the specific collection.
You can change the method like this and Then use it as stream in streamBuilder:
list(FirebaseFirestore _firesore) async {
CollectionReference _col = _firesore.collection('Buisiness');
var _result = await _col.snapshots();
return _result;
}

Related

Future Builder with for loop in flutter

In my application, I have two future builders:
CollectionReference stream = Firestore.instance.collection('users');
List<String> myIDs =[];
List <dynamic> mylist =[];
List<String> myNames =[];
String? userName;
Widget userValues() {
return FutureBuilder(
future: getrecords(),
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (snapshot.hasData &&
snapshot.connectionState == ConnectionState.done) {
return ListView.builder(
shrinkWrap: true,
itemCount: snapshot.data!.length,
itemBuilder: (context, index) {
return Text(snapshot.data? [index] ?? "got null");
},
);
}
else {
return CircularProgressIndicator();
}
},
);
}
..................
Future getrecords() async{
final data = await stream.get();
mylist.addAll(data);
mylist.forEach((element) {
final String firstPartString = element.toString().split('{').first;
final String id = firstPartString.split('/').last;
myIDs.add(id.trim());
});
return(myIDs);
}
....................
Widget Names() {
return FutureBuilder(
future: getNames(),
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (snapshot.hasData &&
snapshot.connectionState == ConnectionState.done) {
return ListView.builder(
shrinkWrap: true,
itemCount: snapshot.data!.length,
itemBuilder: (context, index) {
return Text(snapshot.data?[index] ?? "got null");
},
);
}
else {
return CircularProgressIndicator();
}
},
);
}
............................
Future getNames() async{
for (var id in myIDs ){
var names = stream.document(id).collection('userName').document('userName');
var document = await names.get();
userName = document['name'];
myNames.add(userName!);
}
return(myNames);
}
The first future (userValues) works fine, and I get the result just fine, but the other one with the for loop is not working properly and is not returning values until I hot reload, then a name will be added to the list, and so on with each hot reload.
What I want to achieve is to keep the loading indicator until the for loop is over, then build the screen.
UPDATE:
If I could manage to make it so that the "Names" futurebuilder awaits for the userValues to complete before starting, then my problem would be solved, but what I realized is that it's taking the initial value of the return from "userValues," which is non, and using it to build.
Future getNames() async{
await Future.delayed(const Duration(seconds: 2));
for (var id in myIDs ){
var names = stream.document(id).collection('userName').document('userName');
var document = await names.get();
userName = document['name'];
myNames.add(userName!);
}
return(myNames);
}
When I added this 2 seconds delay, it worked properly but is there any other way to make it wait for the first future to complete then start the second one?
You can use the await keyword on the future returned from getrecords() to wait for the completion of getrecords() before starting the getNames() function:
Future getNames() async{
await getrecords();
for (var id in myIDs ){
var names = stream.document(id).collection('userName').document('userName');
var document = await names.get();
userName = document['name'];
myNames.add(userName!);
}
return(myNames);
}

Flutter notiftyListeners contuniously rebuilding

This is my service function that communicate with the database
Future<News?> getNewsList(String token) async {
var url = Uri.tryParse('${baseUrl}get-news-list');
var response =
await http.post(url!, headers: {'Authorization': 'Bearer $token'});
if (response.statusCode == 200) {
var map = json.decode(response.body);
var list = News.fromJson(map);
print("map:${list.data![0]}");
return News.fromJson(map);
} else {
return News(message: null, status: false, data: null);
}
}
This my viewModel class I am using mvvm pattern
class NewsViewModel extends ChangeNotifier {
ApiOp api = ApiOp();
Future<List<NewsModel?>?> getNews() async {
String token ="token-here";
var map = await api.getNewsList(token);
List<NewsModel?>? list = map!.data;
print("object:${list![0]!.title}");
notifyListeners();
return list;
}
}
And here where I show the data on the view
Consumer<NewsViewModel?>(
builder: (context, value, child) => FutureBuilder<dynamic>(
future: value!.getNews(),
builder: (context, snapshot) {
List? list = snapshot.data;
print("list:$list");
return ListView.builder(
itemCount: 2,
itemBuilder: (context, index) {
return FirsatWidget(
title:"",
date: "",
photoUrl: "lib/assets/temp/4.jpg",
);
},
);
}),
),
But when I run the app some print function spamming in the debug like these prints
print("object:${list![0]!.title}");
print("map:${list.data![0]}");
Is this code continuously send post to the server or just rebuilding the consumer
Is this code continuously send post to the server or just rebuilding
the consumer
You are continuosly sending http reqests to your server.
You don't need to use Consumer<NewsViewModel?> and notifyListeners(); because you are using FutureBuilder which gets data from that future.
Try this code:
FutureBuilder<dynamic>(
future: Provider.of<NewsViewModel>(context,listen:false).getNews(),
builder: (context, snapshot) {
List? list = snapshot.data;
print("list:$list");
return ListView.builder(
itemCount: 2,
itemBuilder: (context, index) {
return FirsatWidget(
title:"",
date: "",
photoUrl: "lib/assets/temp/4.jpg",
);
},
);
}),
),
And comment notifyListeners()
class NewsViewModel extends ChangeNotifier {
ApiOp api = ApiOp();
Future<List<NewsModel?>?> getNews() async {
String token ="token-here";
var map = await api.getNewsList(token);
List<NewsModel?>? list = map!.data;
print("object:${list![0]!.title}");
// notifyListeners();
return list;
Let me know if you have issues and still have rebuilds.

Future builder returns null although my list is not empty

I have this future builder which loads a list of movies in my provider class. Whenever I reload my screen, the movies do not get returned. Below is the future builder
FutureBuilder(
future: movieData.getTrendingMovies(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(
child: CircularProgressIndicator(),
);
} else if (snapshot.hasData) {
return Swiper(
itemBuilder: (BuildContext context, i) {
return ChangeNotifierProvider(
create: (context) => Movie(),
child: MovieContainer(
imageUrl: movieData.movies[i].imageUrl,
id: movieData.movies[i].id,
rate: movieData.movies[i].rate,
title: movieData.movies[i].title,
),
);
},
itemCount: movieData.movies.length,
viewportFraction: 0.25,
scale: 0.4,
);
} else {
return Text(snapshot.error.toString()); // it returns null on the screen
}
}),
Also in my homescreen where I display my movies, after the build method, I create a listener(moviesData) to listen to all changes in the movies provider.
final movieData = Provider.of<Movies>(context, listen: false);
Below is also the methos which fetches the movies from a restfulAPI using http get request
Future<void> getTrendingMovies() async {
List<String> movieTitles = [];
List<String> movieImageUrls = [];
List<String> movieDescriptions = [];
List<String> movieReleaseDates = [];
List<String> movieRates = [];
List<String> movieIds = [];
const _apiKey = '******************************';
const url =
'https://api.themoviedb.org/3/trending/all/week?api_key=$_apiKey';
try {
final response = await http.get(Uri.parse(url));
if (response.statusCode >= 400) {
print(response.statusCode);
return;
}
final extractedData = json.decode(response.body);
List moviesList = extractedData['results'] as List;
List<Movie> loadedMovies = [];
for (int i = 0; i < moviesList.length; i++) {
String movieTitle = moviesList[i]['original_title'] ?? '';
String? movieImage =
'https://image.tmdb.org/t/p/w400${moviesList[i]['poster_path']}'; //results[0].poster_path
String movieDescription =
moviesList[i]['overview'] ?? ''; //results[0].overview
String movieReleaseDate = moviesList[i]['release_date'] ?? '';
String? movieRate = moviesList[i]['vote_average'].toString();
String? movieId = moviesList[i]['id'].toString();
movieTitles.add(movieTitle);
movieImageUrls.add(movieImage);
movieDescriptions.add(movieDescription);
movieReleaseDates.add(movieReleaseDate);
movieRates.add(movieRate);
movieIds.add(movieId);
loadedMovies.add(
Movie(
id: movieIds[i],
title: movieTitles[i],
imageUrl: movieImageUrls[i],
description: movieDescriptions[i],
rate: double.parse(movieRates[i]),
releaseDate: movieReleaseDates[i],
),
);
}
_movies = loadedMovies;
notifyListeners();
//print(_movies.last.title); //This prints the name of the last movie perfectly....This gets called unlimited times whenever I set the listen of the **moviesData** to true
} catch (error) {
print(error);
}
}
There's a couple of things to unpack here.
Instead of a ChangeNotifierProvider, I believe you should use a Consumer widget that listens to your Movies provided service when you call the notifyListeners call, so make it Consumer<Movie>.
You can still call it using the Provider.of above for the sake of making the async call via the FutureBuilder, but I believe because you're not returning anything out of the getTrendingMovies and is just a Future<void> and you're querying the snapshot.hasData, well there is no data coming through the snapshot. Maybe instead you should call snapshot.connectionState == ConnectionState.done as opposed to querying for whether it has data.
Make sure that the response.body is truly returning a JSON value, but I believe your issue is in one of the points above.

Firebase Realtime Database and Flutter - Snapshot has no data

I try to implement the Firebase Realtime Database in Flutter and I want to display updated values in realtime. I try to achieve this with a StreamBuilder.
StreamBuilder Code
StreamBuilder(
stream: GuestbooksDatabase().getAllGuestbooksSync().asStream(),
builder: (context, snapshot) {
if (!snapshot.hasData || !snapshot.data.length) {
return CircularProgressIndicator();
} else {
return ListView.builder(
shrinkWrap: true,
itemCount: snapshot.data.length,
itemBuilder: (context, index) {
return Text(snapshot.data[index].title);
});
}
}),
The stream function
Future<List<Guestbook>> getAllGuestbooksSync() async {
List<Guestbook> guestbooks = [];
databaseRef.onValue.listen((event) async {
var dataSnapshot = event.snapshot;
if (dataSnapshot.value != null) {
dataSnapshot.value.forEach((key, value) async {
Guestbook guestbook = await Guestbook.fromJson(value);
guestbook.setId(key);
guestbooks.add(guestbook);
});
await Future.delayed(Duration.zero);
print(guestbooks); // Result: All Instances of Guestbook
return guestbooks;
}
});
}
I only see the CircularProgressIndicator() what means that the snapshot has no data.
What's the issue there?
You can use StreamController for this.
Create a new controller -
final StreamController streamController = StreamController<List>.broadcast();
Convert Future<List> to void type for your getAllGuestbooksSync() function and return nothing.
It can and will be called in initState() -
void getAllGuestbooksSync() {
List<Guestbook> guestbooks = [];
databaseRef.onValue.listen((event) async {
var dataSnapshot = event.snapshot;
if (dataSnapshot.value != null) {
dataSnapshot.value.forEach((key, value) async {
Guestbook guestbook = await Guestbook.fromJson(value);
guestbook.setId(key);
guestbooks.add(guestbook);
});
print(guestbooks); // Result: All Instances of Guestbook
streamController.add(guestbooks); // Adding list to the stream
}
});
}
In your StreamBuilder use -
stream: streamController.stream,

How to return Future List from DataSnapshot

I want to return a Future List from Firebase Database snapshot and this is my code but I cant get it work properly:
Future<List<CocheDetailItem>> getCoches(ids) async {
List<CocheDetailItem> coches = [];
final dbRef = FirebaseDatabase.instance.reference().child('17082019');
for (var i = 0; i < ids.length; i++) {
var id = ids[i];
dbRef.child(id).once().then((DataSnapshot snapshot) {
if (snapshot.value != null) {
Map<dynamic, dynamic> jsres = snapshot.value;
CocheDetailItem coche = CocheDetailItem.fromJson(jsres);
coches.add(coche);
}
});
print('here is i ${ids[i]} ');
}
return coches;
}
The return I get is empty Area. Can anyone help me with this, please?
Note, dbRef.child(id).once(); is a async function, so you must wait it ends to get your data. Use await keyword to do it.
Future<List<CocheDetailItem>> getCoches(ids) async {
List<CocheDetailItem> coches = [];
final dbRef = FirebaseDatabase.instance.reference().child('17082019');
for (var i = 0; i < ids.length; i++) {
var id = ids[i];
var dataSnapshot = await dbRef.child(id).once();
if (dataSnapshot.value != null) {
Map<dynamic, dynamic> jsres = dataSnapshot.value;
CocheDetailItem coche = CocheDetailItem.fromJson(jsres);
coches.add(coche);
}
print('here is i ${ids[i]} ');
}
return coches;
}
well.. I don't use firebase but I send a request to my database with this (you have to use async and await)
Future<List<PlaceModel>> getPlaces(String ciudad, String tipo) async {
Uri request = Uri.http('domain.com', '/getPlaces/$ciudad/$tipo');
ResponseModel response = ResponseModel.fromJsonMap(json.decode((await http.get(request)).body));
List<PlaceModel> items = [];
if(response.res) {
if(response.value != null) {
for(var item in response.value) {
final place = PlaceModel.fromJsonMap(item);
items.add(place);
}
}
}
print("Places Loaded: ${items.length}");
return items;
}
I use my ResponseModel to convert the json answer in an object.
Then I show it with the future builder:
class PlacesListPage extends StatelessWidget{
final _selectedLocation, _selectedList;
PlacesListPage(this._selectedLocation, this._selectedList);
final _provider = PlaceProvider();
#override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.all(8.0),
child: FutureBuilder(
future: _provider.getPlaces(_selectedLocation, _selectedList), // async request to database
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (snapshot.hasData) { // check when your request is done
if(snapshot.data.length != 0) { // check if any data has been downloaded
return ListView.builder( // build a listview of any widget with snapshot data
itemCount: snapshot.data.length,
itemBuilder: (BuildContext context, int index) {
// i just return containers but you can use any custom widget, it's like a forEach and use the index var
return Container(
child: Text(snapshot.data[index]),
);
},
);
} else {
// If you don't have anything in your response shows a message
return Text('No data');
}
} else {
// shows a charge indicator while the request is made
return Center(
child: CircularProgressIndicator(),
);
}
},
),
);
}
}