Dart make synchronous api call - flutter

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

Related

How can I access json data as variable inside my scaffold in 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;
}
)

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.

How to display a Firebase list in REAL TIME using BLoC Pattern?

I have a TODO List function (Alarmas), but I feel I'm not taking advantage of Firebase's Realtime features enough.
The Widget displays the list very well, however when someone puts a new task from another cell phone, I am not being able to show it automatically, but I must call the build again by clicking on the "TODO button" in the BottomNavigationBar.
Is there a way that the new tasks are automatically displayed without doing anything?
I'm using BLOC Pattern and Provider to get Data through Streams...
#override
Widget build(BuildContext context) {
alarmaBloc.cargarAlarmas();
///---Scaffold and others
return StreamBuilder(
stream: alarmaBloc.alarmasStream,
builder: (BuildContext context, AsyncSnapshot<List<AlarmaModel>> snapshot){
if (snapshot.hasData) {
final tareasList = snapshot.data;
if (tareasList.length == 0) return _imagenInicial(context);
return ListView(
children: [
for (var itemPendiente in tareasList)
_crearItem(context, alarmaBloc, itemPendiente),
//more widgets
],
);
} else if (snapshot.hasError) {
return Text(snapshot.error.toString());
}
return Center (child: Image(image: AssetImage('Preloader.gif'), height: 200.0,));
},
),
#puf published a solution in How to display a Firebase list in REAL TIME? using setState, but I don't know how to implement it because I can't use setState inside my BLoC pattern page.
UPDATE
My BLoC Pattern looks like this...
class AlarmaBloc {
final _alarmaController = new BehaviorSubject<List<AlarmaModel>>();
final _alarmaProvider = new AlarmaProvider();
Stream <List<AlarmaModel>> get alarmasStream => _alarmaController.stream;
Future<List<AlarmaModel>> cargarAlarmas() async {
final alarmas = await _alarmaProvider.cargarAlarmas();
_alarmaController.sink.add(alarmas);
return alarmas;
}
//---
dispose() {
_alarmaController?.close();
}
And my PROVIDER looks like this...
Future<List<AlarmaModel>> cargarAlarmas() async {
final List<AlarmaModel> alarmaList = new List();
Query resp = db.child('alarmas');
resp.onChildAdded.forEach((element) {
print('Provider - Nuevo onChild Alarma ${element.snapshot.value['fecha']} - ${element.snapshot.value['nombreRefEstanque']} - ${element.snapshot.value['pesoPromedio']}}');
final temp = AlarmaModel.fromJson(Map<String,dynamic>.from(element.snapshot.value));
temp.idAlarma = element.snapshot.key;
alarmaList.add(temp); // element.snapshot.value.
});
await resp.once().then((snapshot) {
print("Las Alarmas se cargaron totalmente - ${alarmaList.length}");
});
return alarmaList;
How can I display a List from Firebase in "true" Real Time using BLoC Pattern?

snapshot.ConnectionState is always waiting in the FutureBuilder using provider package

I am using cloud firestore as a backend and the provider package for state management for my app. I have a BooksProvider class (ChangeNotifierProvider) which I use to fetch, add, update and delete data. In this class, I have an async method to fetch data from cloud firestore.
Future<void> fetchAndSetData() async {
try {
final response = await bookCollection.getDocuments();
List<Book> loadedBooks = [];
final querySnapshot = response.documents;
if (querySnapshot.isEmpty) {
return;
}
querySnapshot.forEach((book) {
loadedBooks.add(
Book(
id: book.documentID,
title: book.data['title'],
description: book.data['description'],
image: book.data['image'],
rate: book.data['rate'],
category: CategoryName.values[book.data['category_index']],
date: book.data['date'],
price: book.data['price'],
isFree: book.data['isFree'],
isFavorite: book.data['isFavorite']),
);
});
_booksList = loadedBooks;
notifyListeners();
} catch (e) {
throw e;
}
}
I am trying to use it with the future builder but it doesn't work as it is always stuck in ConnectionState.waiting case.
class BooksScreen extends StatelessWidget {
#override
Widget build(BuildContext context) {
// final booksData = Provider.of<BooksProvider>(context);
// final books = booksData.booksList;
// final _noContent = Center(child: Text('No books are available.'));
return FutureBuilder(
future: Provider.of<BooksProvider>(context).fetchAndSetData(),
builder: (ctx, dataSnapshot) {
switch (dataSnapshot.connectionState) {
case ConnectionState.waiting:
case ConnectionState.active:
return Text('waiting'); //always returning waiting
break;
case ConnectionState.done:
return Text('done');
break;
case ConnectionState.none:
return Text('none');
break;
default:
return Text('default');
}
},
);
}
}
I don't know what might be wrong, I tried to change the return type of the async method so that it returns a list of Book but it didn't work. I also used if statement instead of the switch case and the same thing kept happening.
In addition, I tried it with if (dataSnapshot.hasdata) as it is suggested by on of the answers, and it worked!, but i want to use a spinner while waiting and when i tried it, the same problem occurred again.
I tried the same method with another approach where i used stateful widget and didChangeDependencies() method to fetch the data and it worked just fine, and I am trying future builder approach because I don't know how to handle errors with didChangeDependencies() approach.
so if you please can help me with the future builder or error handling with the latter approach. Thank you in advance.
Use dataSnapshot.hasData instead of dataSnapshot.connectionState

How to manage blocs thrown exceptions with StreamBuilder?

I'm trying to return a snapshot error state to my StreamBuilder when my provider has problems during the http.get() call. In my case I throw an exception when the the http.get() return a state different from 200 (OK).
I would like to be able to return a bad state to snapshot and execute the specific code for this situation.
Now when I throw the exception the application simply crash.
Provider:
class FmsApiProvider {
Future<List<FmsListResponse>> fetchFmsList() async {
print("Starting fetch FMS..");
final Response response = await httpGet('fms');
if (response.statusCode == HttpStatus.ok) {
// If the call to the server was successful, parse the JSON
return fmsListResponseFromJson(response.body);
} else {
// If that call was not successful, throw an error.
//return Future.error(List<FmsListResponse>());
throw Exception('Failed to load FMSs');
}
}
}
Repository:
class Repository {
final fmsApiProvider = FmsApiProvider();
Future<List<FmsListResponse>> fetchAllFms() => fmsApiProvider.fetchFmsList();
}
Bloc:
class FmsBloc {
final _fmsRepository = Repository();
final _fmsFetcher = PublishSubject<List<FmsListResponse>>();
Observable<List<FmsListResponse>> get allFms => _fmsFetcher.stream;
fetchAllFms() async {
List<FmsListResponse> itemModel = await _fmsRepository.fetchAllFms();
_fmsFetcher.sink.add(itemModel);
}
dispose() {
_fmsFetcher.close();
}
}
My StreamBuilder:
StreamBuilder(
stream: bloc.allFms,
builder: (context, AsyncSnapshot<List<FmsListResponse>> snapshot) {
if (snapshot.hasData) {
return RefreshIndicator(
onRefresh: () async {
bloc.fetchAllFms();
},
color: globals.fcsBlue,
child: ScrollConfiguration(
behavior: NoOverScrollBehavior(),
child: ListView.builder(
shrinkWrap: true,
itemCount:
snapshot.data != null ? snapshot.data.length : 0,
itemBuilder: (BuildContext context, int index) {
final fms = snapshot.data[index];
//Fill a global list that contains the FMS for this instances
globals.currentFMSs.add(
FMSBasicInfo(id: fms.id, code: fms.fmsCode));
return MyCard(
title: _titleContainer(fms.fmsData),
fmsId: fms.id,
wmId: fms.fmsData.workMachinesList.first
.id, //pass the firs element only for compose the image url
imageType: globals.ImageTypeEnum.iteCellLayout,
scaleFactor: 4,
onPressed: () => _onPressed(fms),
);
}),
));
} else if (snapshot.hasError) {
return Text('Fms snapshot error!');
}
return FCSLoader();
})
When the exception is thrown I would like to obtain a snapshot error and then visualize only a text in my page.
You should wrap the api call in a try catch and then add the error to your sink.
class FmsBloc {
final _fmsRepository = Repository();
final _fmsFetcher = PublishSubject<List<FmsListResponse>>();
Observable<List<FmsListResponse>> get allFms => _fmsFetcher.stream;
fetchAllFms() async {
try {
List<FmsListResponse> itemModel = await _fmsRepository.fetchAllFms();
_fmsFetcher.sink.add(itemModel);
} catch (e) {
_fmsFetcher.sink.addError(e);
}
}
dispose() {
_fmsFetcher.close();
}
}
The answer marked as correct did not work for me. Doing some debug I saw that the problem is entering in the catch/throw: you actually never go there, even if you see the Exception in the debug console.
To me, in Debug the application doesn't crash but will have a breakpoint on the Exception and you can continue playing it with the Play button. With the Run button instead you have the same behaviour without the breakpoint (like a real user).
This is the flow of my BLoC implementation: http call -> provider -> repository -> bloc -> ui.
I was trying to handle the case of missing internet connection without checking for it and handling a general error case.
My evidence has been that a throw Exception ('Error'); in the provider does not propagate to the right of the flow. I tried also with other approaches like the try/catch, and applied them at different levels in the code.
Basically what I needed to achieve is to call fetcher.sink.addError('Error'); but from inside the provider, where the error occurs. Then checking snapshot.hasError in the UI will return true, and the error could easily handled.
This is the only (ugly) thing that worked to me: to give the sink object as input in the calls down to the provider itself, and in the onCatchError() function of the http call add the error to the sink through its function.
I hope it could be useful to someone. I know it is not the best practice actually but I just needed a quick-and-dirty solution. If anyone has a better solution/explanation, I will read the comment with pleasure.