I am using the ListView.builder constructor to build a list by retrieving data from Firestore.
I have two collections under which I have data in separate documents.
Collection Messages:
Messages
message1
message2
Every document in message has the following keys: msgBody, sender.
I store the sender id in the sender field in the message documents, which is actually the document name of the respective user in collection Users.
Collection Users:
Users
james
greg
I am currently using the follwing code to read the messages collection and get the documents in it, then I get the value in sender field and create a document reference to the respective sender document and try to get the sender details using the Users document and use the data from the it in my list.
child: StreamBuilder<QuerySnapshot>(
stream: Firestore.instance.collection('Messages').snapshots(),
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (!snapshot.hasData) return Center(child: CircularProgressIndicator());
return new ListView.builder(
itemCount: snapshot.data.documents.length,
padding: const EdgeInsets.all(6.0),
itemBuilder: (context, index) {
DocumentSnapshot ds = snapshot.data.documents[index];
DocumentReference tRef = Firestore.instance.document('/users/'+ds['sender']);
DocumentSnapshot tRefD;
tRef.get().then((tData){
print(tData.data); // I can see the sender data in the console here
tRefD = tData;
});
if (ds.data.isNotEmpty)
{
return Column(
children: <Widget>[
Text(ds['sender']),
Text(tRefD['name']), //App crashes here works when I comment this line
],
);
}
}
)
How do I read data from different documents under different collections and use it in the same ListView.builder?
Any help will be really appreciated :)
By the time your user name is fetched, the UI had already been built. You either need to have a nested FutureBuilder or StreamBuilder one for each document. Or make your async calls in a separate method, call this method in your initState and populate the respective fields and then load them into the UI inside your build method.
Related
There is a StreamBuilder (using RxDart) which displays some date. After click on InkWell widget I need to calculate a new date on basis of old one. The code below simply explains the algo but when I run it there is nothing happens and execution stops after underlined row, i.e. I never see the value of lastCalcDate.
GUI:
child: StreamBuilder(
stream: bloc.getDate,
builder: (context,snapshot) {
return InkWell(
onTap: () => tapHandler
);
}),
void tapHandler() async {
var lastCalcDate = await bloc.getDate.single;
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
print(lastCalcDate);
var newCalcDate = lastCalcDate.add(Duration(days:1));
bloc.setDate(newCalcDate)
}
BLoC:
class Bloc {
// BehaviourSubject is usedto be sure that last sent date will be accessible in `tapHandler`.
final _dateSubject = BehaviourSubject<DateTime>();
Observable<DateTime> get getDate => _dateSubject.stream;
Function(DateTime) get setDate => _dateSubject.add;
}
To implement what I need I created some workaround but I don't like it because I fill that I can do the same thing using observables.
BLoC (workaround):
class Bloc {
final _dateSubject = BehaviourSubject<DateTime>();
Observable<DateTime> get getDate => _dateSubject.stream;
DateTime _date;
void setDateWorkaround(DateTime date) {
_date = date;
_dateSubject.add(date);
}
}
Could you someone to give me advise. What I did wrong?
single will not work because it is going to return the next item in the stream, however, that has to be added first. This means that single will just wait for the next item and in your case it will not happen.
Since you are using rxdart and BehaviorSubject already, you can easily access the current element like this:
class Bloc {
final _dateSubject = BehaviourSubject<DateTime>();
Observable<DateTime> get getDate => _dateSubject.stream;
Function(DateTime) get setDate => _dateSubject.add;
DateTime get currentDate => _dateSubject.value;
}
In this case, I am making use of BehaviorSubject.value, which is actually the whole point of that class.
Now, you can just use currentDate in your tap handler:
void tapHandler() async {
var lastCalcDate = bloc.currentDate;
print(lastCalcDate);
var newCalcDate = lastCalcDate.add(Duration(days:1));
bloc.setDate(newCalcDate)
}
Use StreamProvider from provider
Listen to a Stream and expose the latest value emitted.
I tried to create a "Future" function that returns the "Navigator.push" class instead of "Widget".
I tried the normal method but it didn't work, the current script is like this:
...
return new FutureBuilder<Map<String, dynamic>>(
future: fetchUserQR(new http.Client(),snapshot.data), //scan qr code
builder: (context1, snapshot1) {
if(snapshot1.hasData) {
return Navigator.push( //this the problem
...
my goal is, when I finish scanning the QR code a new page will appear.
hopefully my explanation can be understood.
thank you, best regards.
You need to return a Widget in the futurebuilder's builder method. So return a Container and after this frame push a new page.
return new FutureBuilder<Map<String, dynamic>>(
future: fetchUserQR(new http.Client(),snapshot.data), //scan qr code
builder: (context1, snapshot1) {
if(snapshot1.hasData) {
SchedulerBinding.instance.addPostFrameCallback((_) {
// Navigator.push....
});
return Container();
}
//...
I'm experiencing an annoying thing, where the first item in my listview keeps re-render, when scrolling up. Even if I'm at the top.
only way i noticed this, was because I have a widget, that on load, fetches an url, and get the meta title, description and image, and displaying it in a nice card.
My listviews are fairly simple:
ListView.builder(
physics: AlwaysScrollableScrollPhysics(),
controller: _scrollController,
itemCount: model.posts.posts.length,
itemBuilder: (context, index) {
// Items goes here
});
How do I stop it from happening?
The widget that keeps re-rendering, is a stateless widget that imports a ScopedModel model, that fetches some data from the internet, and scraped for meta data, and then updated the model.
#override
Widget build(BuildContext context) {
UrlEmbedModel _model = new UrlEmbedModel(); // <-- the ScopedModel
_model.fetchHtml(url); // <-- the url param comes from the constuctor
// Rest of the widget
}
Here is the code that fetches content from the net.
void fetchHtml(url) {
http.get(url).then((response) {
if (response.statusCode == 200) {
// If server returns an OK response, parse the JSON
var document = parse(response.body);
var list = document.getElementsByTagName('meta');
for (var item in list) {
if (item.attributes['property'] == "og:title") {
_title = item.attributes['content'];
}
if (item.attributes['property'] == "og:description") {
_description = item.attributes['content'];
}
if (item.attributes['property'] == "og:image") {
_imageUrl = item.attributes['content'];
}
notifyListeners();
}
} else {
// If that response was not OK, throw an error.
throw Exception('Failed to load post');
}
});
}
The code you wrote, seems OK, but what about the function that makes the request? Can you show it?
If it's a Future function, it'll only make a request once and then finish it, it's not like a stream function that will be always listening to an event.
EDIT
First of all, if this functions makes a request, then, the type of the functions must be Future, void type if don't return anything, after that, add the async call. You could change the .then method to an await method, it'll suit you better.
Future<void> fetchHtml(url) async {
final Response response = await get(url);
final dynamic documents = json.decode(response.body); //import 'dart:convert';
print(documents); // print to see what you get than, pass it to the variables you want the data
if (response.statusCode == 200) {
//in here
}
}
I can see a feel things in the fetch request, I'd be glad if you answer it:
Why you're not deserializing the json you receiving?
var documents = json.decode(response.body)
You could print the documents variable after deserializing it and atribute it to the widgets you want
The way you're doing it it's not wrong, but could improve it.
Found the culprit.
The issue wasn't the listview, it was the RefreshIndicator that I used.
As soon I removed it, the issue went away.
This seems to be a bug with Widget.
I have encountered an issue while experimenting with flutter.
I have an AppBar with some Actions.
One of these actions is a calendar widget. My desired behavior will be by the new date selection the data on my Scaffold to be changed accordingly.
The issue is that, although I have managed to accomplished this behavior, the call to my API performed twice. I have identify that the issue was the RefreshIndicator that I had put in place (in order for the user to pull to refresh the page on demand), but I do not understand why...
For some reason when I change the date and consequently the data changed, it identify this as refresh state and then executes _handleRefresh(). The problem is, I still want to have the pull-down-to-refresh behavior.
Files on (tabView.dart file)
Scaffold's widget tree
RefreshIndicator(
key: _modelRefreshKey,
child: ListView.builder(
itemCount: this._fetchedData?.length,
itemBuilder: (BuildContext context, int index) {
if (this._fetchedData!= null) {
final MyModel myModel = this._fetchedData[index];
return (index == 0)
? ResultsLayout(
model: myModel ,
lastUpdateTxt: myModel.someTXT,
)
: MyModelInheritedWidget(
model: myModel,
child: ModelCardLayout(),
);
} else {
return EmptyWidget();
}
},
),
onRefresh: _handleRefresh,
),
Handle on refresh function
Future<Null> _handleRefresh() async {
Completer<Null> completer = new Completer<Null>();
this.getData().then((_) {
completer.complete();
});
return completer.future;
}
On select new date this function executes which refresh call again the data (hometab.dart file)
if (picked != null && picked != _selectedDate) {
_selectedDate = picked;
modelRefreshKey.currentState.widget.selectedDate = picked;
modelRefreshKey.currentState?.getData();
}
It is worth to point out the date method is located at where I create the tabs and the actual data to refresh is a part of a tab. I mention this in case it is some how related to my issue.
Any insights will be really helpful.
Thanks in advance.
I would have a variable like the following to see if the app is waiting for an API response:
_isWaitingForResponse = false;
Future getData() {
if(_isWaitingForResponse) return;
_isWaitingForResponse = true;
//change _isWaitingForResponse on api's response
}
I have used Fluro package inside my Flutter app, and I implemented all of my routes with a handler like the example of this package.
Could you please let me know what is the correct way to pass an object or list of objects between routes?
You could do that via ModalRoute.of(context).settings.arguments
so if you have object:
class ScreenArguments {
final String title;
ScreenArguments(this.title);
}
navigate with:
Navigator.of(context).pushNamed("/screen", arguments: ScreenArguments("Title"));
and in handler:
static Handler _screenHandler = Handler(handlerFunc: (BuildContext context, Map<String, dynamic> params) {
final ScreenArguments args = ModalRoute.of(context).settings.arguments;
return PassArgumentsScreen(args?.title);
});
The solution presented at How to pass JSON response to another view works unless your object has a string (or the decoded JSON contains) a / as #Chirag said.
I'm giving my solution as it worked, since my object has an URL inside.
Given the router:
_router.define("/groups/:group", handler: Handler(handlerFunc: (context, params) {
String param = params["group"][0];
GroupAPIModel group = GroupAPIModel.fromJson(decodeJsonDataForFluro(param));
return GroupDetailsScreen(group);
}));
Defining:
String encodeJsonDataForFluro(Map<String, dynamic> mapToEncode) {
return jsonEncode(mapToEncode).replaceAll("/", HtmlEscape().convert("/"));
}
Map<String, dynamic> decodeJsonDataForFluro(String encodedMap) {
return jsonDecode(encodedMap.replaceAll(HtmlEscape().convert("/"), "/"));
}
This method would reach that route:
void _onGroupClicked(GroupAPIModel group) {
String bodyJson = encodeJsonDataForFluro(group.toJson());
router.navigateTo(context: context, path: "/groups/$bodyJson");
}
Just struggled with this and found a solution. You can do this:
Object object = <Your Object>
Navigator.of(context).pushNamed("/page/" + object.id.toString(),
arguments: object);
And then access the custom object in Fluro like this:
static void setupRouter() {
router.define("page/:pageId", handler: _pageHandler);
}
static Handler _pageHandler =
Handler(handlerFunc: (BuildContext context, Map<String, dynamic> params) {
Object object = ModalRoute.of(context).settings.arguments;
return Widget(object);
});
While Fluro doesn't have this, this is a solution that isn't completely terrible.
I used to pass the data as an encoded JSON and String format like all other solutions in this question. Recently, the Fluro plugin provides a way of pass arguments as a class object between the routes like a Flutter navigator.
After pushing a route with a custom RouteSettings you can use the BuildContext.settings extension to extract the settings. Typically this would be done in Handler.handlerFunc so you can pass RouteSettings.arguments to your screen widgets.
/// Push a route with custom RouteSettings if you don't want to use path params
FluroRouter.appRouter.navigateTo(
context,
'home',
routeSettings: RouteSettings(
arguments: MyArgumentsDataClass('foo!'),
),
);
/// Extract the arguments using [BuildContext.settings.arguments] or [BuildContext.arguments] for short
var homeHandler = Handler(
handlerFunc: (context, params) {
final args = context.settings.arguments as MyArgumentsDataClass;
return HomeComponent(args);
},
);
In the case of using the Flutter navigator instead of the Fluro plugin use this link or check the following approach.
The Navigator provides the ability to navigate to a named route from any part of an app using a common identifier. In some cases, you might also need to pass arguments to a named route. For example, you might wish to navigate to the /user route and pass information about the user to that route.
To pass different pieces of data, create a class that stores this information.
// You can pass any object to the arguments parameter.
// In this example, create a class that contains a customizable
// title and message.
class ScreenArguments {
final String title;
final String message;
ScreenArguments(this.title, this.message);
}
Now you need to define a handler using ModalRoute:
static Handler _screenHandler = Handler(
handlerFunc: (BuildContext context, Map<String, dynamic> params) {
final ScreenArguments args = ModalRoute
.of(context)
.settings
.arguments;
return PassArgumentsScreen(args?.title);
});
And navigate with:
Navigator.pushNamed(
context,
"/screen",
arguments: ScreenArguments(
'Class Arguments Screen',
'This message is extracted in the build method.',
),
);