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
}
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 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.
I have the following code snippet:
_openEditListingDialog(Listing listing,
Function(Listing) onSubmittedCallback) async {
Navigator
.of(context)
.push(
new MaterialPageRoute<Listing>(
builder: (BuildContext context) {
return new ListingDialog.edit(listing);
},
fullscreenDialog: true,
),
)
.then((Listing newEntry) {
if (newEntry != null) {
newEntry.id = listing.id;
onSubmittedCallback(newEntry);
}
});
}
VSCode complains on the .then line with the following error:
[dart] The argument type '(Listing) -> Null' can't be assigned to the parameter type '(Object) -> FutureOr<Null>'.
What does this mean and how do I correct this? I am new to dart and flutter and error messages like this are cryptic for me. My code is based on this example: https://github.com/MSzalek-Mobile/weight_tracker/blob/v0.4.1/lib/home_page.dart.
TBH I'm not sure why VS Code is flagging that as an error for you - I see nothing substantially wrong with it and neither does my VS Code or IntelliJ. I pasted your code in and after adding a Listing class, I had no problem at all (for the record, if you paste enough code that someone can actually run things when you have a problem it makes it a lot easier for them to help you).
And in this particular case, the line number for the error might be quite helpful.
Maybe make sure your VS Code and Flutter are up to date (I'm using the Master channel/branch which has been updated to dart 2.0 so that might be part of it maybe...).
There are a couple of things you could try. First, it seems as though your push method isn't picking up the right type - I'm fairly sure the error you're seeing is at the .then(... where it expects and Object and you're telling it to be a Listing. You can change your .push to .push<Listing> to force it to return a Listing.
Next, I'm not actually familiar with what you're doing with Function(Listing). I don't think that's proper dart, or at least any more. I think the recommended way of doing that is typedef-ing a method with the right parameter type i.e. typedef void ListingCallback(Listing listing); and then using that as the argument, if you're going to use a callback (see later on in the answer for an arguably better alternative).
The other thing you're doing wrong, which many people seem to do at first in dart/flutter, is mis-using async. If you use async in a method signature, you should not be using .then for futures, but rather using await. See here and this part of effective dart. I've re-written your code to do that properly:
//outside class somewhere
typedef void ListingCallback(Listing listing);
...
_openEditListingDialog(
Listing listing, ListingCallback onSubmittedCallback) async {
Listing newEntry = await Navigator.of(context).push<Listing>(
new MaterialPageRoute<Listing>(
builder: (BuildContext context) {
return new ListingDialog.edit(listing);
},
fullscreenDialog: true,
),
);
if (newEntry != null) {
newEntry.id = listing.id;
onSubmittedCallback(newEntry);
}
}
Note that this also gets rid of that whole function that was causing you problems.
Also, you could just use futures and remove the async in the method signature, but then you're going to have to make sure to actually return a future.
And lastly, you could (should) probably be returning a Future rather than passing in a callback.
Future<Listing> _openEditListingDialog(Listing listing) async {
Listing newEntry = await Navigator.of(context).push<Listing>(
new MaterialPageRoute<Listing>(
builder: (BuildContext context) {
return new ListingDialog.edit(listing);
},
fullscreenDialog: true,
),
);
if (newEntry != null) {
newEntry.id = listing.id;
}
return newEntry;
}
You pass a function
Function(Listing) onSubmittedCallback) async {
// no return here
});
This means Dart infers return type Null
I think changing it to
FutureOr<Null> Function(Listing) onSubmittedCallback) async =>
and removing } at the end (last line of your code) should fix it.
If you want to keep the function body {...} use
FutureOr<Null> Function(Listing) onSubmittedCallback) async {
await Navigator
.of(context)
...