i would love to call the Future fetchPost from a RaisedButton or in other words i don't wan't the FutureBuilder to do anything until i click the button, i tried calling fetchPost from the button but it won't work and I'm stuck.
PS: I used the example from this page https://flutter.io/cookbook/networking/fetch-data/
Your help is appreciated.
import 'dart:async';
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
Future<Post> fetchPost() async {
final response =
await http.get('https://jsonplaceholder.typicode.com/posts/1');
if (response.statusCode == 200) {
// If the call to the server was successful, parse the JSON
return Post.fromJson(json.decode(response.body));
} else {
// If that call was not successful, throw an error.
throw Exception('Failed to load post');
}
}
class Post {
final int userId;
final int id;
final String title;
final String body;
Post({this.userId, this.id, this.title, this.body});
factory Post.fromJson(Map<String, dynamic> json) {
return Post(
userId: json['userId'],
id: json['id'],
title: json['title'],
body: json['body'],
);
}
}
class FirstFragment extends StatelessWidget {
FirstFragment(this.usertype,this.username);
final String usertype;
final String username;
#override
Widget build(BuildContext context) {
final ThemeData theme = Theme.of(context);
final Size screenSize = MediaQuery.of(context).size;
return new SingleChildScrollView(
padding: new EdgeInsets.all(5.0),
child: new Padding(
padding: new EdgeInsets.symmetric(vertical: 0.0, horizontal: 0.0),
child: new Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
new Container(
child: new RaisedButton(
child: new Text('Call'),
onPressed: (){
fetchPost();
},
),
),
new Container(
child: FutureBuilder<Post>(
future: fetchPost(),
builder: (context, snapshot) {
if (snapshot.hasData) {
return Text(snapshot.data.title);
} else if (snapshot.hasError) {
return Text("${snapshot.error}");
}
// By default, show a loading spinner
return CircularProgressIndicator();
},
)
)
],
),
),
);
}
}
As Dhiraj explained above calling fetchPost alone won't change UI, so you need to reset UI by calling setState.
Below is how your code should look like
import 'dart:async';
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
Future<Post> fetchPost() async {
final response =
await http.get('https://jsonplaceholder.typicode.com/posts/1');
if (response.statusCode == 200) {
// If the call to the server was successful, parse the JSON
return Post.fromJson(json.decode(response.body));
} else {
// If that call was not successful, throw an error.
throw Exception('Failed to load post');
}
}
class Post {
final int userId;
final int id;
final String title;
final String body;
Post({this.userId, this.id, this.title, this.body});
factory Post.fromJson(Map<String, dynamic> json) {
return Post(
userId: json['userId'],
id: json['id'],
title: json['title'],
body: json['body'],
);
}
}
class FirstFragment extends StatefulWidget {
FirstFragment(this.usertype,this.username);
final String usertype;
final String username;
#override
_FirstFragmentState createState() => new _FirstFragmentState(usertype, username);
}
class _FirstFragmentState extends State<FirstFragment> {
_FirstFragmentState(this.usertype,this.username);
final String usertype;
final String username;
#override
Widget build(BuildContext context) {
final ThemeData theme = Theme.of(context);
final Size screenSize = MediaQuery.of(context).size;
return new SingleChildScrollView(
padding: new EdgeInsets.all(5.0),
child: new Padding(
padding: new EdgeInsets.symmetric(vertical: 0.0, horizontal: 0.0),
child: new Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
new Container(
child: new RaisedButton(
child: new Text('Call'),
onPressed: (){
fetchPost();
setState(() {
});
},
),
),
new Container(
child: FutureBuilder<Post>(
future: fetchPost(),
builder: (context, snapshot) {
if (snapshot.hasData) {
return Text(snapshot.data.title);
} else if (snapshot.hasError) {
return Text("${snapshot.error}");
}
// By default, show a loading spinner
return CircularProgressIndicator();
},
)
)
],
),
),
);
}
}
Calling fetchPost alone wont do changes in UI.
At first inside build your futurebuilder is execcuted which gets data from fetchPost.
Further then to fetchPost agiain you need to rebuild.
To do so inside onPressed of raised button:
onPressed: (){
setState((){})
},
And to fetch post only on button click (not for first time) you should use then()
Details here : https://www.dartlang.org/tutorials/language/futures
Related
I'm trying to change the state of isSyncing then rebuild the widget with set state once await api.fetchProducts() is completed. api.fetchProducts() is what i used to fetch from API then store local using sqflite.
I tried using cloudSyn.then() but it wont work.
class SyncProgress extends StatefulWidget {
#override
_SyncProgressState createState() => _SyncProgressState();
}
class _SyncProgressState extends State<SyncProgress> {
bool isSyncing = true;
String progressString = 'Syncing your data....';
final db = DatabaseHelper();
final bloc = ProductBloc();
#override
void initState() {
super.initState();
}
Future cloudSync() async{
await api.fetchProducts();
//Here is the challenge
setState(() {
isSyncing = false;
progressString = 'Syncing complete....';
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: isSyncing ? _indicateProgress() : _syncDone()
);
}
Widget _indicateProgress(){
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
CircularProgressIndicator(),
SizedBox(height: 50.0,),
Text(progressString, style: TextStyle(
fontSize: 16.0,
),),
],
),
);
}
_syncDone(){
print('Syncing completed');
//return Navigator.push(context, MaterialPageRoute(builder: (context) => HomePage()));
}
}
Use then to force setState function to execute only after fetchProducts() is finished:
Future cloudSync() async{
await api.fetchProducts().then(
setState(() {
isSyncing = false;
progressString = 'Syncing complete....';
});
);
}
I have this code that has the parent widget Homepage and the child widget CountryList. In CountryList, I have created a function that uses an API to get a list of countries. I felt like enabling a RefreshIndicator in the app, so I had to modify the Homepage widget and add GlobalKey to access getCountryData() function of CountryList widget. The RefreshIndicator has done its job well. But the problem now is that when I pull and use the RefreshIndicator in the app, the getCountryData() function is called, but even after showing all data in the list, the circular spinner doesn't go (shown in the screenshot).
So, could anyone please suggest me a way to make the spinner go?
The code of main.dart containing Homepage widget is given below:
import 'package:flutter/material.dart';
import 'country_list.dart';
GlobalKey<dynamic> globalKey = GlobalKey();
void main() => runApp(MaterialApp(home: Homepage()));
class Homepage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("List of countries"), actions: <Widget>[
IconButton(icon: Icon(Icons.favorite), onPressed: (){},)
],),
body: RefreshIndicator(child: CountryList(key:globalKey), onRefresh: (){globalKey.currentState.getCountryData();},),
);
}
}
And the code of country_list.dart containing CountryList widget is:
import 'package:flutter/material.dart';
import 'dart:async';
import 'package:http/http.dart' as http;
import 'dart:convert';
import 'package:flutter_svg/flutter_svg.dart';
class CountryList extends StatefulWidget {
CountryList({Key key}) : super(key: key);
#override
_CountryListState createState() => _CountryListState();
}
class _CountryListState extends State<CountryList> {
List<dynamic> _countryData;
bool _loading = false;
#override
void initState() {
// TODO: implement initState
super.initState();
this.getCountryData();
}
Future<String> getCountryData() async {
setState(() {
_loading = true;
});
var response =
await http.get(Uri.encodeFull("https://restcountries.eu/rest/v2/all"));
var decodedResponse = json.decode(response.body);
setState(() {
_countryData = decodedResponse;
_loading = false;
});
}
#override
Widget build(BuildContext context) {
return _loading?Center(child: Column(mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[CircularProgressIndicator(), Padding(padding: EdgeInsets.all(5.0),), Text("Loading data...", style: TextStyle(fontSize: 20.0),)],)):ListView.builder(
itemCount: _countryData.length,
itemBuilder: (BuildContext context, int index) {
return Card(
child: ListTile(
leading: SvgPicture.network(_countryData[index]['flag'], width: 60.0,),
title: Text(_countryData[index]['name']),
trailing: IconButton(
icon: Icon(Icons.favorite_border),
onPressed: () {},
),
),
);
},
);
}
}
You need to add return here:
Future<String> getCountryData() async {
setState(() {
_loading = true;
});
var response =
await http.get(Uri.encodeFull("https://restcountries.eu/rest/v2/all"));
var decodedResponse = json.decode(response.body);
setState(() {
_countryData = decodedResponse;
_loading = false;
});
return 'success';
}
and here:
body: RefreshIndicator(
child: CountryList(key: globalKey),
onRefresh: () {
return globalKey.currentState.getCountryData();
},
),
The onRefresh callback is called. The callback is expected to update the scrollable's contents and then complete the Future it returns. The refresh indicator disappears after the callback's Future has completed, I think you should return Future<String> from getCountryData.
I got the below geolocation.dart file, that works perfectly as stand alone widget:
import 'package:flutter/material.dart';
import 'package:location/location.dart';
import 'package:flutter/services.dart';
import 'package:simple_permissions/simple_permissions.dart';
class LocationField extends StatefulWidget {
const LocationField({
this.fieldKey,
this.onSaved,
});
final Key fieldKey;
final FormFieldSetter<String> onSaved;
#override
_LocationFieldState createState() => _LocationFieldState();
}
class _LocationFieldState extends State<LocationField> {
Location _location = new Location();
final lat = TextEditingController();
final lon = TextEditingController();
// #override
// void initState() {
// super.initState();
// lat.addListener(_addLatValue);
// lon.addListener(_addLonValue);
//}
#override
Widget build(BuildContext context) {
return Row(
textDirection: TextDirection.rtl,
children: <Widget>[
FlatButton(
child: const Icon(Icons.my_location),
onPressed: () => _getLocation(),
),
Expanded(child: Column(
textDirection: TextDirection.ltr,
children: <Widget>[
TextFormField(
controller: lat,
decoration: InputDecoration(
prefixIcon: Text("Latittude: ")
),
),
TextFormField(
controller: lon,
decoration: InputDecoration(
prefixIcon: Text("Longitude: ")
),
)
])
)
],
);
}
_getLocation() async {
Map<String, double> location;
var error = null;
try {
await SimplePermissions.requestPermission(Permission.AccessFineLocation);
location = await _location.getLocation();
} on PlatformException catch (e) {
if (e.code == 'PERMISSION_DENIED') {
error = 'Permission denied';
} else if (e.code == 'PERMISSION_DENIED_NEVER_ASK') {
error =
'Permission denied - please ask the user to enable it from the app settings';
}
location = null;
}
print("error $error");
setState(() {
lat.text = ('${location["latitude"]}');
lon.text = ('${location["longitude"]}');
});
}
}
And display the below, at which the location coordinate appear upon clicking the location icon, as below:
I can also insert it as a widget in my main app, as:
class _SignUpPageState extends State<SignUpPage> {
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(),
body: Form(
key: _formKey,
child: Column(
children: <Widget>[
LocationField(),
RaisedButton(
onPressed: signUp,
child: Text('Sign up'),
),
],
)
),
);
}
void signUp() {
// what to write here to get the geolocation points lat/lon printed?
}
My question, is: How can I get the geolocation points lat/lon printed upon clicking the signup button, how can I get the value of the 2 fields from the sub-widget?
In Flutter, passing state down the widget tree is quite easy using InheritedWidget & co., while passing data upwards actually involves some thinking.
Similar to the TextEditingControllers you're using, you could create a LocationController that holds the location data:
class LocationController {
Location _location = Location();
get location => _location;
set location(Location val) {
_location = val;
if (onChanged != null) _onChanged(val);
}
VoidCallback _onChanged;
}
This controller can then be passed to the LocationField like this:
class LocationField extends StatefulWidget {
LocationField({
this.fieldKey,
#required this.controller,
this.onSaved,
});
final Key fieldKey;
final LocationController controller;
final FormFieldSetter<String> onSaved;
#override
_LocationFieldState createState() => _LocationFieldState();
}
class _LocationFieldState extends State<LocationField> {
final lat = TextEditingController();
final lon = TextEditingController();
#override
void initState() {
super.initState();
widget.controller._onChanged = (location) => setState(() {
lat.text = ('${location["latitude"]}');
lon.text = ('${location["longitude"]}');
});
lat.addListener(_addLatValue);
lon.addListener(_addLonValue);
}
#override
Widget build(BuildContext context) { ... }
_getLocation() async {
String error;
try {
await SimplePermissions.requestPermission(Permission.AccessFineLocation);
widget.controller.location = await _location.getLocation();
} on PlatformException catch (e) {
if (e.code == 'PERMISSION_DENIED') {
error = 'Permission denied';
} else if (e.code == 'PERMISSION_DENIED_NEVER_ASK') {
error =
'Permission denied - please ask the user to enable it from the app settings';
}
location = null;
}
print("error $error");
}
}
Then, in your widget up the tree, you can access the controller to retrieve the location:
class _SignUpPageState extends State<SignUpPage> {
LocationController controller = LocationController();
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Form(
key: _formKey,
child: Column(
children: <Widget>[
LocationField(controller: controller),
RaisedButton(onPressed: signUp, child: Text('Sign up')),
],
)
),
);
}
void signUp() {
final location = controller.location;
}
}
An added benefit is that you could set the controller's location from the widget up in the tree and the LocationField will automatically rebuild to reflect that change.
import 'dart:async';
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:carousel_pro/carousel_pro.dart';
import 'package:http/http.dart' as http;
class Home extends StatelessWidget
{
#override
Widget build(BuildContext context)
{
return MyApp(post: fetchPost());
}
}
Future<Post> fetchPost() async {
final response = await http.get('https://en.wikipedia.org/w/api.php?format=json&action=query&prop=extracts&exintro&explaintext&redirects=1&titles=Zambia');
if (response.statusCode == 200) {
// If the call to the server was successful, parse the JSON
return Post.fromJson(json.decode(response.body));
} else {
// If that call was not successful, throw an error.
throw Exception('Failed to load post');
}
}
class Post {
final int pageid;
final int ns;
final String title;
final String extract;
Post({this.pageid, this.ns, this.title, this.extract});
factory Post.fromJson(Map<String, dynamic> json) {
return Post(
pageid: json['pageid'],
ns: json['ns'],
title: json['title'],
extract: json['extract'],
);
}
}
class ImageCarousel extends StatelessWidget
{
final carousel = Carousel(
showIndicator: false,
boxFit: BoxFit.cover,
images: [
AssetImage('assets/images/animals.jpg'),
AssetImage('assets/images/bigfalls.jpg'),
AssetImage('assets/images/resort.jpg'),
AssetImage('assets/images/soca.jpg'),
AssetImage('assets/images/towncity.jpg')
],
animationCurve: Curves.fastOutSlowIn,
animationDuration: Duration(microseconds: 20000),
);
#override
Widget build(BuildContext context)
{
double screenHeight = MediaQuery.of(context).size.height / 3;
return ListView(
children: <Widget>[
Container(
height: screenHeight,
color: Colors.red,
child: carousel,
),
const Text('About Zambia', style: TextStyle(fontWeight: FontWeight.bold)),
],
);
}
}
class MyApp extends StatelessWidget {
final Future<Post> post;
MyApp({Key key, this.post}) : super(key: key);
#override
Widget build(BuildContext context) {
return new Container(
child: FutureBuilder<Post>(
future: post,
builder: (context, snapshot) {
if (snapshot.hasData) {
return Text(snapshot.data.title);
} else if (snapshot.hasError) {
return Text("${snapshot.error}");
}
// By default, show a loading spinner
return CircularProgressIndicator();
},
),
);
}
}
I'm using the example in flutter's documentation on how to fetch data from the internet (https://flutter.io/docs/cookbook/networking/fetch-data), and in place of https://jsonplaceholder.typicode.com/posts/1 I'm using ( https://en.wikipedia.org/w/api.php?format=json&action=query&prop=extracts&exintro&explaintext&redirects=1&titles=Zambia ) but for some reason, I can't get the information to be displayed on my App, forgive the ignorance but I'm new to programming and flutter...
You are parsing the json in the wrong way: the json from that url has a different structure, the keys you are trying to fetch are nested inside "query":{"pages":{"34415" while you are searching for them at the top level.
E.g. in this case, this :
pageid: json['pageid']
should be:
pageid: json['query']['pages']['34415']['pageid']
But it works only in this specific case. Instead, you should first fetch all the pages you get by that query from json['query']['pages'] then loop over the keys (the ids of every page got) and fetch the pages.
Suppose I have a SingleChildScrollView, its content is read from a file:
singleChildScrollView(
padding: EdgeInsets.all(8.0),
child: nw Text(
getTextFromFile(), //<---read from file
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 19.0,
),
));
Future<String> getFileData(String path) async {
return await rootBundle.loadString(path);
}
Future<String> getTextFromFile() async {
return getFileData("test.txt");
}
I got the following error:
The argument type 'Future<String>' can't be assigned to the parameter
type 'String'.
How to solve the issue?
Using a FutureBuilder should solve your problem. I modified you code so you can see how to use it. initialData is not required.
#override
Widget build(BuildContext context) {
return new FutureBuilder(
future: getTextFromFile(),
initialData: "Loading text..",
builder: (BuildContext context, AsyncSnapshot<String> text) {
return new SingleChildScrollView(
padding: new EdgeInsets.all(8.0),
child: new Text(
text.data,
style: new TextStyle(
fontWeight: FontWeight.bold,
fontSize: 19.0,
),
));
});
}
Future<String> getFileData(String path) async {
return await new Future(() => "test text");
}
Future<String> getTextFromFile() async {
return getFileData("test.txt");
}
}
StatefulWidget can be used for this purpose.
Declare a member variable String _textFromFile = ""; in your State class and update its value on future resolve by using setState() method.
I called your getTextFromFile() method from the constructor, but you may call it from anywhere.
Running code:
import 'package:flutter/material.dart';
import 'dart:async';
class StatefullWidgetDemo extends StatefulWidget {
#override
_StatefulWidgetDemoState createState() {
return new _StatefulWidgetDemoState();
}
}
class _StatefulWidgetDemoState extends State<StatefullWidgetDemo> {
String _textFromFile = "";
_StatefulWidgetDemoState() {
getTextFromFile().then((val) => setState(() {
_textFromFile = val;
}));
}
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text('Stateful Demo'),
),
body: new SingleChildScrollView(
padding: new EdgeInsets.all(8.0),
child: new Text(
_textFromFile,
style: new TextStyle(
fontWeight: FontWeight.bold,
fontSize: 19.0,
),
),
),
);
}
Future<String> getFileData(String path) async {
return "your data from file";
}
Future<String> getTextFromFile() async {
return await getFileData("test.txt");
}
}
Simple answer here=>
The class which calls the function:
#override
Widget build(BuildContext context) {
return new Scaffold(
child: FutureBuilder(
future: function(),
builder: (BuildContext context, AsyncSnapshot<String> text) {
return new Text(text.data);
});
)}
And the function:
Future<String> function() async {
return 'abc';
}
Here's a similar question:
flutter / dart error: The argument type 'Future<File>' can't be assigned to the parameter type 'File'
The solution proposed there is quite elegant and works properly. Where the IDE says it's expecting Type and not Future<Type>, put await in front of that argument
Another solution to get data on initialization would be to call getTextFromFile() in initState(), set state with new string and use that string in your widget:
String fileData = '';
Future<String> getFileData(String path) async {
return await rootBundle.loadString(path);
}
void getTextFromFile() async {
try {
String data = await getFileData("test.txt");
setState(() {
fileData = data;
});
} catch (ex) {
print(ex);
}
}
#override
void initState() {
super.initState();
getTextFromFile();
}
new singleChildScrollView(
padding: new EdgeInsets.all(8.0),
child: new Text(
fileData,
style: new TextStyle(
fontWeight: FontWeight.bold,
fontSize: 19.0,
),
));
I would like to add one more answer, as I tested other answers I got an error.
Moving the async part (plus setState) from the constructor to initState() solved that for me.
Enjoy
class TestAsyncInStateful extends StatefulWidget {
const TestAsyncInStateful({super.key});
#override
State<TestAsyncInStateful> createState() => _TestAsyncInStatefulState();
}
class _TestAsyncInStatefulState extends State<TestAsyncInStateful> {
#override
void initState() {
super.initState();
getTextFromServer().then(
(value) => setState(() {
textFromServer = value;
}),
);
}
String? textFromServer;
#override
Widget build(BuildContext context) {
return textFromServer == null? const SizedBox() :Text(textFromServer!);
}
}