How can I access json data as variable inside my scaffold in flutter? - flutter

How can I access var apiData = jsonDecode(response.body);
inside Widget build(BuildContext context) => Scaffold()
I want to use something like
if (apiData["studentEmail"] == "") { return const SignIn(); } else { return const Dashboard(); }

First of all, I suggest that you do the service operations in a separate class. I suggest you look at the service-repository pattern for this.
Bring your data from the api by creating the getApi method inside the service class.
For example,
class MyApi{
final String _getApi =
"https://free.currconv.com/api/v7/convert?q=USD_TRY,EUR_TRY&compact=ultra&apiKey=26cb9ffd85f9bee9c208";
Future<StudentModel?> getDatas() async {
var response = await Dio().get(_getApi);
if (response.statusCode == 200) {
return StudentModel.fromJson(response.data);
} else {
debugPrint('${response.statusCode} : ${response.data.toString()}');
throw UnimplementedError();
}
}
}
After that, Using FutureBuilder, give the future property the method that brings the api to your api class. And now you can access your data with the builder's AsynSnapshot. You can easily access the data in the future method with the snapshot that FutureBuilder now gives you.
FutureBuilder<StudentModel>(
future: MyApi.getDatas,
builder: (context, AsynSnapshot asynSnapshot){
// You can easily access the data in the future method with the
// snapshot that FutureBuilder now gives you.
asynSnapshot.data.yourData;
}
)

Related

getting the return value of Future Function in Flutter without Stateless/Stateful Widget

I just want to return the value of each condition and use it as the value of the function.
Here is my code for it.
var getStuff = chechIfExisting();
chechIfExisting() async {
var isExisting = await FirebaseFirestore.instance
.collection('merchants')
.doc(userUid)
.get();
if (isExisting.exists) {
return 'exists';
}
if (!isExisting.exists) {
return 'nope';
} else {
return 'error';
}
}
and I am not using any Stateless and Stateful Widget since this file contains only the widgets such as appbar/mydrawer. I wanted to use the 'getStuff' variable in a if statement under the myDrawer Widget, since I want to dynamically check if the the data that I am fetching is existing or not.
myDrawer(BuildContext context) {
print('getStuff');
// only prints 'Instance of 'Future<String>' and does not return the value.
}
I want to be able to use the 'getStuff' variable as
myDrawer(BuildContext context) {
if(getStuff == 'exists'){
// code here
}
Any helps/ideas on how to solve this are appreciated!
with this line:
var getStuff = chechIfExisting();
You're not waiting for the method to finishes executing, since it's a Future, not using either await/async or then to resolve values after the Future finishes will get you that getStuff() is a type of Instance of 'Future<String>, before the running of the myDrawer function, you need either to:
var getStuff = await chechIfExisting(); // inside an async method
or:
chechIfExisting().then((resolvedValue) {
getStuff = resolvedValue;
});
then you can run the myDrawer method and get it working fine

Flutter The return type 'Map<int, int>' isn't a 'void', as required by the closure's context

Hi i want to return a map from a FirebaseDatabase. But i get the Error Code:
The return type 'Map<int, int>' isn't a 'void', as required by the closure's context.
if i print the map i get the right result. Im new in Flutter and i dont get it why its doesnt work. I guess i need to change the method type, but how?
String u1= 'Backsquat';
Dataread(String u1);
DatabaseReference data = FirebaseDatabase.instance.reference();
FirebaseAuth auth = FirebaseAuth.instance;
Map? read() {
Map <int,int> werte;
data.child("Kraftwerte").child(auth.currentUser.uid).child(u1).onValue.listen((event) {
werte = event.snapshot.value;
print(werte);
return werte;
}); ```
The error comes from the fact that your stream is asynchronous, while your actual function is synchronous.
If all you want is to return event.snapshot.value for every item on your stream, you can do this:
Stream<Map?> read() {
return data.child("Kraftwerte").child(auth.currentUser.uid).child(u1).onValue.map<Map>((event) => event.snapshot.value);
}
If what you want is to get the first value of the stream:
Future<Map?> read() async {
final event = await data.child("krafwerte").child(auth.currentUser.uid).child(u1).onValue.first;
return event.snapshot.value as Map?;
}
Either way, your code must be async
example of using the code:
class MyWidget extends StatelessWidget {
#override
Widget buuld(BuildContext context) {
return FutureBuilder(
future: _read(),
builder: (context, snapshot) {
if (snapshot.hasError) return Text(snapshot.error!);
if (snapshot.hasData) return Text(snapshot.data!['key']); // snapshot.data is the map, it's null if the future is not done.
return CircularProgressIndicator();
}
);
}
Future<Map> _read() {
final event = await data.child("krafwerte").child(auth.currentUser.uid).child(u1).onValue.first;
return event.snapshot.value as Map?;
}
}
To better understand how the future builder widget works, please read the future builder docs

Is there any easy way to use a Future (which performs an http connection) inside a stateful widget without having it reconnect on every screen build?

Every time the screen is rebuilt the getJSONfromTheSite seems to get invoked. Is seems because the future is placed inside the Widget build that every time I rebuild the screen it's just calling the apiResponse.getJSONfromTheSite('sitelist') future. But When I try to simply move the apiResponse.getJSONfromTheSite('sitelist') call outside the Widget and into the initState it doesn't work at all.
I'm not fully grasping the interplay of Futures in relation to a stateful widget, but in this case I need to keep the widget stateful because Im using a pull to refresh function to rebuild my state
class _SitelistScreenState extends State<SitelistScreen> {
RemoteDataSource _apiResponse = RemoteDataSource();
#override
void initState() {
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
child: FutureBuilder(
future: _apiResponse.getJSONfromTheSite('sitelist'),
builder: (BuildContext context, AsyncSnapshot<Result> snapshot) {
if (snapshot.data is SuccessState) {
AppData sitelistCollection = (snapshot.data as SuccessState).value;
}
},
),
);
}
}
// (Do some UI stuff)
class RemoteDataSource {
//Creating Singleton
RemoteDataSource._privateConstructor();
static final RemoteDataSource _apiResponse =
RemoteDataSource._privateConstructor();
factory RemoteDataSource() => _apiResponse;
MyClient client = MyClient(Client());
void init() {}
Future<Result> getJSONfromTheSite(String call, {counter = 0}) async {
debugPrint('Network Attempt by getJSONfromTheSite');
try {
final response = await client
.request(requestType: RequestType.GET, path: call)
.timeout(const Duration(seconds: 8));
if (response.statusCode == 200) {
return Result<AppData>.success(AppData.fromRawJson(response.body));
} else {
return Result.error(
title: "Error", msg: "Status code not 200", errorcode: 1);
}
} catch (error) {
if (counter < 3) {
counter += 1;
await Future.delayed(Duration(milliseconds: 1000));
return getJSONfromTheSite(call, counter: counter);
} else {
return Result.error(
title: "No connection", msg: "Status code not 200", errorcode: 0);
}
}
}
void dispose() {}
}
A FutureBuilder, as the name suggests, wants to build you something using a FUTURE value that you provide. For that to happen, you should perform an operation outside the build method (for example, in the State class or in the initState function) and store its Future value (like a promise in javascript), to be used later on the FutureBuilder.
You have access to this value inside the FutureBuilder on the snapshot.data variable, as I can see you already know by looking at your code. The way I coded the following solution, you should no longer have issues about multiple requests to the website each time it builds the widget UI (getJSONfromTheSite will only be called once and the result from this call will be available to you inside the FutureBuilder!)
The solution:
class _SitelistScreenState extends State<SitelistScreen> {
RemoteDataSource _apiResponse = RemoteDataSource(); // I left this here because I'm not sure if you use this value anywhere else (if you don't, simply delete this line)
// when creating the widget's state, perform the call to the site once and store the Future in a variable
Future<Result> _apiResponseState = RemoteDataSource().getJSONfromTheSite('sitelist');
#override
void initState() {
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
child: FutureBuilder<SuccessState>(
future: _apiResponseState,
builder: (BuildContext context, AsyncSnapshot<Result> snapshot) {
if (snapshot.data is SuccessState) {
AppData sitelistCollection = (snapshot.data as SuccessState).value;
}
},
),
);
}
}
EDIT: Edited answer to use Result as the inner type of the Future (instead of SuccessState).
The FutureBuilder's behavior can be expected as following according to the documentation
The future must have been obtained earlier, e.g. during State.initState, State.didUpdateWidget, 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.
As stated above, if the future is created at the same time as the FutureBuilder, the FutureBuilder will rebuilt every time there's change from the parent. To avoid this change, as well as making the call from initState, one easy way is to use another Widget call StreamBuilder.
An example from your code:
class RemoteDataSource {
final controller = StreamController<AppData>();
void _apiResponse.getJSONfromTheSite('sitelist') {
// ... other lines
if (response.statusCode == 200) {
// Add the parsed data to the Stream
controller.add(AppData.fromRawJson(response.body));
}
// ... other lines
}
In your SiteListScreen:
class _SitelistScreenState extends State<SitelistScreen> {
RemoteDataSource _apiResponse = RemoteDataSource();
#override
void initState() {
super.initState();
_apiResponse.getJSONfromTheSite('sitelist');
}
#override
Widget build(BuildContext context) {
return Scaffold(
child: StreamBuilder<AppData>(
stream: _apiResponse.controller.stream, // Listen to the Stream using StreamBuilder
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (snapshot.hasData) {
AppData sitelistCollection = snapshot.data;
}
},
),
);
}
This StreamBuilder is a popular concept through out most of Flutter's apps nowadays (and is the basis of many Flutter's architecture), so it's a good idea to take a good look and use the best of it.
There is a simple way you do not need to change too much coding. Like
class RemoteDataSource {
Result _result;
//Creating Singleton
RemoteDataSource._privateConstructor();
static final RemoteDataSource _apiResponse =
RemoteDataSource._privateConstructor();
factory RemoteDataSource() => _apiResponse;
MyClient client = MyClient(Client());
void init() {}
Future<Result> getJSONfromTheSite(String call, {counter = 0}) async {
debugPrint('Network Attempt by getJSONfromTheSite');
if (_result != null) {
return _result;
}
try {
final response = await client
.request(requestType: RequestType.GET, path: call)
.timeout(const Duration(seconds: 8));
if (response.statusCode == 200) {
_result = Result<AppData>.success(AppData.fromRawJson(response.body));
return _result;
} else {
return Result.error(
title: "Error", msg: "Status code not 200", errorcode: 1);
}
} catch (error) {
if (counter < 3) {
counter += 1;
await Future.delayed(Duration(milliseconds: 1000));
return getJSONfromTheSite(call, counter: counter);
} else {
return Result.error(
title: "No connection", msg: "Status code not 200", errorcode: 0);
}
}
}
void dispose() {}
}
I only store the success result to _result, I do not sure that you want store the error result. When you rebuild the widget, it will check if it already get the success result. If true, return the stored result, it not, call api.

Dart make synchronous api call

I'm learning flutter and I want to call an api which returns json. Because HttpClient methods are async and I don't want to deal with Future to build my material app, I've used the sync.http library but when I test the following code :
var result = Map();
try {
final apiUri = Uri.parse('https://api.quotable.io/random');
var request = SyncHttpClient.getUrl(apiUri);
var response = request.close();
var contents = response.toString();
result = jsonDecode(contents);
}
on Exception {
result['content'] = 'Could not contact host';
result['author'] = 'None';
}
return result;
I get the error :
Unhandled exception:
Unsupported operation: unsupported http response format
I think this means that the json format isn't supported by the library but I find this weird. Do you know how I can call my api call synchronous ?
I'm not sure about the SyncHttp client from the docs it looks like an internal client used by flutter or the flutter team. I could be wrong but either way its not a good choice for a UI to have sync http requests.
Flutter provides a FutureBuilder Widget which will allow you to use async methods in the build method.
#override
Widget build(BuildContext context) {
return MaterialApp(
home: FutureBuilder(
future: fetchData(), // Your API Call here
builder: (context, snapshot) {
if (snapshot.hasData) {
return Text(''); // Your UI here
} else if (snapshot.hasError) {
return Text('Error');
} else {
return Text('Loading');
}
}
),
);
}
For more info and a performance tips visit
Flutter Widget of the Week - FutureBuilder
FutureBuilder Docs

Flutter error : The argument type 'List<Future<Widget>>' can't be assigned to the parameter type 'List<Widget>'

I'm trying to do a list of item from Firebase Firestore (this is done) and to get for each item a different image URL from Firebase Cloud Storage.
I use a function called getPhotoUrl to change the value of the variable photoUrl. The problem is that the return is executed before getPhotoUrl. If I add await in front of the function getPhotoUrl and async after _docs.map((document), I got an error saying that The argument type 'List<Future>' can't be assigned to the parameter type 'List'.
My code:
class PhotosList extends StatefulWidget {
#override
_PhotosListState createState() => _PhotosListState();
}
class _PhotosListState extends State<PhotosList> {
String photoUrl = 'lib/assets/default-image.png';
List<DocumentSnapshot> _docs;
getPhotoUrl(documentID) {
Reference ref = storage
.ref('Users')
.child(currentUser.uid)
.child('Photos')
.child(documentID)
.child('image_1.jpg');
ref.getDownloadURL().then((value) {
setState(() {
photoUrl = value.toString();
});
}).catchError((e) {
setState(() {
print(e.error);
});
});
}
#override
Widget build(BuildContext context) {
return StreamBuilder(
stream: firestore
.collection('Users')
.doc(currentUser.uid)
.collection('Photos')
.orderBy('date')
.snapshots(),
builder: (BuildContext context, AsyncSnapshot<QuerySnapshot> snapshot) {
if (!snapshot.hasData) return CircularProgressIndicator();
_docs = snapshot.data.docs;
if (_docs.isEmpty)
return Center(
child: Text("The list is empty."));
return Container(
child: ResponsiveGridList(
desiredItemWidth: 100,
squareCells: true,
minSpacing: 5,
children: _docs.map((document) {
getPhotoUrl(document.id);
return PhotosListItem(photoUrl: photoUrl);
}).toList(),
),
);
},
);
}
}
I think you mix 2 different ways. In every build cicle you map your docs and request that photoUrl, but inside that method you call setState, which re-triggers your build method. That way you should end in infinite loop of getting photo url and building your widget.
You have three options:
Load your photoUrls and store them inside your widget -> call set state -> check inside your mapping function if your photo is loaded, if yes, take it, if no, call your getPhotoUrl function
Load your photoUrls synchronously and return url from your function and set it to your PhotosListItem
(I would prefer this) Add your documentId to your photosListItem in your mapping function and inside your item you load this photo url. In this PhotoListItem you have a variable with your imageUrl and in initState you call your getPhotoUrl function
Inside your PhotoItem:
String imageUrl;
#override
void initState() {
Future.delayed(Duration.zero, () {
setState(() {
// load your data and set it to your variable
imageUrl = ..
});
});
super.initState();
}
You might use a FutureBuilder because StreamBuilder seems to be synchronous :
How to convert Future<List> to List in flutter?
Thanks for your answers guys, actually I found an other solution which is to get and write the URL in Firestore directly after uploading the image on the Storage.
This is the article which helped me a lot : https://medium.com/swlh/uploading-images-to-cloud-storage-using-flutter-130ac41741b2
(PS: some of the Firebase names changed since this article but it's still helpful.)
Regards.