I have a Listview.builder inside a FutureBuilder which taking some data from an http request.i have a bool closed i want to prevent some items from refreshing if status bool is true
how can I do that
You can achieve this by placing your call in initState. In this way you can make sure that it will get the data only once.
example:
class FutureSample extends StatefulWidget {
// Create instance variable
#override
_FutureSampleState createState() => _FutureSampleState();
}
class _FutureSampleState extends State<FutureSample> {
Future myFuture;
Future<String> _fetchData() async {
await Future.delayed(Duration(seconds: 10));
return 'DATA';
}
#override
void initState() {
// assign this variable your Future
myFuture = _fetchData();
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Center(
child: FutureBuilder(
future: myFuture,
builder: (ctx, snapshot) {
if (snapshot.hasData) {
return Text(snapshot.data.toString());
}
return CircularProgressIndicator();
},
),
),
);
}
}
In that way you don't need a bool value. There are also different ways to achieve or extend your request. You can check this article for more informations: https://medium.com/flutterworld/why-future-builder-called-multiple-times-9efeeaf38ba2
Related
I have a variable named userName,which depends on databse query,so async is a must.
My older code can be concluded liks this
class IndexScreen extends StatefulWidget {
#override
_IndexScreenState createState() => _IndexScreenState();
}
//use database query function
Future<void> initUser() async{
UserTable().getUserInfo(curUserEmail).then((value)=>null);
}
//show page
class _IndexScreenState extends State<IndexScreen> {
#override
Widget build(BuildContext context) {
initUser().then((value){
final theme = Theme.of(context);
return WillPopScope(
onWillPop: () =>router.navigateTo(context, '/welcome'),
child: SafeArea(
child: Scaffold(
//The static global variable is used in Body in other files
body: Body()
),
),
);
});
}
}
It warns that miss return,I dont knwo how to amend my code.
Thanks!!
You can achive this by using the FutureBuilder widget. Please refer the code below.
class IndexScreen extends StatefulWidget {
#override
_IndexScreenState createState() => _IndexScreenState();
}
//use database query function
Future<Map> initUser() async {
final data =
await UserTable().getUserInfo(curUserEmail);
return data;
}
//show page
class _IndexScreenState extends State<IndexScreen> {
#override
Widget build(BuildContext context) {
return FutureBuilder(
future: initUser(),
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (snapshot.hasData) {
final theme = Theme.of(context);
return WillPopScope(
onWillPop: () => router.navigateTo(context, '/welcome'),
child: SafeArea(
child: Scaffold(
body: Body(),
),
),
);
} else {
// Returns empty container untill the data is loaded
Container();
}
},
);
}
}
I have a page that dynamically accepts a future list and a callback to get the future list to receive data and be able to refresh it through on refresh. a simplified version looks like this:
class ListSearchPage<T> extends StatefulWidget {
final Future<List<T>> itemsFuture;
final ValueGetter<Future<List<T>>> getItemsFuture;
const ListSearchPage({Key key, this.getItemsFuture, this.itemsFuture})
: super(key: key);
#override
_ListSearchPageState createState() => _ListSearchPageState();
}
class _ListSearchPageState<T> extends State<ListSearchPage> {
Future<List<T>> itemsFuture;
TextEditingController _controller;
#override
void initState() {
itemsFuture = widget.itemsFuture;
_controller = TextEditingController();
super.initState();
}
#override
void dispose() {
_controller.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return FutureBuilder(
future:
itemsFuture != null ? itemsFuture : widget.getItemsFuture(),
builder: (context, snapshot) {
return RefreshIndicator(
onRefresh: () async {
setState(() {
itemsFuture = null;
_controller.text = '';
});
},
child: ...
);
});
}
}
So the first time, the page loads with the future already loaded. when the user refreshes, I mark the future as null so the callback gets called and the data can be re-fetched.
I'm trying to implement flutter_hooks throughout the app now and I've refactored this widget to be like this (simplified version):
class ListSearchPage<T> extends HookWidget {
final Future<List<T>> itemsFuture;
final ValueGetter<Future<List<T>>> getItemsFuture;
const ListSearchPage({Key key, this.getItemsFuture, this.itemsFuture})
: super(key: key);
#override
Widget build(BuildContext context) {
final itemsFutureNotifier = useState(this.itemsFuture);
final TextEditingController _controller = useTextEditingController();
return FutureBuilder(
future:
itemsFutureNotifier.value != null ? itemsFutureNotifier.value : getItemsFuture(),
builder: (context, snapshot) {
return RefreshIndicator(
onRefresh: () async {
itemsFutureNotifier.value = null;
_controller.text = '';
},
child: ...
);
});
}
}
This works the first time, however after that the value keeps on getting assigned to null, and therefore the value notifier does not get notified about the change. How can I force the widget to rebuild in this case like before? and as a bonus, do you see a better solution for this?
Thanks in advance.
update
This is itemsFuture
final future = useMemoized(() => repository.fetchData());
This is getItemsFuture
() => repository.fetchData()
The idea behind it is to fetch the data before the search page is opened. In my use case works.
I've found a solution to my problem, but I won't post it as an answer because I don't believe is clean and I rather see if someone finds the proper way of doing it.
current solution
#override
Widget build(BuildContext context) {
// feels like a dirty solution for rebuilding on refresh
final counterNotifier = useState(0);
final itemsFutureNotifier = useState(this.itemsFuture);
final TextEditingController _controller = useTextEditingController();
return ValueListenableBuilder(
valueListenable: counterNotifier,
builder: (context, value, child) {
return FutureBuilder(
future:
itemsFutureNotifier.value != null ? itemsFutureNotifier.value : getItemsFuture(),
builder: (context, snapshot) {
return RefreshIndicator(
onRefresh: () async {
counterNotifier.value++;
itemsFutureNotifier.value = null;
_controller.text = '';
},
child: ...
);
});
});
As you can see I now have a counter notifier that will actually rebuild the ValueListenableBuilder and will make the FutureBuilder fetch the data
I think itemsFuture is not necessary to set to null (because it can be a initial statement inside useState).
#override
Widget build(BuildContext context) {
final fetchData = useState(itemsFuture ?? getItemsFuture());
return Scaffold(
body: FutureBuilder(
future: fetchData.value,
builder: (context, snapshot) {
return RefreshIndicator(
onRefresh: () async {
fetchData.value = getItemsFuture();
},
child: ...
);
},
),
);
}
I'm trying to use FutureBuilder but i'm countering an issue, my future is called but not executed. And i couldn't understand why. Does someone has an idea why ?
This is my code:
class TesPage extends StatefulWidget {
#override
_TesPageState createState() => _TesPageState();
}
class _TesPageState extends State<TesPage> {
Future<String> future;
Future<String> futureFunction() async {
try {
await text();
return "Test"; // this is never called
} catch (e) {
return e.toString();
}
}
#override
#mustCallSuper
void initState() {
future = futureFunction();
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: new FutureBuilder<String>(
future: future,
builder: (BuildContext context, snapshot) {
// returning a widget based on the future
},
),
);
}
}
Thank you !
Update
fix my code example
You are not properly waiting for a Future to be resolved.
I suggest you to do the following way:
class TesPage extends StatefulWidget {
#override
_TesPageState createState() => _TesPageState();
}
class _TesPageState extends State<TesPage> {
//Future<String> future;
Future<String> futureFunction() async {
try {
await text();
return "Test"; // this is never called
} catch (e) {
return e.toString();
}
}
//#override
//#mustCallSuper
//void initState() {
// future = futureFunction();
// super.initState();
//}
#override
Widget build(BuildContext context) {
return Scaffold(
body: new FutureBuilder<String>(
future: futureFunction(),
builder: (BuildContext context, snapshot) {
// returning a widget based on the future
},
),
);
}
}
I've commented the unused statements and variables
This way the FutureBuilder will invoke the futureFunction and wait for its result.
Be aware of the states snapshot can be in, check this for complete informations.
Usually you can just check if snapshot.hasData or not, but the class offers you more information about the status of the async computation (aka the futureFunction in this case).
I know how to build a Widget from a FutureBuilder, so this is my future/async function, for example:
Future<Null> myFuture() async
{
// etc.
}
If I want to build an AppBar title, this works fine:
class MyStuff extends StatelessWidget {
#override
Widget build(BuildContext context) {
return AppBar(
title: FutureBuilder(
future: myFuture(),
builder: (context, snapshot) {
// etc...
return Text("blahblah");
}
));
}
}
Now, I want to build the AppBar's bottom which expects a PreferredSizeWidget, so this works, but is not async:
class MyStuff2 extends StatelessWidget {
#override
Widget build(BuildContext context) {
return AppBar(
bottom: PreferredSize(),
);
}
}
But how can I use this in a future/async way? this doesn't even compile:
class MyStuff3 extends StatelessWidget {
#override
Widget build(BuildContext context) {
return AppBar(
bottom: FutureBuilder(, // is there a FutureBuilder for PreferredSizeWidgets?
future: myFuture(),
builder: (context, snapshot) {
// etc...
// I want to compute preferred size from async call result
return PreferredSize(); // pseudo-code ???
}
);
}
}
It's not gonna work since the FutureBuilder returns a Widget , so, what you can do is convert your stateless builder to a stateful one and create your own "PreferredSizeFutureBuilder"
import 'package:flutter/material.dart';
class PreferredSizeFutureBuilder extends StatefulWidget {
#override
_PreferredSizeFutureBuilderState createState() =>
_PreferredSizeFutureBuilderState();
}
class _PreferredSizeFutureBuilderState
extends State<PreferredSizeFutureBuilder> {
bool isCompleted = false;
#override
void initState() {
super.initState();
myFuture();
}
Future<void> myFuture() async {
//do something
setState(() {
isCompleted = true;
});
}
#override
Widget build(BuildContext context) {
return AppBar(
bottom: isCompleted //we check if the future has ended
? PreferredSize(
child: YourComponent(), //your component
preferredSize: Size.fromHeight(12.0), //the size you want
)
: null); //if the future is loading we don't return anything, you can add your loading widget here
}
}
Of course this means that the snapshot object is not gonna work , instead you use the isCompleted bool to check if your future is done, you can even use the ConnectionState instead of a boolean to have the "snapshot" behaviour
You can wrap FutureBuilder into a PreferredSize-Widget.
No need to create an own FutureBuilder-Widget that implements PreferredSize.
class MyStuff2 extends StatelessWidget {
#override
Widget build(BuildContext context) {
return AppBar(
bottom: PreferredSize(
child: FutureBuilder(future: _getData(), builder: (BuildContext context, AsyncSnapshot snapshot) {
if(snapshot.data != null) {
return Text(snapshot.data);
} else {
return Text('Loading...');
}
),
preferredSize: Size.fromHeight(50.0),
);
}
}
I'm trying to avoid rebuilding FutureBuilder in flutter. I have tried solution suggested in below Q's.
How to parse JSON only once in Flutter
Flutter Switching to Tab Reloads Widgets and runs FutureBuilder
still my app fires API every time I navigate to that page. Please point me where I'm going wrong.
Util.dart
//function which call API endpoint and returns Future in list
class EmpNetworkUtils {
...
Future<List<Employees>> getEmployees(data) async {
List<Employees> emps = [];
final response = await http.get(host + '/emp', headers: { ... });
final responseJson = json.decode(response.body);
for (var empdata in responseJson) {
Employees emp = Employees( ... );
emps.add(emp);
}
return emps;
}
}
EmpDetails.dart
class _EmpPageState extends State<EmpPage>{
...
Future<List<Employees>>_getAllEmp;
#override
initState() {
_getAllEmp = _getAll();
super.initState();
}
Future <List<Employees>>_getAll() async {
_sharedPreferences = await _prefs;
String authToken = AuthSessionUtils.getToken(_sharedPreferences);
return await EmpNetworkUtils().getEmployees(authToken);
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: new AppBar( ... ),
body: Container(
child: FutureBuilder(
future: _getAllEmp,
builder: (BuildContext context, AsyncSnapshot snapshot) { ... }
)))
}
}
Update:
I'm using bottomNavigationBar in my app, from which this page is loaded.
You are calling your getEmployees function in initState, which is meant to be called every time your widget is inserted into the tree. If you want to save the data after calling your function the first time, you will have to have a widget that persists.
An easy implementation would be using an InheritedWidget and a data class:
class InheritedEmployees extends InheritedWidget {
final EmployeeData employeeData;
InheritedEmployees({
Key key,
#required Widget child,
}) : assert(child != null),
employeeData = EmployeeData(),
super(key: key, child: child);
static EmployeeData of(BuildContext context) => (context.inheritFromWidgetOfExactType(InheritedEmployees) as InheritedEmployees).employeeData;
#override
bool updateShouldNotify(InheritedEmployees old) => false;
}
class EmployeeData {
List<Employees> _employees;
Future<List<Employees>> get employees async {
if (_employees != null) return _employees;
_sharedPreferences = await _prefs;
String authToken = AuthSessionUtils.getToken(_sharedPreferences);
return _employees = await EmpNetworkUtils().getEmployees(authToken);
}
}
Now, you would only have to place your InheritedEmployees somewhere that will not be disposed, e.g. about your home page, or if you want, even about your MaterialApp (runApp(InheritedEmployees(child: MaterialApp(..));). This way the data is only fetched once and cached after that. You could also look into AsyncMemoizer if that suits you better, but the example I provided should work fine.
Now, you will want to call this employees getter in didChangeDependencies because your _EmpPageState is dependent on InheritedEmployees and you need to look that up, which cannot happen in initState:
class _EmpPageState extends State<EmpPage>{
Future<List<Employees>>_getAllEmp;
#override
void didChangeDependencies() {
_getAllEmp = InheritedEmployees.of(context).employees;
super.didChangeDependencies();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: new AppBar( ... ),
body: Container(
child: FutureBuilder(
future: _getAllEmp,
builder: (BuildContext context, AsyncSnapshot snapshot) { ... }
)))
}
}
I mentioned that your State is now dependent on your InheritedWidget, but that does not really matter as updateShouldNotify always returns false (there are not going to be any additional builds).
I got another way to solve this issue and apply to my app also
Apply GetX controller to call API and render response data
Remove FutureBuilder to call API data
Apply GetX controller to call API data, Like
class NavMenuController extends GetxController {
Api api = new Api();
var cart = List<NavMenu>().obs;
#override
void onInit() {
// TODO: implement onInit
getNavMenuData();
super.onInit();
}
Future<List<NavMenu>> getNavMenuData() async {
var nav_data = await api.getNavMenus();
if(nav_data!=null) {
cart.value = nav_data;
}
return cart;
}
}
Call API using controller on initState() into desired class
class NavMenuDrawer extends StatefulWidget {
#override
_NavMenuDrawerState createState() => _NavMenuDrawerState();
}
class _NavMenuDrawerState extends State<NavMenuDrawer> {
final NavMenuController navMenuController = Get.put(NavMenuController());
#override
void initState() {
// TODO: implement initState
super.initState();
navMenuController.getNavMenuData();
}
Remove below FutureBuilder code for calling API, [if you use FutureBuilder/StreamBuilder whatever]
return FutureBuilder<List<NavMenu>>(
future: api.getNavMenus(),
builder: (context, snapshot) {
return ListView.builder(
scrollDirection: Axis.vertical,
shrinkWrap: true,
physics: ScrollPhysics(),
itemCount: snapshot.data?.length ?? 0,
itemBuilder: (context, index) {
return Column(
children: [
ListTile(
title: Text("${snapshot.data[index].title}"),
Just use GetX controller to get data, like
return ListView.builder(
scrollDirection: Axis.vertical,
shrinkWrap: true,
physics: ScrollPhysics(),
itemCount: navMenuController.cart?.length ?? 0,
itemBuilder: (context, index) {
return Column(
children: [
ListTile(
title: Obx(() {
return Text(
"${navMenuController.cart.value[index].title}");
}),
Note : For more info you can search on how to apply GetX