Flutter Firebase create a dynamic list of streams - firebase

How i can create a dynamic list of stream?
I need a function that build a stream list and return the stream.
My fuction (try to return a value like this List<Stream<'dynamic'>>:
List<Stream<dynamic>> _buildStreamList() async* {
var chat_overview_object = await query_chat_overview_id();
List<Stream<dynamic>> stream_list = [];
for (var i = 0; i < chat_overview_object.length; i++) {
stream_list.add(
FirebaseFirestore.instance
.collection('chat')
.doc('chat_overview_data')
.collection('data')
.doc('chat_overview_id_1234')
.snapshots(),
);
}
yield stream_list;
}
Why i need this?
I try to build a streambuilder with multiple stream on documents.
The streams can be different (for example one stream look at collection a document a other on collection b document b).
The example work but only with fix streams. Now i would like implement a **function which return a dynamic list of streams.
Here my code:
var _data = [];
typedef MultiStreamWidgetBuilder<T> = Widget Function(BuildContext context);
// A widget that basically re-calls its builder whenever any of the streams
// has an event.
class MultiStreamBuilder extends StatefulWidget {
const MultiStreamBuilder({
required this.streams,
required this.builder,
Key? key,
}) : super(key: key);
final List<Stream<dynamic>> streams;
final MultiStreamWidgetBuilder builder;
Widget build(BuildContext context) => builder(context);
#override
State<MultiStreamBuilder> createState() => _MultiStreamBuilderState();
}
class _MultiStreamBuilderState extends State<MultiStreamBuilder> {
final List<StreamSubscription<dynamic>> _subscriptions = [];
#override
void initState() {
super.initState();
_subscribe();
}
#override
void didUpdateWidget(MultiStreamBuilder oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.streams != widget.streams) {
// Unsubscribe from all the removed streams and subscribe to all the added ones.
// Just unsubscribe all and then resubscribe. In theory we could only
// unsubscribe from the removed streams and subscribe from the added streams
// but then we'd have to keep the set of streams we're subscribed to too.
// This should happen infrequently enough that I don't think it matters.
_unsubscribe();
_subscribe();
}
}
#override
Widget build(BuildContext context) => widget.build(context);
#override
void dispose() {
_unsubscribe();
super.dispose();
}
void _subscribe() {
for (final s in widget.streams) {
final subscription = s.listen(
(dynamic data) {
setState(() {
_data.add(data);
print('data: ' + _data.toString());
});
},
onError: (Object error, StackTrace stackTrace) {
setState(() {});
},
onDone: () {
setState(() {});
},
);
_subscriptions.add(subscription);
}
}
void _unsubscribe() {
for (final s in _subscriptions) {
s.cancel();
}
_subscriptions.clear();
}
}
class AppWidget extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MultiStreamBuilder(
streams: _buildStreamList(),
[
FirebaseFirestore.instance
.collection('chat')
.doc('chat_overview_data')
.collection('data')
.doc('chat_overview_id_1233')
.snapshots(),
FirebaseFirestore.instance
.collection('chat')
.doc('chat_overview_data')
.collection('data')
.doc('chat_overview_id_1234')
.snapshots(),
FirebaseFirestore.instance
.collection('chat')
.doc('chat_overview_data')
.collection('data')
.doc('chat_overview_id_1232')
.snapshots()
],
builder: _buildMain,
);
}
}
Widget _buildMain(BuildContext context) {
return MaterialApp(
home: Row(
children: [
Text(
'_data: ' + _data.toString(),
style: TextStyle(fontSize: 15),
)
],
));
}
Edit!
After a while i got a solution:
//multi stream widget builder
typedef MultiStreamWidgetBuilder<T> = Widget Function(BuildContext context);
// widget that basically re-calls its builder whenever any of the streams has an event.
class MultiStreamBuilder extends StatefulWidget {
const MultiStreamBuilder({
required this.streams,
required this.builder,
Key? key,
}) : super(key: key);
final List<Stream<dynamic>> streams;
final MultiStreamWidgetBuilder builder;
Widget build(BuildContext context) => builder(context);
#override
State<MultiStreamBuilder> createState() => _MultiStreamBuilderState();
}
//multi streambuilder
class _MultiStreamBuilderState extends State<MultiStreamBuilder> {
final List<StreamSubscription<dynamic>> _subscriptions = [];
#override
void initState() {
super.initState();
_subscribe();
}
#override
void didUpdateWidget(MultiStreamBuilder oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.streams != widget.streams) {
// Unsubscribe from all the removed streams and subscribe to all the added ones.
// Just unsubscribe all and then resubscribe. In theory we could only
// unsubscribe from the removed streams and subscribe from the added streams
// but then we'd have to keep the set of streams we're subscribed to too.
// This should happen infrequently enough that I don't think it matters.
_unsubscribe();
_subscribe();
}
}
#override
Widget build(BuildContext context) => widget.build(context);
#override
void dispose() {
_unsubscribe();
super.dispose();
}
void _subscribe() {
for (final s in widget.streams) {
final subscription = s.listen(
(dynamic data) {
setState(() {
//check object contain id
var object = chat_overview_object.singleWhere(
(element) => element.chat_overview_id[0].trim() == data.id);
//add to object data
object.last_message_send = data['last_message_send'];
object.last_update = data['last_update'];
object.members_id = data['members_id'];
object.new_message = data['new_message'];
object.user_display_name = data['user_display_name'];
});
//set index
index++;
//check all data load
if (index == chat_overview_object.length) {
//set data has load
data_load.value = false;
}
},
onError: (Object error, StackTrace stackTrace) {
setState(() {});
},
onDone: () {
setState(() {});
},
);
_subscriptions.add(subscription);
}
}
void _unsubscribe() {
for (final s in _subscriptions) {
s.cancel();
}
_subscriptions.clear();
}
}

You are using a wrong approach: You should not build a separate stream for each document, then decide on which stream you are using. Instead, you should build only 1 stream that returns all those documents you may need.
Then, inside your StreamBuilder, you can apply your if condition to decide which document you need to use. This way, the document will always be up-to-date.
Make sure you apply the right where clause to call the appropriate documents from the database.
EDIT 1:
So what you need is known as a collectionGroup:
db.collectionGroup('data').where('yourField', isEqualTo: 'whatever').snapshots()

Related

Await message in flutter waiting woocommerce rest api

i use woocommerce rest api with flutter to get product variations.
Woocommerce rest api is too slowly to get this variations.
I need to send a message to the user to wait for the process to finish.
How to put this message in the code?
#override
Future<List<ProductVariation>> getProductVariations(Product product,
{String lang = 'en'}) async {
try {
final List<ProductVariation> list = [];
int page = 1;
while (true) {
String endPoint =
"products/${product.id}/variations?per_page=100&page=$page";
if (kAdvanceConfig["isMultiLanguages"]) {
endPoint += "&lang=$lang";
}
var response = await wcApi.getAsync(endPoint);
if (response is Map && isNotBlank(response["message"])) {
throw Exception(response["message"]);
} else {
if (response is List && response.isEmpty) {
/// No more data.
break;
}
for (var item in response) {
if (item['visible']) {
list.add(ProductVariation.fromJson(item));
}
}
/// Fetch next page.
page++;
}
}
return list;
} catch (e) {
//This error exception is about your Rest API is not config correctly so that not return the correct JSON format, please double check the document from this link https://docs.inspireui.com/fluxstore/woocommerce-setup/
rethrow;
}
}
Any help?
Refer to this small example:
class Waitscreen extends StatefulWidget {
Waitscreen ({Key key}) : super(key: key);
#override
_WaitscreenState createState() => _WaitscreenState();
}
class _WaitscreenState extends State<Waitscreen> {
bool _isLoading = false;
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: RaisedButton(
child: Text(_isLoading ? "Loading..." : "Load"),
onPressed: () async {
setState((){_isLoading = !_isLoading;});
// TODO
await Future.delayed(Duration(seconds: 5)); // await getProductVariations...
// TODO
setState((){_isLoading = !_isLoading;});
}
),
),
);
}
}
Then you can do something like, according to your need!

Can not retrieve documents from Firebase

So I'm trying to retrieve all documents from a certain collection 'avtvity' in my Firestore through loadmap() method. I have put the function in my initState() since I want to immediately add the data to my Place which is a class i created to add places. However, the data is not being placed in my 'places' which is a List which makes me question that the loadMap() is not being called.
any idea how to fix it?
here is the code:
class PlaceTrackerApp extends StatefulWidget {
final FirebaseUser firebaseUser;
PlaceTrackerApp({this.firebaseUser});
#override
_PlaceTrackerAppState createState() => _PlaceTrackerAppState();
}
class _PlaceTrackerAppState extends State<PlaceTrackerApp> {
AppState appState = AppState();
#override
void initState() {
super.initState();
print(widget.firebaseUser);
loadMap();
}
static StreamBuilder<QuerySnapshot> loadMap() {
return StreamBuilder(
stream: Firestore.instance.collection('avtivity').snapshots(),
builder: (context, snapshot) {
if(!snapshot.hasData) return Text("Loading... Please wait,");
for (int i = 0; i < snapshot.data.documents.length; i++) {
StubData.places.add(new Place(
id: snapshot.data.documents[i]['id'],
latLng: snapshot.data.documents[i]['latlng'],
name: snapshot.data.documents[i]['title'],
description: snapshot.data.documents[i]['description'],
starRating: snapshot.data.documents[i]['rating'],
category: PlaceCategory.activities,
));
}}
);
}
Here is a screenshot of database:

List is showing null when should show the data from firestore

I want to search in firestore data but the list is not showing me data from firestore. I want to return my list with firestore data but it is not showing me and i want to search in list data according to my choice.
Please tell me where I am wrong?
class serachDeligat extends SearchDelegate<String> {
Firestore _firestore = Firestore.instance;
String ref = 'items';
Future<List<DocumentSnapshot>> getSearch() async =>
await _firestore.collection(ref).getDocuments().then((snaps) {
return snaps.documents;
});
List<Map> search = <Map>[];
Future getDocs() async {
search = await (await getSearch()).map((item) => item.data).toList();
return search;
}
#override
void initState() {
getDocs();
}
#override
List<Widget> buildActions(BuildContext context) {
// TODO: implement buildActions
return [
IconButton(
icon: Icon(Icons.clear),
onPressed: () {
print('$search');
},
),
];
}
#override
Widget buildLeading(BuildContext context) {
// TODO: implement buildLeading
return IconButton(
icon: Icon(Icons.arrow_back),
onPressed: () {
close(context, null);
},
);
}
#override
Widget buildResults(BuildContext context) {
return Container(
child: Center(),
);
}
#override
Widget buildSuggestions(BuildContext context) {
return Container();
}
}
I will share what I did in one of my projects which had the same problem.I tried many ways but all of them end up with a null.Then I change my function with the help of StreamSubscription.This will change the data even after retrieving from firestore since it maintains a connection with firestore.I loaded data inside the initstate.
StreamSubscription<QuerySnapshot> dailyTaskSubscription;
ProgressDialog progressDialog;
List<ScheduleTask> mondayTaskList = List();
#override
void initState() {
super.initState();
mondayTaskList = List();
dailyTaskSubscription?.cancel();
dailyTaskSubscription = scheduleService
.getDailyTaskList(studentId, 'monday')
.listen((QuerySnapshot snapshot) {
final List<ScheduleTask> tasks = snapshot.documents
.map((documentSnapshot) =>
ScheduleTask.fromMap(documentSnapshot.data))
.toList();
});
}
here is the answer to my question
I should use the await in my onPressed
class serachDeligat extends SearchDelegate<String>{
Firestore _firestore = Firestore.instance;
String ref = 'items';
Future<List<DocumentSnapshot>> getSearch() async =>
await _firestore.collection(ref).getDocuments().then((snaps) {
return snaps.documents;
});
List<Map> search = <Map>[];
Future getDocs() async {
search=await
(await getSearch()).map((item) => item.data).toList();
return search;}
#override
void initState() {
getDocs();
super.query;
}
List<Map> searchAi = <Map>[];
#override
List<Widget> buildActions(BuildContext context) {
// TODO: implement buildActions
return[IconButton(
icon: Icon(Icons.clear),
onPressed: ()async {
searchAi=await getDocs();
print(searchAi);
},),];}
#override
Widget buildLeading(BuildContext context) {
// TODO: implement buildLeading
return IconButton(
icon: Icon(Icons.arrow_back),
onPressed: (){
close(context, null);
},
);
}
#override
Widget buildResults(BuildContext context) {
return Container(
child: Center(),);}
#override
Widget buildSuggestions(BuildContext context) {
return Container();}}

Navigation not working when the app is already running and Firebase Dynamic Links are used

I have this Loader Widget that checks for Dynamic Links and navigates to /game route(gamePage() widget) if the app was opened using a dynamic link. if the app was manually opened it will navigate to the /home route.
My code works fine for when the app is closed and then it is opened through a dynamic link.
But when the app is already open and is on the home page, then the code does not work because the context is empty.
Loader.dart
class Loader extends StatefulWidget {
#override
_LoaderState createState() => _LoaderState();
}
class _LoaderState extends State<Loader> {
Future<void> initDynamicLinks() async {
final PendingDynamicLinkData data = await FirebaseDynamicLinks.instance.getInitialLink();
final Uri deepLink = data?.link;
print(deepLink);
if (deepLink != null) {
Navigator.pushReplacementNamed(context, '/game'); //this works because app has just started
}
else{
FirebaseDynamicLinks.instance.onLink(
onSuccess: (PendingDynamicLinkData dynamicLink) async {
final Uri deepLink = dynamicLink?.link;
if (deepLink != null) {
print(context); // prints null
Navigator.pushReplacementNamed(context, '/game'); //this does not work because the app is already running and its on /home and context is null
}
},
onError: (OnLinkErrorException e) async {
print('error Opening the link');
print(e.message);
}
);
Navigator.pushReplacementNamed(context, '/home'); // load home widget if there are no deep links
}
}
#override
void initState(){
super.initState();
initDynamicLinks();
}
#override
Widget build(BuildContext context) {
//Loading animation widget code
}
}
main.dart
void main() {
runApp(MaterialApp(
initialRoute: "/load",
routes:<String, WidgetBuilder> {
'/load': (context) => Loader(),
'/home': (context) => Home(),
'/game': (context) => gamePage(),
},
),
);
}
I am new to Flutter so any help is appreciated. Thank You.
Yes, you are right. It doesn't work without context. So every time the deep link invokes you need to provide a context.
To implement that, create a custom class for the manage deep link. Then you can initiate that deep-link by providing context.
Please see the example below.
Custome Class
class DynamicLinkService {
Future handleDynamicLinks(BuildContext context) async {
final PendingDynamicLinkData data =
await FirebaseDynamicLinks.instance.getInitialLink();
_handleDeepLink(data, context);
FirebaseDynamicLinks.instance.onLink(
onSuccess: (PendingDynamicLinkData dynamicLink) async {
// handle link that has been retrieved
_handleDeepLink(dynamicLink, context);
}, onError: (OnLinkErrorException e) async {
print('Link Failed: ${e.message}');
});
}
void _handleDeepLink(PendingDynamicLinkData data, BuildContext context) {
final Uri deepLink = data?.link;
if (deepLink != null) {
print('_handleDeepLink | deeplink: $deepLink');
var isPost = deepLink.pathSegments.contains('post');
var isInvite = deepLink.pathSegments.contains('invite');
if(isInvite){
Navigator.of(context).pushAndRemoveUntil(MaterialPageRoute(builder: (context) =>
NavigationHomeScreen()), (Route<dynamic> route) => false);
}
}
}
}
Initiate the deep-link from your Loader widget
class Loader extends StatefulWidget {
#override
_LoaderState createState() => _LoaderState();
}
class _LoaderState extends State<Loader> {
final DynamicLinkService _dynamicLinkService = DynamicLinkService();
#override
void initState(){
super.initState();
_dynamicLinkService.handleDynamicLinks(context);
}
#override
Widget build(BuildContext context) {
//Loading animation widget code
}
}

Pagination in Flutter with Firebase Realtime Database

I am trying to paginate in Flutter with firebase realtime databse.
I have tried this in Firestore and it works fine there but I want this with realtime database.
I am fetching data for the first time like this.
Widget buildListMessage() {
return Flexible(
child: StreamBuilder(
stream: _firebase.firebaseDB
.reference()
.child("chats")
.child("nsbcalculator")
.orderByChild('timestamp')
.limitToFirst(15)
.onValue,
builder: (context, AsyncSnapshot<Event> snapshot) {
if (!snapshot.hasData) {
return Center(
child: CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation<Color>(themeColor)));
} else {
if (snapshot.data.snapshot.value != null) {
listMessage = Map.from(snapshot.data.snapshot.value)
.values
.toList()
..sort(
(a, b) => a['timestamp'].compareTo(b['timestamp']));
if (lastVisible == null) {
lastVisible = listMessage.last;
listMessage.removeLast();
}
}
return ListView.builder(
...
);
}
},
),
);
}
After that to paginate I am using a listener with ScrollController
void _scrollListener() async {
if (listScrollController.position.pixels ==
listScrollController.position.maxScrollExtent) {
_fetchMore();
}
}
and finally
_fetchMore() {
_firebase.firebaseDB
.reference()
.child("chats")
.child("nsbcalculator")
.orderByChild('timestamp')
.startAt(lastVisible['timestamp'])
.limitToFirst(5)
.once()
.then((snapshot) {
List snapList = Map.from(snapshot.value).values.toList()
..sort((a, b) => a['timestamp'].compareTo(b['timestamp']));
if (snapList.isNotEmpty) {
print(snapList.length.toString());
if (!noMore) {
listMessage.removeLast();
//Problem is here.....??
setState(() {
listMessage..addAll(snapList);
});
lastVisible = snapList.last;
print(lastVisible['content']);
}
if (snapList.length < 5) {
noMore = true;
}
}
});
}
Its working fine as realtime communication but when I try to paginate in _fetchMore() setState is called but it refreshes the state of whole widget and restarts the StreamBuilder again and all data is replaced by only new query. How can I prevent this??
Calling setState will redraw your whole widget and your list view. Now, since you supplying the steam that provides the first page, after redraw it just loads it. To avoid that you could use your own stream and supply new content to it. Then your StreamBuilder will handle the update automatically.
You need to store the full list of your items as a separate variable, update it and then sink to your stream.
final _list = List<Event>();
final _listController = StreamController<List<Event>>.broadcast();
Stream<List<Event>> get listStream => _listController.stream;
#override
void initState() {
super.initState();
// Here you need to load your first page and then add to your stream
...
_list.addAll(firstPageItems);
_listController.sink.add(_list);
}
#override
void dispose() {
super.dispose();
}
Widget buildListMessage() {
return Flexible(
child: StreamBuilder(
stream: listStream
...
}
_fetchMore() {
...
// Do your fetch and then just add items to the stream
_list.addAll(snapList);
_listController.sink.add(_list);
...
}
Try this one
pagination for RealTime list
class FireStoreRepository {
final CollectionReference _chatCollectionReference =
Firestore.instance.collection('Chat');
final StreamController<List<ChatModel>> _chatController =
StreamController<List<ChatModel>>.broadcast();
List<List<ChatModel>> _allPagedResults = List<List<ChatModel>>();
static const int chatLimit = 10;
DocumentSnapshot _lastDocument;
bool _hasMoreData = true;
Stream listenToChatsRealTime() {
_requestChats();
return _chatController.stream;
}
void _requestChats() {
var pagechatQuery = _chatCollectionReference
.orderBy('timestamp', descending: true)
.limit(chatLimit);
if (_lastDocument != null) {
pagechatQuery =
pagechatQuery.startAfterDocument(_lastDocument);
}
if (!_hasMoreData) return;
var currentRequestIndex = _allPagedResults.length;
pagechatQuery.snapshots().listen(
(snapshot) {
if (snapshot.documents.isNotEmpty) {
var generalChats = snapshot.documents
.map((snapshot) => ChatModel.fromMap(snapshot.data))
.toList();
var pageExists = currentRequestIndex < _allPagedResults.length;
if (pageExists) {
_allPagedResults[currentRequestIndex] = generalChats;
} else {
_allPagedResults.add(generalChats);
}
var allChats = _allPagedResults.fold<List<ChatModel>>(
List<ChatModel>(),
(initialValue, pageItems) => initialValue..addAll(pageItems));
_chatController.add(allChats);
if (currentRequestIndex == _allPagedResults.length - 1) {
_lastDocument = snapshot.documents.last;
}
_hasMoreData = generalChats.length == chatLimit;
}
},
);
}
void requestMoreData() => _requestChats();
}
ChatListView
class ChatView extends StatefulWidget {
ChatView({Key key}) : super(key: key);
#override
_ChatViewState createState() => _ChatViewState();
}
class _ChatViewState extends State<ChatView> {
FireStoreRepository _fireStoreRepository;
final ScrollController _listScrollController = new ScrollController();
#override
void initState() {
super.initState();
_fireStoreRepository = FireStoreRepository();
_listScrollController.addListener(_scrollListener);
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Flexible(
child: StreamBuilder<List<ChatModel>>(
stream: _fireStoreRepository.listenToChatsRealTime(),
builder: (context, snapshot) {
return ListView.builder(
itemCount: snapshot.data.length,
controller: _listScrollController,
shrinkWrap: true,
reverse: true,
itemBuilder: (context, index) {
...
}
);
}
)
),
);
}
void _scrollListener() {
if (_listScrollController.offset >=
_listScrollController.position.maxScrollExtent &&
!_listScrollController.position.outOfRange) {
_fireStoreRepository.requestMoreData();
}
}
}
ChatModel Class
class ChatModel {
final String userID;
final String message;
final DateTime timeStamp;
ChatModel({this.userID, this.message, this.timeStamp});
//send
Map<String, dynamic> toMap() {
return {
'userid': userID,
'message': message,
'timestamp': timeStamp,
};
}
//fetch
static ChatModel fromMap(Map<String, dynamic> map) {
if (map == null) return null;
return ChatModel(
userID: map['userid'],
message: map['message'],
timeStamp: DateTime.fromMicrosecondsSinceEpoch(map['timestamp'] * 1000),
);
}
}

Resources