FutureBuilder From Routing Arguments - flutter

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

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

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

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

How to wait for an alert to pop up before proceeding without making it necessary for the user to tap to proceed?

I'm a beginner to Flutter, and in my current (first) project I have an alert that I want to pop up before the next bit of code runs.
If I use await I have found that it then requires the user to tap on the screen (outside the bounds of the alert) in order for the program to continue. Is there any way to just make sure the alert is present before proceeding without requiring the tap?
Here is my alert method:
myAlert(BuildContext context) {
return showDialog<void>(
context: context,
builder: (BuildContext context) {
return new AlertDialog(
title: Text('Some text'),
);
},
);
}
I call the method and then in the next line call another method which does some intensive computations:
myAlert(context);
compute();
Both methods are called on the press of a button (I don't know if this would be relevant in any way, so just putting it out there).
I want compute() to be called only after the alert appears. As of right now, compute() begins running before the alert pops up.
Thanks!
This is a bit confusing, but if you want compute to run when creating the dialog, run it inside the builder (a better option might be to create a separate widget and do it in his initState).
myAlert(BuildContext context) {
return showDialog<void>(
context: context,
builder: (BuildContext context) {
return new MyAlertDialog();
},
);
}
my_alert_dialog.dart
class MyAlertDialog extends StatefulWidget {
#override
_MyAlertDialogState createState() => _MyAlertDialogState();
}
class _MyAlertDialogState extends State<MyAlertDialog> {
#override
void initState() {
compute()//HERE u can du your compute
super.initState();
}
#override
Widget build(BuildContext context) {
return //Build here your Dialog Content...
}
}
If you wanna wait for the dialog close and then execute your compute, use await.
myAlert(BuildContext context) async{
return await showDialog<void>(
context: context,
builder: (BuildContext context) {
return new AlertDialog(
title: Text('Some text'),
);
},
);
}

Can I skip BlocBuilder render in some case?

There's a screen that have a bloc MyBloc myBloc
In the screen's build method, it's like this:
Widget build(BuildContext context) {
return MyCustomLoadingStack( //My custom widget to have the main content below the loading widget
_buildContent(context), //My main content
_buildLoading(context)); //My loading on top
}
And my 2 method:
Widget _buildContent(BuildContext context) {
return Column(children: <Widget>[
OtherWidgetOne(),
OtherWidgetTwo(),
BlocBuilder<MyEvent, MyState>(
bloc: myBloc,
builder: (BuildContext context, MyStatestate) {
switch (state.type) {
case MyStateList.doneWorking:
return MyDataWidget(); // this content cares about displaying the data only
default:
return Container(); //otherwise display nothing
}
},
)
]);
}
Widget _buildLoading(BuildContext context) {
return BlocBuilder<MyEvent, MyState>(
bloc: myBloc,
builder: (BuildContext context, MyState state) {
switch (state.type) {
case MyStateList.loading:
return LoadingView(); //This _buildLoading cares the loading only
default:
return Container(); //If it's not loading the show nothing for the loading layer
}
},
)
}
My problem is when the content is currently showing data. When I yield MyState(type: MyStateList.loading) to show the loading when doing something else (like load more for the data which is currently showing). Both BlocBuilder are called and then the _buildContent(context) show nothing because it doesn't meet the MyStateList.doneWorking condition. And of course the _buildLoading(context) shows the loading on an empty content bellow.
Is there anyway I can skip the BlocBuilder inside _buildContent(context) to keeps showing the current data and still have the loading on top?
I though about having a Widget to contains the data or empty Container() to use in the default case of the _buildContent(context) but it doesn't make sense to me because they may re-render the same widget.
Thank you for your time.
Great question! Is there a reason why you're using BlocBuilder instead of StreamBuilder?
To show nothing while the data is loading and auto populate the data once it's loaded, I typically do the following:
Widget _buildLoading(BuildContext context) {
return StreamBuilder<Model>(
stream: bloc.modelStream,
builder: (context, snapshot) {
if (snapshot.hasError) return _buildErrorWidget(snapshot.error, context);
if (snapshot.hasData) {
return DesiredWidget()
} else {
return LoadingView()
}
}
I haven't seen your BloC file but you may have to add a couple lines like the following:
final _modelFetcher = BehaviorSubject<Model>();
Stream<Model> get modelStream => _modelFetcher.stream;
Function(Model) get changeModelFetcher => _modelFetcher.sink.add;
#override
void dispose() async {
await _modelFetcher.drain();
_modelFetcher.close();
}
Let me know if this helps at all.
There is a pullrequest beeing discussed to fix this problem right now:
https://github.com/felangel/bloc/issues/315?fbclid=IwAR2x_Q1x5MIUUPE7zFRpjNkhjx5CzR0qiRx-P3IKZR_VRGEp3eqQisTthDo
For now, you can use a class for a more complex state. This can be something like:
class MyState extends Equatable {
final bool isLoading;
final List<MyData> data;
bool get hasData => data.isNotEmpty;
MyState(this.isLoading,this.data) : super([isLoading,data]);
}
I use Equatable (https://pub.dev/packages/equatable) for easier isEqual implementation.
Widget _buildContent(BuildContext context) {
return Column(children: <Widget>[
OtherWidgetOne(),
OtherWidgetTwo(),
BlocBuilder<MyEvent, MyState>(
bloc: myBloc,
builder: (BuildContext context, MyStatestate) {
if (state.hasData) {
return MyDataWidget(); // this content cares about displaying the data only
} else {
return Container(); //otherwise display nothing
}
},
)
]);
}
Widget _buildLoading(BuildContext context) {
return BlocBuilder<MyEvent, MyState>(
bloc: myBloc,
builder: (BuildContext context, MyState state) {
if (state.isLoading) {
return LoadingView(); //This _buildLoading cares the loading only
} else {
return Container(); //If it's not loading the show nothing for the loading layer
}
},
);
}
Downside of this appraoch is, that the datawidget will redraw, even tho the data doesnt change. This will be fixed with the mentioned pullrequest.