How to call api once in futurebuilder - flutter

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

Related

Please comment on 3 flutter_bloc writing styles: BlocBuilder, BlocListener, BlocConsumer

I am practicing with flick_bloc and I wonder when to use BlocBuilder, when to use BlocListener and when to use BlocConsumer. I asked a few people, they said that BlocBuilder is used the most and I also started and just practiced with it, but it seems that Blocbuilder only changed for the first time, I don't know if it's true. Can you guys give me some comments on these spellings
Bloc Builder
Used for building widgets, For Example: If you want to show a list of employee names on a page, you can return a ListView widget based on the bloc state. Also, if the employee list comes from an API, then you will need different states such as Loading, Success and Failure states. Based on these different states you can return different widgets from BlocBuilder. A CircularProgressIndicator for showing loading state, ListView for showing employee list in the success state and Error text widget for showing error message if the API fails.
BlocBuilder<BlocA, BlocAState>(
builder: (context, state) {
if (state is Loading) {
return CircularProgressIndicator();
}
}
)
Bloc Listener
BlocBuilder can only return widgets. If you want to show a snackbar or want to Navigate from one page to another, then you need to use BlocListener for that.
BlocListener<BlocA, BlocAState>(
listener: (context, state) {
if (state is Success) {
Navigator.push(context,
MaterialPageRoute(builder: (context) => const SecondRoute()),
);
}
}
)
Bloc Consumer
If you have the use of both BlocListener and BlocBuilder, then it is better to use BlocConsumer. It reduces the boilerplate of using BlocListener and BlocBuilder together.
Code Without Bloc Consumer:
BlocListener<BlocA, BlocAState>(
listener: (context, state) {
if (state is Success) {
Navigator.push(context,
MaterialPageRoute(builder: (context) => const SecondRoute()),
);
}
},
child: BlocBuilder<BlocA, BlocAState>(
builder: (context, state) {
if (state is Loading) {
return CircularProgressIndicator();
}
}
),
)
Code using Bloc Consumer:
BlocConsumer<BlocA, BlocAState>(
listener: (context, state) {
if (state is Success) {
Navigator.push(context,
MaterialPageRoute(builder: (context) => const SecondRoute()),
);
}
},
builder: (context, state) {
if (state is Loading) {
return CircularProgressIndicator();
}
}
)
BlocBuilder: You can use it to just build out your widgets, but the draw back is that you can't build in Snackbars or Dialogs into the flow, because you must return a widget in blocbuilder and you don't want to return a snackbar or dialog.
BlocListener: This would permit you to use your dialogs and snackbars, but the issue is that it can't let you do anything a blocbuilder would let you do. Which is as you might have guessed, is to return a widget, it's more suited for dismissible UI components like the dialogs and snackbars.
BlocConsumer: This widget helps you combine both a BlocListener and a BlocBuilder, so you can return static components and dismissible UI components.
So if you won't need Snackbars or Dialogs, use a BlocBuilder, If you need Snackbars or Dialogs, use a BlocListener. If you want both of them to work in synergy use a BlocConsumer.
BlocBuilder
This is used when we want to draw a Widget based on what is the current State. In the following example a new “text” gets drawn every time the state changes.
Sample Example
BlocBuilder<OrdersBloc, OrdersState>(
buildWhen: (context, state) {
return state is OrdersState.OrderCompleted
},
builder: (context, state) {
if (state is OrdersState.OrderCompleted) {
return Container(child: Text('Order Completed!'));
} else if (OrdersState.OrderInProgress) {
return Container(child: Text('In Progress'));
} else if (OrdersState.OrderRequested) {
return Container(child: Text('A customer placed an order!'));
} else {
return Container(child: Text('Waiting for an order'));
}
},
);
BlocListener
This is just a listener not a builder (like the above), that means that its job is keep listening for new changes in the state and not to return a widget. You can use listener when you want to show any dialog or any toast, or navigation from one page to another(these are few examples).
Sample Example
BlocListener<OrdersBloc, OrdersState>(
listenWhen: (context, state) {
return state is OrdersState.OrderCompleted;
},
listener: (context, state) {
// Navigate to next screen
Navigator.of(context).pushNamed('OrderCompletedScreen');
},
child: Container(child: Text('Always draw this text!')),
);
BlocConsumer
This is used when we want to draw something based on the current state and execute some actions depending on the new arriving states. This is a mix between “BlocListener” and “BlocBuilder”.
Sample Example
BlocConsumer<OrdersBloc, OrdersState>(
listenWhen: (context, state) {
return state is OrdersState.OrderCompleted ||
state is OrdersState.OrderRefunded;
},
listener: (context, state) {
if (state is OrdersState.OrdersCompleted) {
// Navigate to next screen
Navigator.of(context).pushNamed('OrderCompletedScreen');
} else if (state is OrdersState.OrderRefunded) {
// Report to analytics
Analytics.reportRefunded(state.orderId);
}
},
buildWhen: (context, state) {
return state is OrdersState.OrderCompleted ||
state is OrdersState.OrderInProgress ||
state is OrdersState.OrderRequested;
},
builder: (context, state) {
if (state is OrdersState.OrderCompleted) {
return Container(child: Text('Order Served!'));
} else if (OrdersState.OrderInProgress) {
return Container(child: Text('In Progress'));
} else {
return Container(child: Text('No State'));
}
},
);

How can I use Future builder with provider?

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) {

Stream builds a stack of Widget

So, I am using a stream to track the user's authentication state. Here is my setup, which works fine so far.
class Root extends ConsumerWidget {
final Widget _loadingView = Container(color: Colors.white, alignment: Alignment.center, child: UiHelper.circularProgress);
#override
Widget build(BuildContext context, ScopedReader watch) {
return watch(userStreamProvider).when(
loading: () => _loadingView,
error: (error, stackTrace) => _loadingView,
data: (user) => user?.emailVerified == true ? Products() : Login(),
);
}
}
The problem is, stream builds the UI multiple times. And I have a welcome dialog inside of my products page, which opens multiple times and as soon as I start the app it becomes a mess.
What should I do to avoid this scenario?
** Here I am using riverpod package
I personally recommend wrapping your widget with a StreamBuilder using the onAuthStateChanged stream. This stream automatically updates when the user change its state (logged in or out). Here is an example that may help you!
Stream<FirebaseUser> authStateChanges() {
FirebaseAuth _firebaseInstance = FirebaseAuth.instance;
return _firebaseInstance.onAuthStateChanged;
}
return StreamBuilder(
stream: authStateChanges(),
builder: (context, AsyncSnapshot snapshot) {
if (snapshot.hasData) {
// isLoggedIn
} else if (snapshot.hasData == false &&
snapshot.connectionState == ConnectionState.active) {
// isLoggedOut
} else {
// loadingView
}
},
);

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 make the connection to waiting state by using StreamBuilder in flutter

My requirement is to make that StreamBuilder connection state to waiting.
I'm using publish subject, whenever I want to load data in stream builder I'm just adding data to the sink by calling postStudentsToAssign() method, here this method making an API call which takes some time, in that time I to want make that streamBuilder connection state to waiting
Stream Builder:
StreamBuilder(
stream: studentsBloc.studentsToAssign,
// initialData: [],
builder: (context, snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.waiting:
// While waiting for the data to load, show a loading spinner.
return getLoader();
default:
if (snapshot.hasError)
return Center(child: Text('Error: ${snapshot.error}'));
else
return _getDrawer(snapshot.data);
}
}),
Initializing Observable:
final _assignStudentSetter = PublishSubject<dynamic>();
Observable<List<AssignMilestoneModel>> get studentsToAssign =>
_studentsToAssignFetcher.stream;
Method that add's data to Stream:
postStudentsToAssign(int studyingClass, String milestoneId, String subject,
List studentList) async {
var response = await provider.postAssignedStudents(
studyingClass, milestoneId, subject, studentList);
_assignStudentSetter.sink.add(response);
}
You can send null to the stream, so the snapshot.connectionState changes to active. I don't know why and whether it's official solution, but it works (at least now). I found this accidentally.
I would like the Flutter team to explain how to set snapshot's connectionState. It's not clear from StreamBuilder documentation. It seems you should replace the stream with a new one to have snapshot in waiting state. But it's agains the logic you want to implement.
I checked StreamBuilder source to find out that the AsyncSnapshot.connectionState starts as waiting (after stream is connected), after receiving data changes to active. snapshot.hasData returns true if snapshot.data != null. That's how following code works.
class SearchScreen extends StatelessWidget {
final StreamController<SearchResult> _searchStreamController = StreamController<SearchResult>();
final SearchService _service = SearchService();
void _doSearch(String text) async {
if (text?.isNotEmpty ?? false) {
_searchStreamController.add(null);
_searchService.search(text)
.then((SearchResult result) => _searchStreamController.add(result))
.catchError((e) => _searchStreamController.addError(e));
}
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Column(children: <Widget>[
SearchBar(
onChanged: (text) => _doSearch(text),
),
StreamBuilder<SearchResult>(
stream: _searchStreamController.stream,
builder: (BuildContext context, AsyncSnapshot<SearchResult> snapshot) {
Widget widget;
if (snapshot.hasData) {
widget = Expanded(
// show search result
);
}
else if (snapshot.hasError) {
widget = Expanded(
// show error
);
}
else if(snapshot.connectionState == ConnectionState.active){
widget = Expanded(
// show loading
);
}
else {
// empty
widget = Container();
}
return widget;
},
),
]),
);
}
}