Flutter - how to update screen with latest api response - flutter

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...

Related

Flutter: My notifyListeners() doesn't work, but only in the release apk

I have a page that shows a loading while making my API call, and once the call is done it shows the received data.
On debugger everything works correctly, but when I create the apk with 'flutter build apk', and download it, the loading remains indefinitely.
I also put a showDialog at the end of my Provider function that makes the API call (I put this showDialog just below notifyListeners().
I can't understand why in debug it works and in release it doesn't.
(This notifyListeners thing not working just does it for every API call I make)
This is the code of the provider function that makes the api call:
Future<void> getUserSites(context) async {
_userSites.clear();
isLoading = true;
notifyListeners();
try {
final response = await NetworkService.call(
url: '/api/structure/Sites',
method: Method.Get,
context: context) as List<dynamic>;
for (var i = 0; i < response.length; i++) {
_userSites.add(Sites.fromJson(response.elementAt(i)));
}
if (defaultSite == null) {
if (SimplePreferences.getDefaultSite() == null) {
defaultSite = _userSites.isNotEmpty ? _userSites.first : null;
if (defaultSite != null) {
SimplePreferences.setDefaultSite(defaultSite!.id);
}
} else {
defaultSite = _userSites.firstWhere(
(element) => element.id == SimplePreferences.getDefaultSite()!);
}
}
} catch (e) {
inspect(e);
if (SimplePreferences.getToken() != null) {
showDialog(
context: context,
builder: (ctx) => AlertDialog(
title: const Text('General Error'),
content: Text(e.toString()),
actions: [
TextButton(
onPressed: () {
Navigator.of(context).pop();
},
child: const Text(
'Ok',
),
)
],
),
);
}
// throw e;
}
isLoading = false;
notifyListeners();
showDialog(
context: context,
builder: (ctx) => AlertDialog(
title: const Text('getUserSites done!'),
content: Text(_userSites.toString()),
actions: [
TextButton(
onPressed: () {
Navigator.of(context).pop();
},
child: const Text(
'Ok',
),
)
],
),
);
}
this is the Home page code:
class HomePageScreen extends StatelessWidget { const HomePageScreen({super.key}); static const String routeName = '/';
#override Widget build(BuildContext context) { log('New Page: Home Page'); final provider = Provider.of<MyManager>(context);
return provider.isLoading ? const Center(
child: CircularProgressIndicator(),
)
: SingleChildScrollView(
physics: const BouncingScrollPhysics(),
child: Container(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
MainButton(
onTap: () async {
Navigator.of(context)
.pushNamed(ShowPatrolScreen.routeName);
await provider.getPatrol(context);
},
icon: Icons.home,
title: 'ShowPatrol',
),
printSito(provider.defaultSite?.description ?? 'Nessun Sito', context),
PrintRequestZ(
showCompleted: false,
),
],
),
),
);
}
Widget printSito(String name, context) { .... //pass context for Navigator and Theme } } `
this is the main page:
...
final myScreens = [
const HomePageScreen(),
...
];
#override
void initState() {
// TODO: implement initState
super.initState();
print('token: ${SimplePreferences.getToken()}');
if (SimplePreferences.getToken() == null){
Navigator.of(context).pushReplacementNamed('/Auth');
}
var provider = Provider.of<MyManager>(context, listen: false);
provider.setAll(context); //this function calls all my API calls, but for testing, I commented out all other functions and kept only the one written above
}
#override
Widget build(BuildContext context) {
var provider = Provider.of<MyManager>(context);
return Scaffold(
appBar: const MyAppBar(title: 'Ronda',canGoBack: false,),
body: myScreens[currentPage],
bottomNavigationBar: ...
),
}
Thanks in advance!
after some research i found the solution.
You have to use WidgetsBinding.instance.addPostFrameCallback
in the parent component.
So my home page now looks like this:
#override
void initState() {
// TODO: implement initState
super.initState();
print('token: ${SimplePreferences.getToken()}');
if (SimplePreferences.getToken() == null){
Navigator.of(context).pushReplacementNamed('/Auth');
}
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
var provider = Provider.of<MyManager>(context, listen: false);
provider.setAll(context); //this function calls all my API calls, but for testing, I commented out all other functions and kept only the one written above
});
}
I don't quite understand why though. If someone could explain it to me, I'd be very happy
Use Consumer to access the Provider's Variable
return Consumer<YourProviderName>(builder : (context, value, child){
return value.isLoading? const Center(
child: CircularProgressIndicator(),
):YourWidget(),
});

FutureBuilder gives an unexpected result and slowing UI down

It is a first statefull widget
bool _isPressed = false;
...
ElevatedButton(
child: const Text('Run long calculations'),
onPressed: () {
setState(() {
_isPressed = !_isPressed;
});
},
),
_isPressed ? const Result() : Container(),
...
and Result widget with its builds function returns
FutureBuilder<String>(
future: _process(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(
child: LinearProgressIndicator(),
);
} else {
if (snapshot.error != null) {
return const Center(
child: Text('An error occurred'),
);
} else {
return Text('${snapshot.data}');
}
}
},
);
Future<String> _process() async {
await argon2.hashPasswordString('dummy text', salt: Salt.newSalt()); // long calculations
return 'dummy result';
}
Why the FutureBuilder does not render LinearProgressIndicator before it render final text? Actualy, the LinearProgressIndicator is rendered for a very small amount of time before final text rendered, but there is something wrong with it, because the circular indicator should spin much longer.
_process() seems to slow down the application and that's why the progress indicator does not spin. But how can it be if the result of the computation is Future and the code awaits for it...
I think its better to change your conditions like below .
based on flutter Doc
if (snapshot.hasData) {
// data
return Text('${snapshot.data}');
} else if (snapshot.hasError) {
// error
} else {
// CircularProgressIndicator
return SizedBox(
child: CircularProgressIndicator(),
width: 60,
height: 60,
);
}
If this answer does not help you and you think have a UI freeze because of heavy task in _process() method you should do the process task in separate Isolate.
Your code is fine, if you replace the _getHash body with just a Future.delayed() the progress indicator shows fine. Hence the problem is in hashPasswordString. If you look at the implementation of this function you'll notice that in fact it is synchronous.
So the quick fix would be - create a static function like that:
static String _calculateHash(String input) {
final result = argon2.hashPasswordStringSync(input,
salt: Salt.newSalt(), iterations: 256, parallelism: 8);
return result.hexString;
}
and use it with the compute function:
Future<String> _hash() {
return compute(_calculateHash, 'input text');
// this is not needed anymore
// DArgon2Result result = await argon2.hashPasswordString('input text',
// salt: Salt.newSalt(), iterations: 256, parallelism: 8);
// return result.hexString;
}
static String _calculateHash(String input) {
final result = argon2.hashPasswordStringSync(input,
salt: Salt.newSalt(), iterations: 256, parallelism: 8);
return result.hexString;
}
The long and proper fix - create a PR for the dargon2_flutter package.
The problem is with this line:
if (snapshot.connectionState == ConnectionState.waiting)
You see, ConnectionState.waiting is used when there is no connection yet, for example when a stream has no value.
Here is what each connection state is:
Active
after an asyncronous computation started, but before it ends.
None
When there is no asyncronous computation at all (for example, the future is None on a future builder)
Done
After the asyncronous computation has ended
Waiting
Before the asynchronous computation begins
So when you check if the connection state is waiting, the value is true for a split second and then the connection state switches to active, here is what your if statement should look like:
if (snapshot.connectionState == ConnectionState.active)
MohandeRr has suggested the impmentation flutter docs has used, but i usually do it like this
if (snapshot.connectionState != ConnectionState.done) {
return const Center(
child: LinearProgressIndicator(),
);
}
if (snapshot.hasError) {
return const Center(
child: Text('An error occurred'),
);
}
return Text('${snapshot.data}');
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Home(),
);
}
}
class Home extends StatefulWidget {
#override
_HomeState createState() => _HomeState();
}
class _HomeState extends State<Home> {
bool _isPressed = false;
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("Demo"),
actions: [
TextButton(
child: const Text(
'Press',
style: TextStyle(
color: Colors.white,
),
),
onPressed: () {
setState(() {
_isPressed = !_isPressed;
});
},
)
],
),
body: _isPressed
? FutureBuilder<String>(
future: process(),
builder: (context, snapshot) {
if (snapshot.hasData) {
return Center(
child: Text(snapshot.data ?? ""),
);
} else if (snapshot.hasError) {
return const Center(
child: Text('An error occurred'),
);
} else {
return const Center(
child: LinearProgressIndicator(),
);
}
},
)
: const Center(
child: Text("Hidden"),
),
);
}
Future<String> process() async {
await Future.delayed(const Duration(seconds: 3));
return "Hello World";
}
}
There are 2 problems at play here:
You are creating a new Future (_process()) for every build loop. You need to put that in your state and reuse/clear it appropriately.
You are using the wrong ConnectionState check - snapshot.connectionState != ConnectionState.done is probably what you need

Flutter - whenComplete() not working as expected when using Providers

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.

flutter pull up to refetch data from api

I want to use Refresh indicator so that when you pull up the page you are in right now rebuilds i will share with you my code i have tried many times but really i can't find a straight way around it here is my code
class Companies {
final int id;
final String name;
final String companyLogo;
Companies({this.id, this.name, this.companyLogo});
factory Companies.fromJson(Map<String, dynamic> json) {
return Companies(
id: json['id'],
name: json['name'],
companyLogo: json['company_logo'],
);
}
}
Future<List<Companies>> fetchCompanies() async {
final response = await http.get('$webSiteUrl/company/api/fetch');
if (response.statusCode == 200) {
// If the server did return a 200 OK response,
// then parse the JSON.
return parseCompanies(response.body);
} else {
// If the server did not return a 200 OK response,
// then throw an exception.
throw Exception('Failed to load the companies');
}
}
List<Companies> parseCompanies(String responseBody) {
final parsed = json.decode(responseBody).cast<Map<String, dynamic>>();
return parsed.map<Companies>((json) => Companies.fromJson(json)).toList();
}
class CompaniesPage extends StatefulWidget{
#override
_CompaniesState createState() => _CompaniesState();
}
class _CompaniesState extends State<CompaniesPage> {
var refreshKey = GlobalKey<RefreshIndicatorState>();
Future<List<Companies>> companies;
#override
void initState() {
super.initState();
companies = fetchCompanies();
}
Future<Null> refreshCompanies() async {
refreshKey.currentState?.show(atTop: false);
setState(() {
companies = fetchCompanies();
});
return await companies;
}
Widget build(BuildContext context) {
checkVersion(context);
return Scaffold(
body: Center(
child: FutureBuilder<List<Companies>>(
future: companies,
builder: (context, snapshot) {
if (snapshot.hasData) {
List<Companies> companies = snapshot.data;
if(companies.length >= 1){
return MainLayout(
RefreshIndicator(
key: refreshKey,
onRefresh: refreshCompanies,
child: GridView.count(
crossAxisCount: 2 ,
children: List.generate(companies.length, (index) {
return GestureDetector(
onTap: () => {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => Categories(companies[index].id, companies[index].name)),
)},
child: CompaniesInterface(companies[index].id , companies[index].name , companies[index].companyLogo),
);
}),
),
),
);
}else{
return EmptyDataBase();
}
} else if (snapshot.hasError) {
return ConnectionError();
}
// By default, show a loading spinner.
return DefaultTabController(
length: 1,
child: TabBar(
indicatorColor: Colors.transparent,
tabs: <Widget>[
Tab(
child: LoadingBouncingGrid.square(
backgroundColor: Colors.cyan,
size: 40,
),
),
],
),
);
},
),
),
);
}
}
as you can see i have tested it but it isn't refreshing the page correctly what i want is how should i rebuild this page on pull up so the missing part from my code i think is refreshCompanies() function
Update :
class _CompaniesState extends State<CompaniesPage> {
StreamController<List<Companies>> companiesStreamController;
var refreshKey = GlobalKey<RefreshIndicatorState>();
Future<List<Companies>> fetchCompanies() async {
final response = await http.get('$webSiteUrl/company/api/fetch');
if (response.statusCode == 200) {
// If the server did return a 200 OK response,
// then parse the JSON.
return parseCompanies(response.body);
} else {
// If the server did not return a 200 OK response,
// then throw an exception.
throw Exception('Failed to load the companies');
}
}
loadCompanies() async {
fetchCompanies().then((result) async {
companiesStreamController.add(result);
return result;
});
}
Future<Null> refreshCompanies() async {
refreshKey.currentState.show(atTop: true);
setState(() {
loadCompanies();
});
}
#override
void initState() {
checkVersion(context);
companiesStreamController = new StreamController();
Timer.periodic(Duration(seconds: 1), (_) => loadCompanies());
super.initState();
}
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: StreamBuilder<List<Companies>>(
stream: companiesStreamController.stream,
builder: (context, snapshot) {
if (snapshot.hasData) {
List<Companies> companies = snapshot.data;
if(companies.length >= 1){
return MainLayout(
RefreshIndicator(
onRefresh: refreshCompanies,
key: refreshKey,
child: GridView.count(
crossAxisCount: 2 ,
children: List.generate(companies.length, (index) {
return GestureDetector(
onTap: () => {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => Categories(companies[index].id, companies[index].name)),
)},
child: CompaniesInterface(companies[index].id , companies[index].name , companies[index].companyLogo),
);
}),
),
),
);
}else{......rest of code
Add a StreamController:
StreamController<List<Companies>> dataController;
Initialize it in your initState:
dataController = StreamController();
Move fetchCompanies inside your widget and before returning the result add it to your stream:
var result = parseCompanies(response.body);
dataController.add(result);
Use a StreamBuilder instead of FutureBuilder:
StreamBuilder<List<Companies>>(
stream: dataController.stream,
builder: (context, snapshot) {
...
}
)

Setting video path before initializing video controller

So, I'm trying to use flutter's example to test a video, but I want to provide a file path that is saved in the persistent storage. My problem is that I can't wrap my head around on how to do that.
Here's my code: https://dartpad.dev/6930fc8c208c9bd1c00ae34303365e48
Future<String> getVideo() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
var videoid = prefs.getString('fileview');
return videoid;
}
#override
void initState() {
getVideo();
_controller = VideoPlayerController.file(File(getVideo()));
// Initialize the controller and store the Future for later use.
_initializeVideoPlayerFuture = _controller.initialize();
// Use the controller to loop the video.
_controller.setLooping(true);
super.initState();
}
}
So I can't set getVideo() to File because it's a future in initstate.
You can write another async function for initialising your controller and listen that future for building your UI.
Future initPlayer() async {
var filePath = await getVideo();
_controller = VideoPlayerController.file(File(filePath));
_initializeVideoPlayerFuture = _controller.initialize();
_controller.setLooping(true);
return _initializeVideoPlayerFuture;
}
You have to write another function to handle the playing state, because the player will be null when the build method will run for the first time.
bool get isVideoPlaying {
return _controller?.value?.isPlaying != null && _controller.value.isPlaying;
}
Finally, modify your build method like:
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Butterfly Video'),
),
body: FutureBuilder(
future: initPlayer(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
return AspectRatio(
aspectRatio: _controller.value.aspectRatio,
child: VideoPlayer(_controller),
);
} else {
return Center(child: CircularProgressIndicator());
}
},
),
floatingActionButton: FloatingActionButton(
onPressed: () {
setState(() {
if (isVideoPlaying) {
_controller?.pause();
} else {
_controller?.play();
}
});
},
child: Icon(
isVideoPlaying ? Icons.pause : Icons.play_arrow,
),
),
);
}