I'm creating an app that has a list inside a screen. What I want to do is whenever the app makes the HTTP request (getting the data), I want to show CircularProgressIndicator() on the screen. I tried to use a FutureBuilder to implement this, but the app recursively/continuously loading the data (when the ListView is set, the app load the data again and again). Here are some of my code:
FutureBuilder Widget
Widget _buildFuture(BuildContext context){
return FutureBuilder(
future: listenForBeers(),
builder: (context, snapshot) {
if(snapshot.connectionState == ConnectionState.done){
if(snapshot.hasError){
print('_buildFuture: Loading error');
return Center(
child: Text(
snapshot.error.toString(),
textAlign: TextAlign.center,
textScaleFactor: 1.3,
),
);
}
print('_buildFuture: Showing the Data');
return _buildBeers();
}
else{
print('_buildFuture: Loading the data');
return Center(
child: Column(
children: <Widget>[
SizedBox(height: 100),
CircularProgressIndicator()
],
),
);
}
}
);
}
initState() and listenForBeers() method
#override
void initState(){
super.initState();
listenForBeers();
}
Future listenForBeers() async {
final Stream<Beer> stream = await getBeers();
stream.listen((Beer beer) => setState(() => _beers.add(beer)));
}
getBeers() method
Future<Stream<Beer>> getBeers() async {
final String url = 'https://api.punkapi.com/v2/beers';
final client = new http.Client();
final streamedRest = await client.send(http.Request('get', Uri.parse(url)));
return streamedRest.stream
.transform(utf8.decoder)
.transform(json.decoder)
.expand((data) => (data as List))
.map((data) => Beer.fromJSON(data));
}
I'm not sure how to implement the right way because I'm new to Flutter as well. If you need other code feel free to ask, and any help would be appreciated. Thanks!
CReate AsyncMemoizer in State Class
AsyncMemoizer _memoizer = AsyncMemoizer();
Now Change
Future listenForBeers() async {
return this._memoizer.runOnce(() async {
final Stream<Beer> stream = await getBeers();
stream.listen((Beer beer) => setState(() => _beers.add(beer)));
)};
}
Future refreshBeers() async {
_memoizer = AsyncMemoizer();
return listenForBeers();
}
Details at https://medium.com/saugo360/flutter-my-futurebuilder-keeps-firing-6e774830bc2
Initialize stream in initstate and keep referance like this.
Stream<Beer> stream;
#override
void initState(){
super.initState();
stream = await getBeers();
stream.listen((Beer beer) => setState(() => _beers.add(beer)));
}
Related
In FutureBuilder when working with an API you can easily show loading spinner when data is not yet available with this code,
if(snapshot.connectionState == ConnectionState.waiting){
return Center(
child: CircularProgressIndicator(),
);
}
how do I do same for GetBuilder when using getx as state management library?
Here's a basic example of re-building based on the value of an isLoading bool. I'm just changing the value of a String but this should give you the idea of doing a proper API call in a GetX function and displaying an indicator. While I typically default to using GetBuilder whenever possible, showing loading indicators I generally just use Obx so I don't have to call update() twice.
class TestController extends GetxController {
bool isLoading = false;
String data = '';
Future<void> fetchData() async {
isLoading = true;
update(); // triggers the GetBuilder rebuild
await Future.delayed(
const Duration(seconds: 2),
() => data = 'Data Loaded',
);
isLoading = false;
update();
}
}
You can test this by throwing this in a Column. Just make sure the controller is initialized first at some point with Get.put(TestController());
GetBuilder<TestController>(
builder: (controller) => controller.isLoading
? CircularProgressIndicator()
: Text(controller.data)),
ElevatedButton(
onPressed: () => controller.fetchData(),
child: Text('Fetch Data'),
),
If you don't want to have to manually call the function you can also lose the isLoading bool use a FutureBuilder but then just pass a Future function from a GetX class to keep that logic out of your UI.
Update
Here's an example using live dummy data of random Kanye quotes from
https://api.kanye.rest Copy the code below into your IDE and run it and it should make sense.
Basic ApiCaller class
class ApiCaller extends GetConnect {
final url = 'https://api.kanye.rest';
Future<String> fetchData() async {
final response = await httpClient.get(url);
return response.body['quote'] as String;
}
}
Updated TestController class
class TestController extends GetxController {
String data = 'no data';
bool isLoading = false;
Future<void> updateData() async {
_updateIsLoading(true);
final apiCaller = ApiCaller();
await Future.delayed(
const Duration(seconds: 1),
() => data = 'Data Loaded',
); // just to add more visible time with loading indicator
data = await apiCaller.fetchData();
_updateIsLoading(false);
}
void _updateIsLoading(bool currentStatus) {
isLoading = currentStatus;
update();
}
}
Example with GetBuilder and FutureBuilder
class Home extends StatelessWidget {
#override
Widget build(BuildContext context) {
final controller = Get.put(TestController());
return Scaffold(
body: Center(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
FutureBuilder(
future: ApiCaller().fetchData(),
builder: (context, snapshot) {
if (snapshot.hasData) {
return Text('FutureBuilder: ${snapshot.data}');
} else {
return CircularProgressIndicator();
}
},
),
GetBuilder<TestController>(
builder: (_) => controller.isLoading
? CircularProgressIndicator()
: Text('GetBuilder: ${controller.data}'),
),
ElevatedButton(
onPressed: () => controller.updateData(),
child: Text('Update GetBuilder'),
),
],
),
),
),
);
}
}
Example with FutureBuilder with function from GetX class
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.
Problem My FutureBuilder waits when app first runs but doesn't wait when app updates.
When my app finishes loading and I change to a different ToggleButton, the FutureBuilder starts to rerun immediately instead of waiting for getData() and it fully completes before getData() is finished and then when getData() is finally finished, FutureBuilder runs again.
This problem does not happen when the app first runs. When the app first runs, the FutureBuilder waits for getData() to complete before running.
I need FutureBuilder to wait for getData() to finish when a different button is pressed just like it does when the app first starts up.
Note: I removed as much unnecessary code as I could for readability. I can add more code if it will help.
Code:
class PriceScreenState extends State<PriceScreen> {
String selectedCurrency = 'USD';
String selectedGraphType = "1D";
var isSelectedGraph = <bool>[true, false, false, false, false, false];
getData() async {
isWaiting = true;
try {
Map graphData = await GraphData().getGraphData(
selectedCurrency: selectedCurrency,
selectedGraphType: selectedGraphType);
isWaiting = false;
setState(() {
graphValues = graphData;
});
} catch (e) {
print(e);
}
}
#override
void initState() {
super.initState();
futureData = getData();
}
#override
Widget build(BuildContext context) {
...(other code)...
ToggleButtons( ****************TOGGLEBUTTONS***********
children: <Widget>[
Padding(
padding: EdgeInsets.symmetric(horizontal: 16.0),
child: Text('1D'),
),
...(more Buttons)...
],
onPressed: (int index) {
setState(() {
for (int buttonIndex = 0;
buttonIndex < isSelectedGraph.length;
buttonIndex++) {
if (buttonIndex == index) {
isSelectedGraph[buttonIndex] = true;
selectedGraphType = graphType[buttonIndex];
} else {
isSelectedGraph[buttonIndex] = false;
}
}
});
getData();
},
isSelected: isSelectedGraph,
),
Expanded(
child: FutureBuilder( *************FUTUREBUILDER*********
future: futureData,
builder: (context, snapshot) {
if (graphValues.isEmpty) {
return new Container();
} else {
return Graph(graphValues);
}
}),
)
As you are using a FutureBuilder you don't need to call setState anymore. Here is a possible rework of your code:
Future<Map> futureData;
Future<Map> getData() async {
try {
Map graphData = await GraphData().getGraphData(
selectedCurrency: selectedCurrency,
selectedGraphType: selectedGraphType,
);
return graphData;
} catch (e) {
throw Exception(e);
}
}
#override
void initState() {
super.initState();
futureData = getData();
}
#override
Widget build(BuildContext context) {
// Only coding the FutureBuilder for the example
return FutureBuilder<Map>(
future: futureData,
builder: (context, snapshot) {
// Future is still loading
if (!snapshot.hasData)
return CircularProgressIndicator();
else if (snapshot.data.isEmpty)
return Container();
else
return Graph(snapshot.data);
},
);
}
For your FutureBuilder to work correctly you need to return a value in your getData and use the snapshot variable.
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,
),
),
);
}
I have FutureBuilder to fetch User profil from API and code to fetch user like this :
Future<List<UserModel>> getUserByUsername({#required String username}) async {
try {
final response =
await _client.get("$_baseUrl/getUserByUsername?username=$username");
final Map<String, dynamic> responseJson = json.decode(response.body);
if (responseJson["status"] == "ok") {
List userList = responseJson['data'];
final result = userList
.map<UserModel>((json) => UserModel.fromJson(json))
.toList();
return result;
} else {
throw CustomError(responseJson['message']);
}
} catch (e) {
return Future.error(e.toString());
}
}
If you can see in above GIF, My FutureBuilder are inside BottomNavigationBar. Every i change the screen/page from BottomNavigationBar and come back to my FutureBuilder is always refresh !
How can i fixed it to only once to refresh ?
Home Screen
class _HomeScreenState extends State<HomeScreen> {
#override
Widget build(BuildContext context) {
final username = Provider.of<SharedPreferencesFunction>(context).username;
return SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
CardTime(),
FutureBuilder(
future: userApi.getUserByUsername(username: username),
builder: (BuildContext context,
AsyncSnapshot<List<UserModel>> snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
if (snapshot.hasError) {
return Center(
child: Text(
snapshot.error.toString(),
),
);
} else {
final user = snapshot.data[0];
return CardProfil(
imageUrl: "${userApi.baseImageUrl}/${user.fotoUser}",
detailProfil: [
Text(
user.namaUser,
style: TextStyle(fontWeight: FontWeight.bold),
),
Text(user.idDevice),
],
);
}
} else {
return Center(
child: CircularProgressIndicator(),
);
}
},
),
],
),
);
}
}
Shared Preferences Function
import 'package:flutter/cupertino.dart';
import 'package:shared_preferences/shared_preferences.dart';
class SharedPreferencesFunction extends ChangeNotifier {
SharedPreferencesFunction() {
initialSharedPreferences();
getUsername();
}
String _username;
String get username => _username;
void initialSharedPreferences() {
getUsername();
}
Future updateUsername(String username) async {
SharedPreferences pref = await SharedPreferences.getInstance();
await pref.setString("username", username);
//! It's Important !!! After update / remove sharedpreferences , must called getUsername() to updated the value.
getUsername();
notifyListeners();
}
Future removeUsername() async {
SharedPreferences pref = await SharedPreferences.getInstance();
final result = await pref.remove("username");
//! It's Important !!! After update / remove sharedpreferences , must called getUsername() to updated the value.
getUsername();
print(result);
notifyListeners();
}
Future getUsername() async {
SharedPreferences pref = await SharedPreferences.getInstance();
final result = pref.getString("username");
_username = result;
notifyListeners();
}
}
final sharedpref = SharedPreferencesFunction();
Update Question
I already try Initialize FutureBuilder and use initState and didChangeDependencies . But new problem is , if i initialize inside initState my profil not rebuild because Provider listen=false.
If i using didChangeDependencies my FutureBuilder still refresh every i change screen.
Something wrong ?
Using initState
Using didChangeDependencies
Initialize the Future during initState or didChangeDependencies instead.
class _HomeScreenState extends State<HomeScreen> {
Future<List<UserModel>> user;
#override
void initState() {
super.initState();
// must use listen false here
final username = Provider.of<SharedPreferencesFunction>(context, listen: false).username;
user = userApi.getUserByUsername(username: username);
}
#override
void didChangeDependencies() {
super.didChangeDependencies();
final username = Provider.of<SharedPreferencesFunction>(context).username;
user = userApi.getUserByUsername(username: username);
}
#override
Widget build(BuildContext context) {
return SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
FutureBuilder(
future: user,
builder: (context, snapshot) {
// ...
},
),
],
),
);
}
}
I faced a similar case and use AutomaticKeepAliveClientMixin on each view / page / tab bar view / widget / child to keep the page not refreshing every time I go back and forth through the tab bar.
class YourClass extends StatefulWidget {
YourClass({
Key key
}): super(key key);
#override
_YourClassState createState() => _YourClassState();
}
// Must include AutomaticKeepAliveClientMixin
class _YourClassState extends State<YourClass> with AutomaticKeepAliveClientMixin {
Future resultGetData;
void getData() {
setState(() {
resultGetData = getDataFromAPI();
});
}
// Must include
#override
bool get wantKeepAlive => true;
#override
void initState() {
getData();
super.initState();
}
#override
Widget build(BuildContext context) {
super.build(context); // Must include
return FutureBuilder(
future: resultGetAllByUserIdMCId,
builder: (context, snapshot) {
// ...
// Some Code
// ...
}
);
}
}
If you want to refresh the data you could use RefreshIndicator that runs the getData() function. Put this code inside FutureBuilder. The key: PageStorageKey(widget.key) will keep the scroll in the exact same place where you left of.
return RefreshIndicator(
onRefresh: () async {
getData();
},
child: ListView.separated(
key: PageStorageKey(widget.key),
itemCount: data.length,
separatorBuilder: (BuildContext context, int index) {
return Divider(height: 0);
},
itemBuilder: (context, index) {
return ...;
},
),
);
Use IndexedStack as the parent of tabbar.
You have to put your Future Builder in a Stateful Widget then define a
late final Future myFuture;
then you have to initialize it in the initstate so the future will be executed only one time.