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
Related
I'm building a chat app using Flutter with the MVVM pattern and the provider package. Right now, I'm trying to implement the screen to show the list of chats a user has.
Here is my ViewModel class:
class ConversationsViewModel with ChangeNotifier {
final MatchesService chatService = MatchesService();
final String userId;
final List<ChatCell> chatCells = [];
ConversationsViewModel({required this.userId}) {
loadChats();
}
void loadChats() async {
final chats = await chatService.getChats(userId: userId);
for (final chat in chats) {
final messages = await chatService.getMessages(
userId: userId,
chatId: chat.chatId,
limit: 1,
);
final cell = ChatCell(chat: chat, message: messages.first);
chatCells.add(cell);
notifyListeners();
}
}
}
and here is my widget:
class Conversations extends StatefulWidget {
const Conversations({Key? key}) : super(key: key);
#override
_ConversationsState createState() => _ConversationsState();
}
class _ConversationsState extends State<Conversations> {
#override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Selector<ConversationsViewModel, List<ChatCell>>(
selector: (context, vm) => vm.chatCells,
builder: (context, chatCells, _) {
return ListView.builder(
scrollDirection: Axis.vertical,
shrinkWrap: true,
itemCount: chatCells.length,
itemBuilder: (context, index) =>
Text(chatCells[index].message.message.body),
);
},
)
],
);
}
}
However, for some reason, the widget never reloads even when notifyListeners is called.
I'm not too sure what's wrong, since afaik I am following all the patterns and usage that the provider package dictates.
I also tried invoking loadChats in initState but realized that was even more wrong than doing so in the VM's constructor.
Any help would be appreciated.
Have you wrap your MaterialApp with ChangeNotifierProvider?
Example:
ChangeNotifierProvider(
create: (context) => ConversationsViewModel(),
child: MaterialApp()
)
I think your problem is you are not defining your provider, so you can't access its data. Under your build method you should add something like this:
final conversationsProvider = Provider.of<ConversationsViewModel>(context);
And then you can use it by doing:
conversationsProvider.chatCells;
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
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 relatively new to Flutter and am currently struggling with FutureBuilders.
I've read Remi's answer on this thread, which basically states Flutter's philosophy that widget builds should be idempotent. I get the principle in theory but how does that work practically though?
Consider the following snippet:
return Scaffold(
body: SafeArea(
child: Stack(
children: [
Consumer<DataPresenter>(
builder: (context, presenter, _) {
return DefaultFutureBuilder<List<Data>>(
future: presenter.data(),
builder: (context, data) {
// bla bla
}
);
}
),
// More widgets
],
),
),
);
}
Where this is my DefaultFutureBuilder:
class DefaultFutureBuilder<T> extends StatelessWidget {
final Future<T> future;
final Widget Function(BuildContext, T data) builder;
final Widget Function(BuildContext) errorBuilder;
DefaultFutureBuilder({
#required this.future,
#required this.builder,
this.errorBuilder,
});
#override
Widget build(BuildContext context) {
var errBuilder = errorBuilder ?? _buildDefaultErrorScreen;
return FutureBuilder<T>(
future: future,
builder: (context, snapshot) {
var indicator = Center(child: CircularProgressIndicator());
if (snapshot.connectionState == ConnectionState.done) {
if (snapshot.hasData)
return builder(context, snapshot.data);
else if (snapshot.hasError)
return errBuilder(context);
}
return indicator;
},
);
}
Widget _buildDefaultErrorScreen(BuildContext context) {
// default
}
}
Now I know that the call to presenter.data() (fetches some async data) is not kosher. But I do want to re-fetch the data when the presenter notifies the consumer while at the same time I do not want to fetch it when Flutter rebuilds my widget tree because of framework shenanigans.
My initial idea was to build the presenter so that it only fetches the data when it does not have any data at the moment. I could then set the data to null from other methods and notify the listeners to rebuild the affected widget subtrees.
Like that:
class DataPresenter extends ChangeNotifier {
// fields, services, bla bla
List<Data> _data;
Future<List<Data>> data() async {
if (_data == null) {
var response = await _service.fetchMyStuff(params: params);
_data = response.data;
}
return _data;
}
}
The problem with this solution is that any rebuilds that happen while I fetch the data for the first time will cause another request.
I'd be grateful if someone could point out which part of the framework / architecture I didn't get.
I think this might solve your problem I had this problem too very annoying anything you do and your app rebuilds. So they way I solved it was by memorizing my future. Now this may or may not work for you. If it treats the Consumer rebuild as a new future then you will be good because then the FutureBuilder will rebuild when the Consumer does which is what you want if I understand correctly. Here's what you do.
//Initialize at top of class
final AsyncMemoizer _memoizer = AsyncMemoizer();
Future<void> _someFuture() {
return this._memoizer.runOnce(() async {
//Do your async work
});
}
Then _someFuture() would be what you pass to FutureBuilder. Here is a good article on this issue. Flutter: My FutureBuilder Keeps Firing!
If you want to rebuild a provider then you can use
context.refresh(providerThatYouWantToRebuild);
This method is useful for features like "pull to refresh" or "retry on error", to restart a specific provider at any time.
Documentation is available at:
https://riverpod.dev/docs/concepts/combining_providers#faq
https://pub.dev/documentation/riverpod/latest/all/ProviderContainer/refresh.html
https://pub.dev/documentation/flutter_riverpod/latest/all/BuildContextX/refresh.html
I would suggest a solution, without a futureBuilder
You fetch data after your first Build of your Stateless Widget
WidgetsBinding.instance.addPostFrameCallback((_) => Provider.of<DataPresenter>(context, listen: false).fetchdata());
you have a difference in your _data List with value null or lenght==0
class DataPresenter extends ChangeNotifier {
// fields, services, bla bla
List<Data> _data;
// getter for the Consumer
List<Data> get data => _data;
Future<List<Data>> fetchdata() async {
_data = await _service.fetchMyStuff(params: params);
if (_data == null)
_data = new List(); //difference between null and length==0
//rebuild Consumer
notifyListeners();
}
}
The Page without FutureBilder
class ProvPage extends StatelessWidget {
#override
Widget build(BuildContext context) {
// calls after the widget ist build
WidgetsBinding.instance.addPostFrameCallback((_) => Provider.of<TodoListModel>(context, listen: false).refreshTasks());
return MultiProvider(
providers: [
ChangeNotifierProvider(create: (context) => DataPresenter()),
],
child: Scaffold(
body: SafeArea(
child: Stack(
children: [
Consumer<DataPresenter>(
builder: (context, presenter, _) {
//get current Values vom the DataPresenter._data
List<Data> currData = presenter.data;
// firstime fetch ??
if(currData == null)
return CircularProgressIndicator();
// DataPresenter.getData is finished
return new RefreshIndicator(
onRefresh: () => presenter.fetchdata(),
child: new Text("length " + currData.length.toString())
);
},
),
// More widgets
],
),
),
),
);
}
}
so I am trying to build up a list in my provider from a Future Call.
So far, I have the following ChangeNotifier class below:
class MainProvider extends ChangeNotifier {
List<dynamic> _list = <dynamic>[];
List<dynamic> get list => _list;
int count = 0;
MainProvider() {
initList();
}
initList() async {
var db = new DatabaseHelper();
addToList(Consumer<MainProvider>(
builder: (_, provider, __) => Text(provider.count.toString())));
await db.readFromDatabase(1).then((result) {
result.forEach((item) {
ModeItem _modelItem= ModeItem.map(item);
addToList(_modelItem);
});
});
}
addToList(Object object) {
_list.add(object);
notifyListeners();
}
addCount() {
count += 1;
notifyListeners();
}
}
However, this is what happens whenever I use the list value:
I can confirm that my initList function is executing properly
The initial content from the list value that is available is the
Text() widget that I firstly inserted through the addToList function, meaning it appears that there is only one item in the list at this point
When I perform Hot Reload, the rest of the contents of the list seems to appear now
Notes:
I use the value of list in a AnimatedList widget, so I am
supposed to show the contents of list
What appears initially is that the content of my list value is only one item
My list value doesn't seem to automatically update during the
execution of my Future call
However, when I try to call the addCount function, it normally
updates the value of count without needing to perform Hot Reload -
this one seems to function properly
It appears that the Future call is not properly updating the
contents of my list value
My actual concern is that on initial loading, my list value doesn't
properly initialize all it's values as intended
Hoping you guys can help me on this one. Thank you.
UPDATE: Below shows how I use the ChangeNotifierClass above
class ParentProvider extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider<MainProvider>(
create: (context) => MainProvider(),
),
],
child: ParentWidget(),
);
}
}
class ParentWidget extends StatelessWidget {
final GlobalKey<AnimatedListState> listKey = GlobalKey<AnimatedListState>();
#override
Widget build(BuildContext context) {
var mainProvider = Provider.of<MainProvider>(context);
buildItem(BuildContext context, int index, Animation animation) {
print('buildItem');
var _object = mainProvider.list[index];
var _widget;
if (_object is Widget) {
_widget = _object;
} else if (_object is ModelItem) {
_widget = Text(_object.unitNumber.toString());
}
return SizeTransition(
key: ValueKey<int>(index),
axis: Axis.vertical,
sizeFactor: animation,
child: InkWell(
onTap: () {
listKey.currentState.removeItem(index,
(context, animation) => buildItem(context, index, animation),
duration: const Duration(milliseconds: 300));
mainProvider.list.removeAt(index);
mainProvider.addCount();
},
child: Card(
child: Padding(
padding: const EdgeInsets.all(32.0),
child: _widget,
),
),
),
);
}
return Scaffold(
appBar: AppBar(),
body: Container(
color: Colors.white,
child: Padding(
padding: const EdgeInsets.all(32.0),
child: mainProvider.list == null
? Container()
: AnimatedList(
key: listKey,
initialItemCount: mainProvider.list.length,
itemBuilder:
(BuildContext context, int index, Animation animation) =>
buildItem(context, index, animation),
),
),
),
);
}
}
You are retrieving your provider from a StatelessWidget. As such, the ChangeNotifier can't trigger your widget to rebuild because there is no state to rebuild. You have to either convert ParentWidget to be a StatefulWidget or you need to get your provider using Consumer instead of Provider.of:
class ParentWidget extends StatelessWidget {
final GlobalKey<AnimatedListState> listKey = GlobalKey<AnimatedListState>();
#override
Widget build(BuildContext context) {
return Consumer<MainProvider>(
builder: (BuildContext context, MainProvider mainProvider, _) {
...
}
);
}
As an aside, the way you are using provider is to add the MainProvider to its provider and then retrieve it from within its immediate child. If this is the only place you are retrieving the MainProvider, this makes the provider pattern redundant as you can easily just declare it within ParentWidget, or even just get your list of images using a FutureBuilder. Using provider is a good step toward proper state management, but also be careful of over-engineering your app.