I need to fetch data from three different url before page rendering. So,
this is the method inside my ScopedModel includes multiple http.post methods:
Future fetchData() async {
_isLoading = true;
notifyListeners();
await fetchAvailable();
await fetchOnProgress();
await fetchCompleted();
_isLoading = false;
notifyListeners();
}
The methods inside of fetchData area just classic http.post request with raw Future type.
This is my FutureBuilder:
FutureBuilder(
future: model.fetchData(),
builder: (BuildContext context, AsyncSnapshot snapshot) {
return snapshot.connectionState != ConnectionState.done
? Center(
child: CircularProgressIndicator(),
)
: Column.......
The problem is, the 'future' function, it's executes constantly and never ends. My algorithm about, fetching the json from server, inflates the variables within body into ListView.builder descendants.
The output is as I said recursive post requests. Also, I'm getting this logs, the number of lines increments like 1 - 2 - 3 or 2 - 4 - 6 -8 etc.
uid=10085(my.package.directory) 1.ui identical 1 lines
.......Other logs here
uid=10085(my.package.directory) 1.ui identical 3 lines
And goes on like that...
Also, is there any other useful way to handle that small amount of data before page rendering?
Do not make HTTP requests in FutureBuilder
https://docs.flutter.io/flutter/widgets/FutureBuilder-class.html
The future must have been obtained earlier, e.g. during
State.initState, State.didUpdateConfig, 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.
Here is a possible right solution. This makes your data load just one time inside scaffold. First make sure you are using stateful widget.
Define your data:
Future<List<Data>> futureList;
Make a function to fetch your data from backend:
Future<void> getList() async {
final provider = Provider.of<DataProvider>(context, listen: false);
final list = provider.getData();
setState((){
futureList = list;
});
}
Load your function in init state:
#override
void initState() {
super.initState();
Future.delayed(Duration.zero, () {
getList();
});
}
Call your fetched data in future builder:
Widget _body(BuildContext context) {
return FutureBuilder(
future: futureList,
builder: (BuildContext context, AsyncSnapshot<List> snapshot) {
if (snapshot.hasData) {
if(snapshot.data.length == 0){
return Center(child: Text('List is empty'));
}else {
return ListView.builder(
padding: EdgeInsets.only(left: 5, right: 5, top: 5, bottom: 5),
itemBuilder: (BuildContext context, int index) {
return GestureDetector(
child: DataItem(
index: index,
dataClass: snapshot.data[index],
),
onTap: () {});
},
itemCount: snapshot.data.length);
}
} else {
return Center(child: CircularProgressIndicator());
}
},
);
Related
I add a event to bloc immediately after pop a CupertinoModalPopup. But yield a state and not received in bloc listener. The code is like below.There are more states and events, and it's all working, except this state is not received. I don't know why, maybe it's because I add the event after the showCupertinoModalPopup was pop. I am not sure. I didn't paste the whole codes here, I only kept the relevant code. I did set the breakpoint on yield ProfileUploadAvatarSucceed(user), and it stops here.
BlocBuilder<ProfileBloc, ProfileState>(
builder: (context, state) {
return GestureDetector(
onTap: () async {
final file = await showCupertinoModalPopup(
context: context,
builder: (popupContent) {
return CupertinoActionSheet(
actions: [
CupertinoActionSheetAction(
child: Text('Photo Library'),
onPressed: () async {
final imageFile = await _picker.getImage(
source: ImageSource.gallery,
imageQuality: 70,
maxWidth: 480);
if (imageFile != null) {
Navigator.of(context).pop(imageFile);
}
},
),
],
);
});
BlocProvider.of<ProfileBloc>(context).add(UploadAvatarEvent(file));
};
},
listener: (_, state) {
if (state is ProfileLoadingState) {
BotToast.showLoading();
} else if (state is ProfileUploadAvatarSucceed) {
BotToast.closeAllLoading();
BotToast.showText(text: 'Avatar updated');
}
}
);
ProfileBloc.dart
#override
Stream<ProfileState> mapEventToState(
ProfileEvent event,
) async* {
if (event is UploadAvatarEvent) {
yield ProfileLoadingState();
await _apiProvider.updateUser(currentUser.id, payload);
final user = await _apiProvider.getUser(currentUser.id);
yield ProfileUploadAvatarSucceed(user);
}
}
Because I had problems with Google Maps and Bitmap descriptor, I tried to preload the icons before loading the Map, like this:
Widget preloadPinsAndLoadSearchMap() {
final config = ImageConfiguration(size: Size(48, 48));
final activePin = 'assets/active-pin.png';
final neutralPin = 'assets/neutral-pin.png';
final home = 'assets/home-map.png';
return FutureBuilder(
future: Future.wait([
BitmapDescriptor.fromAssetImage(config, activePin),
BitmapDescriptor.fromAssetImage(config, neutralPin),
BitmapDescriptor.fromAssetImage(config, home),
]),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
return SearchMap(pins: snapshot.data);
} else {
return Container();
}
},
);
}
And the SearchMap and the Markers:
// Each Marker is produced like this:
Marker createMarker(dynamic seller) {
return Marker(
markerId: MarkerId(seller.id),
position: LatLng(seller.latitude, seller.longitude),
onTap: () => onMarkerTap(seller),
// The icons are coming from the FutureBuilder.
icon: isActive ? activePin : neutralPin,
);
}
class SearchMap extends StatelessWidget {
Widget build(BuildContext context) {
final paris = initialPosition ??
LatLng(parisPosition.latitude, parisPosition.longitude);
return GoogleMap(
myLocationButtonEnabled: false,
zoomControlsEnabled: false,
compassEnabled: false,
mapType: MapType.normal,
markers: markers,
initialCameraPosition: CameraPosition(target: paris, zoom: 15),
onMapCreated: onMapCreated,
onTap: (obj) => print(obj),
);
}
}
Happily it works on my devices very well, both iOS and Android. But on my reviewers devices, the icons are just not showing up. Is it a correct way to handle BitmapDescriptor? Maybe I’m wrong on the way to do this?
I have a screen with some informations about the user.
In my build widget I have this:
Column
(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>
[
Text('User Points', style: TextStyle(color: Colors.blueAccent)),
Text('${this.user.points}', style: TextStyle(color: Colors.black, fontWeight: FontWeight.w700, fontSize: 34.0))
],
),
And I have a function to update the dashboard each time user access it.
void updateDashboard() async{
SharedPreferences prefs = await SharedPreferences.getInstance();
var api_key = prefs.getString('api_key');
var userID = prefs.getInt('userID');
var res = await http.post(('http://myApp.com/getDashboardPreferences/'+userID.toString() + '/' + api_key),
headers: {
"Accept": "application/json",
"content-type": "application/json"
});
this.user = User.fromJson(json.decode(res.body));
setState(() {
if (res.statusCode == 200) {
this.user.setPoints(this.user.points);
} else {
print("Something is wrong");
}
});
}
And in initstate I call that function:
#override
void initState() {
updateDashboard();
super.initState();
}
This works, but however, I get that ${this.user.points} is null for a few seconds (I get that warning screen) and then it is loaded and everything is fine. I guess it is because my function is async... But how can I assure that this updateDashboard() is called before rendering Widget build() ?
If you don't want to use FutureBuilder then you can do something like
if(done)
Column( /* whatever */ )
else
Center(child: CircularProgressIndicator())
void updateDashboard() async{
/* whatever */
if (res.statusCode == 200) {
setState(() {
this.user.setPoints(this.user.points);
done = true;
});
}
#override
void initState() {
/* whatever */
done = false;
}
Well, async means that the data might take time to load and is handed off to another thread to load. There can be multiple non blocking ways to do this:
Provide a default value for the user object, so by the time the async method finishes, you show proxy/default values and setState() makes sure the new value gets displayed when loaded.
This method requires you to load widgets conditionally, i.e load Text widget when there's data else load a Container().
Example for the second approach:
this.user!=null?Text('${this.user.points}'):Container()
This way you don't need to show the widget till the value loads. Either of the approaches work and depending on your use-case, you can choose which you can apply.
You can put the function right before the return widget.
You can make a loading variable like bool _loading then anywhere you start loading you async function you can setState of loading to true then setState of loading to false when async function stops loading like this.
void updateDashboard() async{
setState(() {
_loading = true;
});
SharedPreferences prefs = await SharedPreferences.getInstance();
var api_key = prefs.getString('api_key');
var userID = prefs.getInt('userID');
var res = await http.post(('http://myApp.com/getDashboardPreferences/'+userID.toString() + '/' + api_key),
headers: {
"Accept": "application/json",
"content-type": "application/json"
});
this.user = User.fromJson(json.decode(res.body));
setState(() {
if (res.statusCode == 200) {
this.user.setPoints(this.user.points);
} else {
print("Something is wrong");
}
_loading = false; //Set Loading to false
});
}
Then in your widget build, check for when loading is true and show a progress indicator like this.
body: _loading
? Center(
child: CircularProgressIndicator( //Show a Circular Progress indicator
valueColor: new AlwaysStoppedAnimation<Color>(Colors.cyan),
),
)
:Column(//Show your Dashboard)
I hope it helps
I have a widget named GiftGrid that shows a placeholder Text widget when it receives no items, but the test that checks for the specific case fails, even though it works fine in the app:
testWidgets('Null list shows placeholder', (WidgetTester tester) async {
await tester.pumpWidget(MaterialApp(
localizationsDelegates: [MyLocalizationsDelegate()],
home: GiftGrid(null)));
var a = tester.allWidgets.toList();
expect(find.text("PROPER_QUADRUPLE_CHECKED_STRING"), findsOneWidget);
});
where GiftGrid is defined as follows:
return (widget.giftList?.isEmpty ?? true)
? Center(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Text(MyLocalizations.of(context).stringThatTranslatesToSameAsTest),
),
) : StaggeredGridBuilder(...);
The debugger shows the contents of a as follows (sorry for the screenshot, I couldn't find a better way to show it):
EDIT: GifGrid receives a List as the parameter and the following test passes:
testWidgets('Gift shows thumbnail if available', (WidgetTester tester) async {
var gift = GiftModel()
..images = [
"OTHER_IMG_LINK"
]
..thumbnail =
"IMG_LINK";
// Build our app and trigger a frame.
await tester.pumpWidget(MaterialApp(
home: GiftGrid([gift]),
));
expect(find.byWidgetPredicate((widget) {
return widget is TransitionToImage &&
(widget.image as AdvancedNetworkImage).url ==
"IMG_LINK";
}), findsOneWidget);
});
I have a form that uses FocusNodes to visually indicate which part of the form is active. One field extends a PopupRoute as a kind of pop up 'keyboard'. My problem is, when I press that field, the keyboard pops up but the visual effect of the focus doesn't occur.
Some debugging from the FocusNode's listeners shows it gets focus but immediately loses it. I think it is because the new PopupRoute has a new FocusScopeNode, so my FocusNode doesn't have focus any more.
How can I keep the field focused while in the other route? I've tried:
Using FocusScope.of(context).reparentIfNeeded(focusNode) in all the build methods, which didn't do anything (I don't really understand this function tbh)
Passing the current FocusScope.of(context) into my custom PopupRoute to use. This actually worked, but after it's popped, I can't focus anything anymore (I guess it gets disposed?)
Code-wise, I'm calling requestFocus on the field tap, and adding this listener in initState:
widget.focusNode.addListener(() {
print(widget.focusNode);
if (widget.focusNode.hasFocus) {
Navigator.of(context).push(
CustomKeyboardPopupRoute(
state: widget.state,
position: //position stuff,
focusScopeNode: FocusScope.of(context), //the second of my ideas which didn't quite work above
)
).then((_) {
widget.focusNode.unfocus();
});
});
You are on the right track, indeed this happens because of the FocusScopeNode.
Make your keyboard route extend TransitionRoute:
class CustomKeyboardPopupRoute extends TransitionRoute {
#override
bool get opaque => false;
#override
Duration get transitionDuration => Duration(milliseconds: 300);
#override
Iterable<OverlayEntry> createOverlayEntries() sync* {
yield OverlayEntry(
opaque: false,
maintainState: true,
builder: _buildKeyboard,
);
}
Widget _buildKeyboard(BuildContext context) {
final positionAnimation = Tween(begin: Offset(0.0, 1.0), end: Offset.zero).animate(animation);
return SlideTransition(position: positionAnimation, child: Align(
alignment: Alignment.bottomCenter,
child: ...
),);
}
}
thanks very much.
Iterable<OverlayEntry> createOverlayEntries() sync* {
yield OverlayEntry(
opaque: false,
maintainState: true,
builder: (content) {
return ModalBarrier(dismissible: true);
}
);
yield OverlayEntry(
opaque: false,
maintainState: true,
builder: _buildContent,
);
}