How can I use Future builder with provider? - flutter

My main objective is to show a CircularProgressIndicator before I get a location address placename but I keep getting this error The getter 'placeName' was called on null..I did try to check the null value in futureBuilder but I believe my implementation is wrong. Could you please take a look ?
This is my AppData class
class AppData extends ChangeNotifier {
Address pickUpLocation;
void updatePickUpLocationAddress(Address pickUpAddress) {
pickUpLocation = pickUpAddress;
notifyListeners();
}
}
and this is the Address class
class Address {
String placeFormattedAddress;
dynamic placeName;
String placeId;
double latitude;
double longitude;
Address(
{this.placeFormattedAddress,
this.placeName,
this.placeId,
this.latitude,
this.longitude});
}
Now in my MainScreen I am using it like this but the error persisting.
#override
Widget build(BuildContext context) {
\\\
body: Stack(
\\\
Flexible(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
FutureBuilder (
future: Provider.of < AppData > (context)
.pickUpLocation
.placeName,
builder: (context, snapshot) {
if (snapshot.data == null) {
return CircularProgressIndicator();
} else {
return Text(
Provider.of < AppData > (context)
.pickUpLocation
.placeName,
style: TextStyle(fontSize: 12.0),
overflow: TextOverflow.ellipsis,
);
}
}),
],
),
)

There are a few things to be aware of.
Your Future... isn't really a Future. You're just evaluating a synchronous property of your AppData Object provided by your Provider... and that's really it. Therefore the FutureBuilder should immediately evaluate its expression without giving that "loading" experience (it should render the "loaded" widget after a first rebuild).
Say you're using a Provider to return the Future you're looking for (and that's ok), which is something close to your implementation: something like that is erroneous since it would "stress" your widget tree with handling two state changes (the provider changing and/or the future being completed). Furthermore, your future gets fired every time you rebuild this Widget: that's not good (usually, firing a Future multiple times means asking your backend for the same data multiple times).
To fix your situation you have to:
Let your getter properly return and handle a Future;
Initialize the call to your getter (i.e. the Future call) before you call the build method.
Here's how I'd change your provider model/class:
class AppData extends ChangeNotifier {
Address? pickUpLocation;
Future<void> updatePickUpLocationAddress() async {
// TODO: Maybe add error handling?
var pickUpLocation = await Future.delayed(Duration(seconds: 5)); // Request mock
notifyListeners();
}
}
Now, to initialize the Future you either do so in the above Provider (in the Constructor of AppData), or you have to change your Widget to be Stateful so that we can access to the initState() method, in which we can initialize the Future without worrying about multiple calls. In your (now Stateful) Widget:
var myFuture;
// ...
void initState() {
myFuture = Provider.of<AppData>(context).updatePickUpLocationAddress();
}
// ...
Widget build (BuildContext context) {
return // ...
FutureBuilder(
future: myFuture, // already initialized, won't re-initalize when build() is called
builder: (ctx, snapshot) => // ...
}
That should be it for this Provider + Future pattern, although a final consideration should be mentioned.
Take this made up example and look what happens (i.e. read the console) when you fire the tap some times: if ChildWidget1 is disposed, then myFuture will be fired again when it gets re-loaded in the tree. This happens because ChildWidget1 is entirely removed from the tree and then re-added again to it, i.e. it's disposed and re-initialized.
This is useful to remember because having a StatefulWidget doesn't mean we are "immune" from side effects like this one.

Flexible(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (Provider.of < AppData > (context)
.pickUpLocation ==
null)
CircularProgressIndicator(),
if (Provider.of < AppData > (context)
.pickUpLocation !=
null)
Text(
Provider.of < AppData > (context)
.pickUpLocation
.placeName,
style: TextStyle(fontSize: 12.0),
overflow: TextOverflow.ellipsis,
),
),
)

For anyone who is facing this problem
You can easily click on Options+enter on Mac and click on "wrap with Builder"
then just pass the context to your future function
child: Builder(
builder: (context) {
return FutureBuilder(
future: futureFunctionHere(context),
builder: (context, AsyncSnapshot snapshot) {

Related

How to call api once in futurebuilder

My application have different routes and I would like to know how to call my api with cubit just once when the user come for the first time on the screen and also not to re-call the api every time he returns to the screen already initialized.
my structure use bloC
and this is my profile page initialization class
#override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final user = context.read<AuthCubit>().state;
final bloc = context.read<ProfileCubit>();
return Scaffold(
body: FutureBuilder(
future: bloc.updateProfilePicture(user!.id),
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
return BlocBuilder<ProfileCubit, ProfilePicture?>(
buildWhen: (prev, curr) => prev != curr,
builder: (context, picture) {
return picture != null
? Profil(profilePicture: picture, updateIndex: updateIndex)
: Profil(updateIndex: updateIndex);
},
);
}
return Center(
child: CircularProgressIndicator(
color: Colors.orange,
),
);
},
),
);
}
There are many ways to solve this problem
1- easy (but not clean code) is to use boolean global varibal
like isApiReqursted with default value (false) and when call the api set it to true
2- you can cache the response in the repoistory or bloc and make the api method frst check if there are data if there isit does not need to make http request

How to listen to the changes and rebuild the widget in flutter using provider packge?

The problem i am facing is,
I am connecting my ui to backend with websocket using subscribe method(graphql client). That means there is a connection between my ui and backend. I am storing the data i got from backend in the local storage
From the local storage, i am getting that data,
Whenever i receive the data from backend it should be reflected in the ui automatically. For reflecting change in ui, i am using state management provider package.What should i do to make my widget rebuild on listening to the changes i had made using provider package;
class MyNotifier extends ChangeNotifier {
bool _listenableValue = false;
bool get listenableValue => _listenableValue
MyNotifier.instance();
void setValue(){
_listenableValue = !_listenableValue;
notifyListeners();
}
}
...
#override
Widget build(BuildContext context) {
return ChangeNotifierProvider<MyNotifier>(
create: (context) => MyNotifier.instance(),
builder: (context, child){
return Column(
children: [
StaticWidget(),
//This text widget will rebuild when value is changed
Selector<MyNotifier, bool>(
selector: (_, notifier) => notifier.listenableValue,
builder: (_, value, __) => Text('$value');
),
//Change value with button
IconButton(
icon: //icon
onPressed: (){
context.read<MyNotifier>().setValue();
},
);
]
);
}
);
}
Don' t use Consumer. Consumer will rebuild all widgets when a data changed. This is the bad situation for performance.
Selector is the best in my opinion.
If you have your state in a ChangeNotifier like:
class MyState extends ChangeNotifier{
addStorage(Map<String, String> data) {...}
getAllDataFromStorage() {...}
}
Then you can make your UI rebuild by just wrapping the desired widgets in a Consumer.
Consumer<MyState>(builder: (context, state) {
return Container(
padding: EdgeInsets.only(top: 10),
child: LayoutBuilder(builder: (context, constraints) {
if (screenLayout >= 1024) {
return desktopWidget(context, visitsList);
} else if (screenLayout >= 768 && screenLayout <= 1023) {
return tabletWidget(context, visitsList);
} else {
return mobileWidget(context, visitingsList, screenLayout);
}
})},
);
Note that somewhere above this snippet you should have a ChangeNotifierProvider injecting your MyState in the widget tree.
For a really thorough and complete guide take a look at Simple state management
**best way to listen changes in provider its to make getter and setter i will show you example below**
class ContactProvider with ChangeNotifier {
bool isBusy = true;
bool get IsBusy => isBusy;
set IsBusy(bool data) {
this.isBusy = data;
notifyListeners();
}
void maketru(){
IsBusy=false;
}
}
and now you can use this context.read<ContactProvider>().Isbusy;

How to load list only after FutureBuilder gets snapshot.data?

I have a JSON method that returns a List after it is completed,
Future<List<Feed>> getData() async {
List<Feed> list;
String link =
"https://example.com/json";
var res = await http.get(link);
if (res.statusCode == 200) {
var data = json.decode(res.body);
var rest = data["feed"] as List;
list = rest.map<Feed>((json) => Feed.fromJson(json)).toList();
}
return list;
}
I then call this, in my initState() which contains a list hello, that will filter out the JSON list, but it shows me a null list on the screen first and after a few seconds it loads the list.
getData().then((usersFromServer) {
setState(() {. //Rebuild once it fetches the data
hello = usersFromServer
.where((u) => (u.category.userJSON
.toLowerCase()
.contains('hello'.toLowerCase())))
.toList();
users = usersFromServer;
filteredUsers = users;
});
});
This is my FutureBuilder that is called in build() method, however if I supply the hello list before the return statement, it shows me that the method where() was called on null (the list method that I am using to filter out hello() )
FutureBuilder(
future: getData(),
builder: (context, snapshot) {
return snapshot.data != null ?
Stack(
children: <Widget>[
CustomScrollView(slivers: <Widget>[
SliverGrid(
gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: 200.0,
mainAxisSpacing: 10.0,
crossAxisSpacing: 10.0,
childAspectRatio: 4.0,
),
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return Container(
child: puzzle[0],
);
},
childCount: 1,
),
)
]),
],
)
:
CircularProgressIndicator();
});
You are calling your getData method multiple times. Don't do that. Your UI waits for one call and your code for the other. That's a mess. Call it once and wait for that call.
You need to define the future to wait for in your state:
Future<void> dataRetrievalAndFiltering;
Then in your initstate, assign the whole operation to this future:
(note that I removed the setState completely, it's not needed here anymore)
dataRetrievalAndFiltering = getData().then((usersFromServer) {
hello = usersFromServer.where((u) => (u.category.userJSON.toLowerCase().contains('hello'.toLowerCase()))).toList();
users = usersFromServer;
filteredUsers = users;
});
Now your FurtureBuilder can actually wait for that specific future, not for a new Future you generate by calling your method again:
FutureBuilder(
future: dataRetrievalAndFiltering,
builder: (context, snapshot) {
Now you have one Future, that you can wait for.
This response is a little too late to help you, but for anyone wondering how to load a Future and use any of it's data to set other variables without having the issue of saving it in a variable, and then calling setState() again and loading your future again, you can, as #nvoigt said, set a variable of the Future in your state, then you can call the .then() function inside the addPostFrameCallback() function to save the result, and finally using the future variable in your FutureBuilder like this.
class _YourWidgetState extends State<YourWidget> {
...
late MyFutureObject yourVariable;
late Future<MyFutureObject> _yourFuture;
#override
void initState() {
super.initState();
_yourFuture = Database().yourFuture();
WidgetsBinding.instance?.addPostFrameCallback((_) {
_yourFuture.then((result) {
yourVariable = result;
// Execute anything else you need to with your variable data.
});
});
}
#override
Widget build(BuildContext context) {
return FutureBuilder(
future: _yourFuture,
builder: ...
);
}
}
dynamic doSomeStuffWithMyVariable() {
// Do some stuff with `yourVariable`
}
So, the advantage of this is that you can use the data loaded in the future outside the scope of the FutureBuilder, and only loading it once.
In my case, I needed to get a map of objects from my future, then the user could select some of those objects, save them on a map and make some calculations based on that selection. But as selecting that data called setState() in order to update the UI, I wasn't able to do so without having to write a complicated logic to save the user selection properly in the map, and having to call the future again outside the scope of the FutureBuilder to get it's data for my other calculations.
With the example above, you can load your future only once, and use the snapshot data outside the scope of the FutureBuilder without having to call the future again.
I hope I was clear enough with this example. If you have any doubts I will gladly clarify them.

FutureBuilder From Routing Arguments

I'm working on an application in which the user selects an option from a list on Screen A, then details for that item are retrieved and loaded on Screen B.
I'm confused on how to make this work. FutureBuilder requires that the Future be acquired before the build() function, like in initState(). However, routing arguments are obtained through the BuildContext with ModalRoute.of(myBuildContextHere).settings.arguments, and the BuildContext is only available to the build() function.
Moreover, it seems that Screen B is not actually disposed of when the back button is used to return to Screen A. In my very specific case Screen B displays services provided by a Bluetooth device selected in Screen A. If the selected device is not disconnected when returning to Screen A, it will not appear in the list of available devices. When the device is disconnected, Screen B runs into an error when retrieving details about the characteristics for a disconnected device, though I believe Flutter should've disposed of this Widget when it was popped from the Navigation stack. This issue stems from creating the Future for the FutureBuilder at build-time.
Screen A:
Button(
...
onPressed: () {
Navigator.pushNamed(context, '/screenB', arguments: ScreenBArguments("www.google.com"));
},
...
)
Screen B:
#override
void initState() {
// Oh if only I could access those arguments here!
// setState(() {
// future = _resolveIP(ModalRoute.of(myBuildContext).settings.arguments.url);
// });
}
#override
Widgetbuild build(BuildContext context) {
String URL = ModalRoute.of(context).settings.arguments.url;
return Scaffold(
body: Center(
child: FutureBuilder<String>(
future: _resolveIP(URL), // I can technically spawn a Future here, but now it's created at build-time.
builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
if(ConnectState.done) return Text(snapshot.data);
}
)
)
);
}
The problem is you don't have BuildContext in insitState()
Please try this
#override
void initState() {
SchedulerBinding.instance.addPostFrameCallback((_) {
var result = ModalRoute.of(myBuildContext).settings.arguments.url;
// setState(()=>{});
});
}
#override
Widgetbuild build(BuildContext context) {
String URL = ModalRoute.of(context).settings.arguments.url;
return Scaffold(
body: Center(
child: FutureBuilder<String>(
future: result, // I can technically spawn a Future here, but now it's created at build-time.
builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
if(ConnectState.done) return Text(snapshot.data);
}
)
)
);
}

How to run a function inside a child stateful widget from parent widget?

I am trying to run a function(with arguments) inside two-levels down StateFul widget, by clicking a button in the parent of the parent of that child(after having all widgets built, so not inside the constructor). just like in the image below:
More details is that I created a Carousal which has Cards inside, published here.
I created it with StreamBuilder in mind(this was the only use case scenario that I used it for so far), so once the stream send an update, the builder re-create the whole Carousal, so I can pass the SELECTED_CARD_ID to it.
But now I need to trigger the selection of the carousal's Cards programmatically, or in another word no need for two construction based on the snapshot's data like this:
return StreamBuilder(
stream: userProfileBloc.userFaviourateStream,
builder: (context, snapshot) {
if (snapshot.hasData) {
return SelectableCarousal(
selectedId: snapshot.data.toInt(),
onTap: //Update the stream
//some props...,
);
} else {
return SelectableCarousalLoading(
selectedId: null,
onTap: //Update the stream
//some props...,
);
}
},
);
But instead, I'm trying to have something like this so I can use it for others use cases:
Widget myCarousal = SelectableCarousal(
selectedId: null,
onTap: //Update the stream
//some props...,
);
return StreamBuilder(
stream: userProfileBloc.userFaviourateStream,
builder: (context, snapshot) {
// Then when data ready I can update
// the selection by calling two-level down function
if (snapshot.hasData) {
myCarousal.selectById(3);
}
// Build same carousal in all cases.
return myCarousal;
},
);
so this led me to my original question "How to run a function(with arguments) inside two-levels down StateFul widget?".
I appreciate any help. thanks a lot.
I was able to solve that challenge using the BLoC & Stream & StreamSubscription, see the image below:
Inside the Homepage screen:
///...Inside Homepage screen level-0
RaisedButton(
child: Text('Update value in the BLoC'),
onPressed: () {
bloc.changeSelectedState(isSel);
},
),
//...
inside the BLoC:
class Bloc {
final BehaviorSubject<bool> _isSelectedStreamController = BehaviorSubject<bool>();
// Retrieve data from stream
Stream<bool> get isSelectedStream => _isSelectedStreamController.stream;
// Add data to stream
Function(bool) get changeSelectedState => _isSelectedStreamController.sink.add;
void dispose() {
_isSelectedStreamController.close();
}
}
final bloc = Bloc();
Inside any widget in any level as long as it can reach the bloc:
// This inside the two-levels down stateful widget..
StreamSubscription isSelectedSubscription;
Stream isSelectedStream = bloc.isSelectedStream;
isSelectedSubscription = isSelectedStream.listen((value) {
// Set flag then setState so can show the border.
setState(() {
isSelected = value;
});
});
//...other code
#override
Widget build(BuildContext context) {
return Container(
decoration: isSelected
? BoxDecoration(
color: Colors.deepOrangeAccent,
border: Border.all(
width: 2,
color: Colors.amber,
),
)
: null,
//...other code
);
}
so the new design of my widget includes the BLoC as a main part of it, see the image:
and...works like a charm with flexible and clean code and architecture ^^