Flutter FutureBuilder Does Not Wait When App Updates - flutter

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.

Related

Unable to get data from the List to build the ListView.builder()

I'm trying to fetch data from the jsonplaceholder todos API, Once I retrieve the data I'm storing it into a List and notifying all the listeners. But something weird is happening.
class Todos with ChangeNotifier {
List<Todo> _items = [];
List<Todo> get item {
return [..._items];
}
Future fetchAndSetData() async {
try {
const url = 'https://jsonplaceholder.typicode.com/todos';
final List<dynamic> response =
json.decode((await http.get(Uri.parse(url))).body);
List<Todo> extractedTodo =
response.map((dynamic item) => Todo.fromJson(item)).toList();
_items = extractedTodo;
print(_items.length); // Getting 200 which is exact length I'm expecting
notifyListeners();
} catch (err) {
print(err);
}
}
}
The above code is where I'm making a get request and storing the data into the List. The following code is where I'm calling the fetchAndSetData with the help of Provider.
class _HomeScreenState extends State<HomeScreen> {
var _isLoading = true;
#override
void didChangeDependencies() {
Provider.of<Todos>(context, listen: false).fetchAndSetData().then((_) {
setState(() {
_isLoading = false;
});
});
super.didChangeDependencies();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: _isLoading
? const Center(
child: CircularProgressIndicator(),
)
: TodoList(),
);
}
}
The following is where I'm trying to get the todos from the items list.
#override
Widget build(BuildContext context) {
final todos = Provider.of<Todos>(context, listen: false).item;
print(todos.length);
return ListView.builder(
itemCount: todos.length,
itemBuilder: (ctx, index) {
return Card(
child: ListTile(
title: Text(todos[index].title),
),
);
},
);
}
Once didChangeDependencies, it will call the fetchAndSetData and will set the List, so the print statement on the Todos class will print 200 as the length of items I'm expecting but in the TodoList class where I'm calling the getter, the length I'm receiving is 0.
Now the weird part is when I removed the listen: false in the didChangeDependencies, the print statement on the fetchAndSetData getting called again and again. With that I mean the length for the todos is 200 but the print goes beyond 200. As, there is no way that the data gets updated, so I mark those as listen: false
Please help me
Please fetch your data in
Future<List<dynamic>> _post;
#override
void initState() {
super.initState();
_post = fetchAndSetData();
}
and then use a FutureBuilder like this
return FutureBuilder(
future: _post,
builder: (BuildContext context, AsyncSnapshot<dynamic> snapshot) {
if (snapshot.hasData) {

how to make this lazyload scrolling working with provider

it take about 7 days trying make a working example for lazyload listview with provider in flutter with real world example and it's still not working because i think something is missing
As a note : the first load , works good and when i scroll it's print (scroll) but nothing happened it's still in the same page
if i try to return _todolist variable in the _onScrollUpdated it not change page correctly and after three times i see this error
E/flutter ( 7713): [ERROR:flutter/lib/ui/ui_dart_state.cc(166)]
Unhandled Exception: type 'String' is not a subtype of type
'List' E/flutter ( 7713): #0 TodoService.fetchTodos
(package:flutter_todo_provider/services/todo_service.dart:32:21)
json example
https://jsonformatter.org/52c83e
todos_screen.dart
import 'package:flutter/material.dart';
import 'package:flutter_easyloading/flutter_easyloading.dart';
import 'package:flutter_todo_provider/helpers/http_exception.dart';
import 'package:provider/provider.dart';
import 'package:flutter_todo_provider/.env.dart';
import 'package:flutter_todo_provider/services/todo_service.dart';
class TodosScreen extends StatefulWidget {
#override
_TodosScreenState createState() => _TodosScreenState();
}
class _TodosScreenState extends State<TodosScreen> {
ScrollController _controller;
List<dynamic> _todoList;
bool _isLoading ;
#override
void initState() {
super.initState();
_controller = ScrollController();
_controller.addListener(_onScrollUpdated);
}
void dispose() {
_controller.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(Configuration.AppName),
),
body: FutureBuilder(
future: _fetchListItems(),
builder: (context, snapshot){
if(snapshot.hasData){
return _listItems(snapshot.data);
}
return _buildProgressIndicator();
}
),
);
}
_fetchListItems() async {
try {
await Provider.of<TodoService>(context, listen: false).loadNextPage();
_todoList = Provider.of<TodoService>(context, listen: false).items;
} on HttpException catch (e) {
EasyLoading.showError(e.message);
}
return _todoList ;
}
Widget _listItems(data){
_isLoading = Provider.of<TodoService>(context, listen: false).isLoading ;
return ListView.builder(
controller: _controller,
itemCount: data.length ,
itemBuilder: (context, index) {
return ListTile(
title: Text(data[index].title),
subtitle:Text(data[index].content),
trailing: Icon(Icons.print),
);
},
);
}
Future<void> _onScrollUpdated() async {
print("Scroll11");
var maxScroll = _controller.position.maxScrollExtent;
var currentPosition = _controller.position.pixels;
if (currentPosition == maxScroll ) {
try {
await Provider.of<TodoService>(context, listen: false).loadNextPage();
_todoList = Provider.of<TodoService>(context, listen: false).items;
// return _todoList ; if use this line i see the error
} on HttpException catch (e) {
EasyLoading.showError(e.message);
}
}
}
Widget _buildProgressIndicator() {
_isLoading = Provider.of<TodoService>(context, listen: false).isLoading ;
return new Padding(
padding: const EdgeInsets.all(8.0),
child: new Center(
child: new Opacity(
opacity: _isLoading ? 1.0 : 00,
child: new CircularProgressIndicator(),
),
),
);
}
}
todo_service.dart
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_todo_provider/.env.dart';
import 'package:flutter_todo_provider/models/todo.dart';
class TodoService with ChangeNotifier {
bool isLoading = false;
bool isFetching = false;
int currentPage = 1;
int totalRows = 10;
List<Todo> items = [];
loadNextPage() async {
await fetchTodos(currentPage);
currentPage++;
notifyListeners();
}
Future fetchTodos(int currentPage) async {
try {
//404
var options = Options(headers: {
HttpHeaders.authorizationHeader: 'Basic ${Configuration.authToken}'
});
Map<String, dynamic> qParams = {
'current_page': currentPage,
};
Response response = await Dio().get('${Configuration.ApiUrl}/todos/my_todos', options: options, queryParameters: qParams);
List<dynamic> responseBode = response.data["data"];
responseBode.forEach(( dynamic json) {
items.add(Todo.fromJson(json));
});
notifyListeners();
} on DioError catch (e) {
print("Error Message" + e.response.statusMessage);
return items=[];
}
}
}
Here is the code:
class TodoScreen extends StatefulWidget {
// Your service goes here
// (the class extending ChangeNotifier)
#override
_TodoScreenState createState() => _TodoScreenState();
}
class _TodoScreenState extends State<TodoScreen> {
final TodoService todoService = TodoService();
ScrollController _controller;
#override
void initState() {
super.initState();
_controller = ScrollController();
_controller.addListener(_onScrollUpdated);
loadNextPage();
}
void dispose() {
_controller.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Configuration.AppName'),
),
body: ChangeNotifierProvider.value(
value: todoService,
child: Consumer<TodoService>(builder: (_, ctl, __) {
if (todoService.isLoading) {
return _buildProgressIndicator();
} else {
return _listItems(todoService.items);
}
}),
),
);
}
Widget _listItems(data) {
return ListView.builder(
controller: _controller,
itemCount: data.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(data[index].title),
subtitle: Text(data[index].content),
trailing: Icon(Icons.print),
);
},
);
}
Widget _buildProgressIndicator() {
return new Padding(
padding: const EdgeInsets.all(8.0),
child: new Center(
child: CircularProgressIndicator(),
),
);
}
Future<void> _onScrollUpdated() async {
var maxScroll = _controller.position.maxScrollExtent;
var currentPosition = _controller.position.pixels;
if (currentPosition == maxScroll) {
todoService.loadNextPage();
}
}
}
Note that i didn't make changes to your service. The notifyListeners will do all the job for us.
When you are using Provider, the idea is to keep all your data inside the controller or service (the class that extends ChangeNitifier) and just use the variables with notifyListeners to change the behavior of your screen.
The screen needs to be listening for changes, for this we use the pair ChangeNotifierProvider.value with Consumer(builder: (_, ctl, __) {}).
Use ChangeNotifierProvider in some upper level of the widget tree and use Consumer only where you need the widget to be updated. You can even use more than one Consumer, they all just need to be under ChangeNotifierProvider.

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,
),
),
);
}

How to fix recursing HTTP request in FutureBuilder?

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)));
}

Button pressed return a future builder

I have a button and if pressed should return a future builder here is my code.
I already search some examples on the web but no luck, Im new in flutter development and trying to create a simple login with api call.
Future<AccessToken>fetchAccessToken() async{final token = await _repository.fetchToKen();
>>return token;
}
onPressed: () {FutureBuilder<AccessToken>(future:bloc.fetchAccessToken(),builder: (context, snapshot) {if (snapshot.hasError) {return Text('Error');} else if (snapshot.hasData) {return Text('data');} else {return `Center`(child: CircularProgressIndicator(),);}},);}
I want to show a progress indicator while waiting for the api response, but after I receive the response, my builder inside the future builder is not called.
You can't simply return a widget and place it in the widget tree like that. Maybe you can use conditional list for hiding and showing the FutureBuilder widget.
import 'package:flutter/material.dart';
class ApiWidget extends StatefulWidget {
#override
_ApiWidgetState createState() => _ApiWidgetState();
}
class _ApiWidgetState extends State<ApiWidget> {
Repository _repository = Repository();
Future<AccessToken> accessTokenFuture;
bool isButtonPressed = false;
#override
Widget build(BuildContext context) {
return Column(children: <Widget>[
FloatingActionButton(onPressed: () {
setState(() {
try {
isButtonPressed = true;
accessTokenFuture = fetchAccessToken();
} catch (_) {
print('Fetch error');
}
});
}, child: Icon(Icons.add),),
if(isButtonPressed)
FutureBuilder<AccessToken>(
future: bloc.fetchAccessToken(),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return Center(child: CircularProgressIndicator());
}
if (snapshot.hasError) {
return Text('Error');
}
Column(
children: <Widget>[Text(snapshot.data)],
);
},
),
],);
}
}
You can do something like that:
#override
Widget build(BuildContext context) {
return Column(children: <Widget>[
FloatingActionButton(onPressed: () {
setState(() {
try {
isLoading = true;
accessTokenFuture = await fetchAccessToken();
isLoading = false;
} catch (_) {
isLoading = false;
print('Fetch error');
}
});
}, child: Icon(Icons.add),),
_buildAsyncInfo(),
],);
}
Widget _buildAsyncInfo() {
return isLoading ?
CircularProgressIndicator() :
Column(
children: <Widget>[Text(snapshot.data)],
);
}