I'm trying to display a loading while doing an API Request and when finished to show the list with the response or a custom widget to show a message(EmptyListWidget). The problem is that the whenComplete() method is being executed before the async function is finished.
I also tried using then() and using FutureBuilder but I also can't make it work using Provider (allways returns null).
If someone could help, I would really appreciate it.. thanks :)
My List Widget:
class _AbsencesListState extends State<AbsencesList> {
bool _isLoading = false;
bool _isInit = true;
#override
void didChangeDependencies() {
super.didChangeDependencies();
if (_isInit) {
setState(() => _isLoading = true);
Provider.of<AbsencesTypes>(context, listen: false)
.getAbsencesTypes(widget.ctx)
.whenComplete(() {
setState(() => _isLoading = false);
});
_isInit = false;
}
}
#override
Widget build(BuildContext context) {
final absences = Provider.of<Absences>(context).items;
return Stack(
children: [
_isLoading
? const Center(child: CircularProgressIndicator())
: absences.length > 0
? Container()
: EmptyListWidget(ListType.InconsistenciesList),
ListView.builder(
itemBuilder: (_, index) {
return GestureDetector(
onTap: () {},
child: Card(
elevation: 2.0,
child: ListTile(
leading: CircleAvatar(
child: const Icon(Icons.sick),
backgroundColor: Theme.of(context).accentColor,
foregroundColor: Colors.white,
),
title: Padding(
padding: const EdgeInsets.only(top: 3),
child: Text(absences[index].absenceType.name),
),
subtitle: Text(
absences[index].firstDate
),
),
),
);
},
itemCount: absences.length,
)
],
);
}
}
The async function:
class AbsencesTypes with ChangeNotifier {
List<AbsenceType> _absencesTypesList = [];
List<AbsenceType> get items {
return [..._absencesTypesList];
}
void emptyAbsencesTypeList() {
_absencesTypesList.clear();
}
Future<void> getAbsencesTypes(BuildContext context) async {
SharedPreferences _prefs = await SharedPreferences.getInstance();
String token = _prefs.getString(TOKEN_KEY);
http.get(
API_URL,
headers: {"Authorization": token},
).then(
(http.Response response) async {
if (response.statusCode == 200) {
final apiResponse = json.decode(utf8.decode(response.bodyBytes));
final extractedData = apiResponse['content'];
final List<AbsenceType> loadedAbsencesTypes = [];
for (var absenceType in extractedData) {
loadedAbsencesTypes.add(
AbsenceType(
id: absenceType["id"],
name: absenceType["name"].toString(),
code: absenceType["code"].toString(),
totalAllowedDays: absenceType["totalAllowedDays"],
),
);
}
_absencesTypesList = loadedAbsencesTypes;
} else if (response.statusCode == 401) {
Utility.showToast(
AppLocalizations.of(context).translate("expired_session_string"));
Utility.sendUserToLogin(_prefs, context);
}
notifyListeners();
},
);
}
}
Your problem here is probably that you're calling http.get without awaiting for it's result.
The getAbsencesTypes returns the Future<void> as soon as the http.get method is executed, without waiting for the answer, and it results in your onComplete method to be triggered.
A simple fix would be to add the await keyword before the http.get, but you could do even better.
In your code, you're not fully using the ChangeNotifierProvider which could solve your problem. You should check the Consumer class which will be pretty useful for you here, but since it's not your initial question I won't go more in depth on this subject.
Related
I'm trying to use provider with an async function where I'm changing a value of variable and as soon as the value changes, I want all listeners to be notified.
I'm sending a post request and waiting for response in the below async function. I'm waiting for the response and depending on that I want to show message on the Stateful Widget.
The provider seems to change value of the variable but doesn't change state on Text on the screen.
userloginprovider.dart
bool isLoading = false;
HttpService http = HttpService();
class UserLoginProvider with ChangeNotifier {
String loginMessage = '';
late UserAuthorizationResponse userRegistrationResponse;
Future loginUser(userData) async {
Response response;
print(loginMessage);
try {
isLoading = true;
response = await http.loginUser('api/v1/login/', userData);
isLoading = false;
if (response.statusCode == 200) {
var newReponse = response.data;
userRegistrationResponse =
UserAuthorizationResponse.fromJson(newReponse['data']);
loginMessage = newReponse['message'];
} else {
print('status code is not 200.');
}
} on Exception catch (e) {
isLoading = false;
loginMessage = e.toString().substring(11);
}
notifyListeners();
}
}
userloginscreen.dart
class _LoginPageState extends State<LoginPage> {
final UserLoginProvider userLoginProvider = UserLoginProvider();
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: ChangeNotifierProvider(
create: (context) => UserLoginProvider(),
child: Consumer<UserLoginProvider>(
builder: (context, provider, child) {
return Container(
padding: const EdgeInsets.all(8.0),
width: double.infinity,
height: double.infinity,
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(provider.loginMessage.toString()), //<-- I want to change value here.
AuthorizationButtons(
fieldName: 'Username',
textEditingController: usernameController,
),
AuthorizationButtons(
fieldName: 'Password',
textEditingController: passwordController,
),
OutlinedButton(
onPressed: () {
userData = {
'username': usernameController.text,
'password': passwordController.text,
};
userLoginProvider.loginUser(userData);
},
child: const Text('Submit'),
)
],
),
);
},
),
),
);
}
}
A new provider is created in every rebuild
body: ChangeNotifierProvider(
create: (context) => UserLoginProvider(),
Use the one in the state
body: ChangeNotifierProvider(
create: (context) => userLoginProvider,
you are notifying the listeners when it fails which is in catch block:-
on Exception catch (e) {
isLoading = false;
loginMessage = e.toString().substring(11); //here
notifyListeners();
}
}
but if the code runs without the error(exception). you are not notifying it on your code. so,if you want to notify, try something like this
try {
isLoading = true;
response = await http.loginUser('api/v1/login/', userData);
isLoading = false;
if (response.statusCode == 200) {
var newReponse = response.data;
userRegistrationResponse =
UserAuthorizationResponse.fromJson(newReponse['data']);
loginMessage = 'something'; //here
} else {
print('status code is not 200.');
}
notifyListeners();//notify the listeners here
I want to update the screen whenever I call the API. Right now I have the following
Future<String> getData() async {
var response = await http.get(
Uri.parse('https://www.api_endpoint.com'),
headers: {
'Accept':'application/json'
}
);
Timer.periodic(Duration(microseconds: 1000), (_) {
this.setState(() {
data = json.decode(response.body);
print(data); //I can see this in the console/logcat
});
});
}
#override
void initState() {
this.getData();
}
from the line above print(data); I can see the latest api responses in console/logcat but the screen doesn't update with the new values. I can't get my head around why the latest responses aren't shown on screen when this.setState() is called every second with the Timer... all feedback is welcome. Thanks
Future executes once and returns just one result. initState() executed when creating a widget, this is also usually once. For your tasks it is better to use Streams, my solution is not the best in terms of architecture, but as an example it works.
//We create a stream that will constantly read api data
Stream<String> remoteApi = (() async* {
const url = "http://jsonplaceholder.typicode.com/todos/1";
//Infinite loop is not good, but I have a simple example
while (true) {
try {
var response = await Dio().get(url);
if (response.statusCode == 200) {
//remote api data does not change, so i will add a timestamp
yield response.data.toString() +
DateTime.now().millisecondsSinceEpoch.toString();
}
//Pause of 1 second after each request
await Future.delayed(const Duration(seconds: 1));
} catch (e) {
print(e);
}
}
})();
//On the screen we are waiting for data and display it on the screen
// A new piece of data will refresh the screen
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: StreamBuilder<String>(
stream: remoteApi,
builder: (
BuildContext context,
AsyncSnapshot<String> snapshot,
) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
} else if (snapshot.connectionState == ConnectionState.active ||
snapshot.connectionState == ConnectionState.done) {
if (snapshot.hasError) {
return const Text('Error');
} else if (snapshot.hasData) {
return Center(
child: Padding(
padding: const EdgeInsets.all(15.0),
child: Text(
snapshot.data.toString(),
textAlign: TextAlign.center,
),
),
);
} else {
return const Center(child: Text('Empty data'));
}
} else {
return Center(child: Text('State: ${snapshot.connectionState}'));
}
},
),
);
}
Or simplest solution
Future<String> remoteApi() async {
try {
const url = "http://jsonplaceholder.typicode.com/todos/1";
var response = await Dio().get(url);
if (response.statusCode == 200) {
return response.data.toString() +
DateTime.now().millisecondsSinceEpoch.toString();
} else {
throw ("Error happens");
}
} catch (e) {
throw ("Error happens");
}
}
var displayValue = "Empty data";
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Padding(
padding: const EdgeInsets.all(15.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Center(child: Text(displayValue)),
Center(
child: ElevatedButton.icon(
onPressed: () async {
displayValue = await remoteApi();
setState(() {});
},
label: const Text('Get API'),
icon: const Icon(Icons.download),
),
)
],
),
));
}
Ah, you don't actually call your API every timer tick, you just decode the same body from the first call.
If you want to call your API periodically, you need to move the actual http.get call inside the timer method.
Got it using the answer found here... moved the Timer that called this.setState() to the initState method
#override
void initState() {
this.getData();
_everySecond = Timer.periodic(Duration(seconds: 5), (Timer t) {
setState(() {
getData();
});
});
}
Once I searched for how to update the state, change state, etc. found the solution quickly...
I am showing i simple drop down but my options are not opening mean its not showing a dropdown.
I have a simple list like this
[352094083791878, 358480083322091, 358480081409924]
This is my code
class _SettingPageState extends State<SettingPage> {
bool isSwitched = false;
bool _shoW = true;
var items = [];
#override
void initState() {
super.initState();
getImi();
}
getImi() async {
final storage = new FlutterSecureStorage();
String userNumber = await storage.read(key: "userNumber");
String userPassword = await storage.read(key: "userPassword");
print('showimi');
print(userNumber);
print(userPassword);
var map = new Map<String, dynamic>();
showDialog(
context: context,
barrierDismissible: false,
builder: (BuildContext context) {
return Center(
child: SpinKitWave(
color: Color(0xff00abb5), type: SpinKitWaveType.center));
});
var url =
'http://api.igiinsurance.com.pk:8888/drive_api/login.php?number=${userNumber}&password=${userPassword}';
print(url);
http.Response res = await http.get(
url,
headers: <String, String>{'token': 'c66026133e80d4960f0a5b7d418a4d08'},
);
var data = json.decode(res.body.toString());
print(data);
if (data['status'].toString() == "Success") {
Navigator.pop(context);
_shoW = true;
data['data'].forEach((row) {
print(row['imei_number']);
items.add(row['imei_number']);
print(items);
});
} else {
Navigator.pop(context);
_shoW = false;
}
}
#override
Widget build(BuildContext context) {
double width = MediaQuery.of(context).size.width;
double height = MediaQuery.of(context).size.height;
return Scaffold(
body: DecoratedBox(
decoration: BoxDecoration(
image: DecorationImage(
image: AssetImage("images/sidebg.png"), fit: BoxFit.cover),
),
child: Column(
children: [
_shoW
? DropdownButton(
hint: Text('Select Vechile'),
items: items.map((val) {
return DropdownMenuItem<String>(
value: val,
child: new Text(val),
);
}).toList(),
onChanged: null)
: Container()
],
),
),
);
}
}
I am simply adding values in the Items array. I need to show the array in the select down list. But it's not opening the options i have try to put static list but that's also not working .
You need to set onChanged to not null value. onChanged without listener cannot allow you to open list.
If you need to showing current selected value, pass value parameter to DropdownButton. Also you can find more examples in Official Flutter Documentation.
Change Your Code Like THis :
if (data['status'].toString() == "Success") {
Navigator.pop(context);
setState(){
_shoW = true;
}
data['data'].forEach((row) {
print(row['imei_number']);
items.add(row['imei_number']);
print(items);
});
} else {
Navigator.pop(context);
setState(){
_shoW = false;
}
}
I was trying to fetch data from my backend which is developed using Laravel framework. I need to use POST request for each request to pass the API middleware.
When I use GET request without headers, future builder works just fine and got updated immediately after the data on the backend has changed but I can't get Laravel to grab the current authenticated user because no token provided.
But when I use POST request or GET request with headers, it stops updating and I need to switch to another page and go back in order to get the changes.
Please take a look at my script below:
Future<List<Document>> fetchDocuments(http.Client http, String token) async {
final headers = {'Authorization': 'Bearer $token'};
final response = await http.post(
'http://192.168.1.2:8000/api/documents/all',
headers: headers,
);
// Use the compute function to run parseDocuments in a separate isolate.
return compute(parseDocuments, response.body);
}
// A function that converts a response body into a List<DOcument>
List<Document> parseDocuments(String responseBody) {
final parsed = json.decode(responseBody).cast<Map<String, dynamic>>();
return parsed.map<Document>((json) => Document.fromJSON(json)).toList();
}
class Document {
final int id;
final String name;
Document({this.id, this.name});
factory Document.fromJSON(Map<String, dynamic> json) {
return Document(
id: json['id'] as int,
name: json['name'] as String,
);
}
}
class StudentDocumentScreen extends StatefulWidget {
StudentDocumentScreen({Key key}) : super(key: key);
_StudentDocumentScreenState createState() => _StudentDocumentScreenState();
}
class _StudentDocumentScreenState extends State<StudentDocumentScreen> {
final storage = FlutterSecureStorage();
final _uploadURL = 'http://192.168.1.2:8000/api/documents';
final _scaffoldKey = GlobalKey<ScaffoldState>();
final SnackBar uploadingSnackbar = SnackBar(
content: Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
'Uploading file, this may take a while...',
style: TextStyle(color: Colors.white),
),
),
);
final SnackBar successSnackbar = SnackBar(
content: Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
'File uploaded!',
style: TextStyle(color: Colors.white),
),
),
backgroundColor: Colors.green,
);
final SnackBar errorSnackbar = SnackBar(
content: Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
'Oops... Something went wrong!',
style: TextStyle(color: Colors.white),
),
),
backgroundColor: Colors.red,
);
String _token;
String _path;
#override
void initState() {
super.initState();
_getToken();
}
void _getToken() async {
String token = await storage.read(key: 'accessToken');
setState(() => _token = token);
}
void _openFileExplorer() async {
try {
_path = null;
_path = await FilePicker.getFilePath();
} on PlatformException catch (e) {
print('Unsupported operation');
print(e);
}
if (!mounted) return;
if (_path != null || _path.isNotEmpty) {
_uploadDocument(_path);
}
}
void _uploadDocument(String path) async {
_scaffoldKey.currentState.showSnackBar(uploadingSnackbar);
try {
var multipartFile = await http.MultipartFile.fromPath('document', path);
var request = http.MultipartRequest('POST', Uri.parse(_uploadURL));
request.headers.addAll({'Authorization': 'Bearer $_token'});
request.files.add(multipartFile);
http.StreamedResponse response = await request.send();
if (response.statusCode == 200) {
_scaffoldKey.currentState.showSnackBar(successSnackbar);
} else {
_scaffoldKey.currentState.showSnackBar(errorSnackbar);
}
} catch (e) {
print('Error when uploading files');
print(e);
}
}
#override
Widget build(BuildContext context) {
return Scaffold(
key: _scaffoldKey,
appBar: AppBar(
title: Text('Koleksi Dokumen'),
),
body: _buildBody(),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.add),
onPressed: _openFileExplorer,
),
);
}
Widget _buildBody() {
return FutureBuilder<List<Document>>(
future: fetchDocuments(http.Client(), _token),
builder: (context, snapshot) {
if (snapshot.hasError) print(snapshot.error);
return snapshot.hasData
? DocumentList(documents: snapshot.data)
: Center(child: CircularProgressIndicator());
},
);
}
}
class DocumentList extends StatelessWidget {
final List<Document> documents;
const DocumentList({Key key, this.documents}) : super(key: key);
#override
Widget build(BuildContext context) {
return GridView.builder(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
),
itemCount: documents.length,
itemBuilder: (context, index) {
return Card(
child: Padding(
padding: const EdgeInsets.all(20.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Padding(
padding: const EdgeInsets.only(bottom: 20.0),
child: Icon(
Icons.insert_drive_file,
color: Colors.black12,
size: 50.0,
),
),
Text(
documents[index].name,
textAlign: TextAlign.center,
),
],
),
),
);
},
);
}
}
And below is the laravel script:
# in the `api.php`
Route::post('documents/all', 'DocumentController#index');
# in the `DocumentController`
public function index()
{
$userId = auth()->user()->id;
return Document::whereUserId($userId)
->get()
->load('user');
}
Any idea? Thanks in advance.
I just found a temporary solution, putting setState(() {}); after uploading progress do the job.
Please feel free to answer if you guys has a better one.
Solution:
void _uploadDocument(String path) async {
_scaffoldKey.currentState.showSnackBar(uploadingSnackbar);
try {
var multipartFile = await http.MultipartFile.fromPath('document', path);
var request = http.MultipartRequest('POST', Uri.parse(_uploadURL));
request.headers.addAll({'Authorization': 'Bearer $_token'});
request.files.add(multipartFile);
http.StreamedResponse response = await request.send();
if (response.statusCode == 200) {
_scaffoldKey.currentState.showSnackBar(successSnackbar);
setState(() {}); // reload state
} else {
_scaffoldKey.currentState.showSnackBar(errorSnackbar);
}
} catch (e) {
print('Error when uploading files');
print(e);
}
}
I have a futureBuilder widget displaying records from a database, and I'm getting those records from an async future network request. There are a few choiceChip widgets that onSelected assign a new future to the FutureBuilders future.
The first time a choiceChip is clicked the new future is called and created but the displayed records don't update. If you click the same choiceChip a second time, the records update and are displayed correctly.
Why doesn't the futureBuilder update on the first click of the choiceChip even though the new future is being called and assigned correctly?
UPDATE: I switched from a future to a stream and added a check to see if the ConnectionState was done. That seems to have done the trick.
class _HousingFeedState extends State<HousingFeed> with AutomaticKeepAliveClientMixin<HousingFeed> {
Future<List<Housing>> _future;
#override
void initState() {
super.initState();
_future = housingFeed(0, "");
}
Widget _chipRow = new Container(
child: SingleChildScrollView(
child: Row(
children: List<Widget>.generate(
5,
(int index) {
return Padding(
padding: EdgeInsets.only(left: 5.0),
child: ChoiceChip(
selected: _housingType == index,
onSelected: (bool selected) {
setState(() {
_housingType = index;
_future = housingFeed(subcategoryMap[index], '');
});
},
),
);
},
).toList(),
),
),
);
Widget housingBuilder = FutureBuilder<List<Housing>>(
future: _future,
builder: (context, snapshot) {
...
}
);
}
Future<List<Housing>> housingFeed(int subcategoryId, String search) async {
String url = "https://...";
final response = await http.post(url,
headers: {HttpHeaders.contentTypeHeader: 'application/json'},
body: jsonEncode({
"id": userId,
"filter": {"id": subcategoryId},
"search": search
}));
if (response.statusCode == 200) {
var housingListings = List<Housing>();
var jsonData = json.decode(response.body);
jsonData['response'].forEach((listing) {
housingListings.add(Housing.fromJson(listing));
});
return housingListings;
} else {
throw Exception('Failed to load');
}
}