Something is wrong with this code, but I can not figure out what is it.
The problem is that loading is executed, and the Future finishes successfully and prints "From the onPress", but the data function is never executed so never prints "Done from the button!!!"
This is an example that reproduces the problem:
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
void main() {
runApp(ProviderScope(child: MaterialApp(home: Scaffold(body: Center(child: MyHomeAsyncValue()),))));
}
final voidFutureProvider = FutureProvider.autoDispose.family<void, String>((ref, str) {
return Future.delayed(Duration(seconds: 5), () { print(str); });
});
class MyHomeAsyncValue extends ConsumerWidget {
MyHomeAsyncValue({Key? key,}) : super(key: key);
#override
Widget build(BuildContext context, ScopedReader watch) {
return ElevatedButton(
child: Text('Call'),
onPressed: () {
final AsyncValue<void> fromButton = context.read(voidFutureProvider("From the onPress"));
fromButton.when(
data: (value) => print("Done from the button!!!"),
loading: () => print("Loading...."),
error: (error, stackTrace) => print("Error $error"),
);
},
);
}
}
Note: I tried to wrap the button in a Consumer and use watch instead of read, but the result was the same.
Version flutter_riverpod: ^0.14.0+3
Updates:
Also tested avoiding void as the return type, with the same result:
final userUpdateFutureProvider = FutureProvider.autoDispose.family<User, User>((ref, user) async {
return Future.delayed(Duration(seconds: 100), () => user);
});
Update: Created a ticket https://github.com/rrousselGit/river_pod/issues/628
After creating a ticket thinking it is a bug, the author responded really quickly.
Basically, his response is:
This is not how AsyncValue works. when does not "listen" to
data/loading/error. It's a switch-case on the current state.
So replacing the onPressed code with the next new code works:
onPressed: () {
final value = context.read(voidFutureProvider("From the onPress").future);
value
.then((value) => print("Done from the button!!!"))
.onError((error, stackTrace) => print("Error $error"));
},
So the solution looks to use the inner future to register callbacks.
What is not clear for me is why does the same code work outside of the onPressed? 🤷
Here a full working example: https://github.com/angelcervera/testing_riverpod/blob/main/lib/main_onpress_working.dart
Related
I am getting this error when I have signed out from my flutter app and trying to log in again:
StateError (Bad state: Stream has already been listened to.)
The code that gives me this error is on my first page:
#override
void initState() {
AwesomeNotifications().actionStream.listen((notification) async {
if (notification.channelKey == 'scheduled_channel') {
var payload = notification.payload['payload'];
var value = await FirebaseFirestore.instance
.collection(widget.user.uid)
.doc(payload)
.get();
navigatorKey.currentState.push(PageRouteBuilder(
pageBuilder: (_, __, ___) => DetailPage(
user: widget.user,
i: 0,
docname: payload,
color: value.data()['color'].toString(),
createdDate: int.parse((value.data()['date'].toString())),
documentId: value.data()['documentId'].toString(),)));
}
});
super.initState();
}
And on another page that contains the sign out code.
await FirebaseAuth.instance.signOut();
if (!mounted) return;
Navigator.pushNamedAndRemoveUntil(context,
"/login", (Route<dynamic> route) => false);
What can I do to solve this? Is it possible to stop listen to actionstream when I log out? Or should I do it in another way?
Streams over all are single use, they replace the callback hell that that ui is, at first a single use streams can seem useless but that may be for a lack of foresight. Over all (at lest for me) flutter provides all the necessary widgets to not get messy with streams, you can find them in the Implementers section of ChangeNotifier and all of those implement others like TextEditingController.
With that, an ideal (again, at least for me) is to treat widgets as clusters where streams just tie them in a use case, for example, the widget StreamBuilder is designed to build on demand so it only needs something that pumps changes to make a "live object" like in a clock, a periodic function adds a new value to the stream and the widget just needs to listen and update.
To fix your problem you can make .actionStream fit the case you are using it or change a bit how are you using it (having a monkey patch is not good but you decide if it is worth it).
This example is not exactly a "this is what is wrong, fix it", it is more to showcase a use of how pushNamedAndRemoveUntil and StreamSubscription can get implemented. I also used a InheritedWidget just because is so useful in this cases. One thing you should check a bit more is that the variable count does not stop incrementing when route_a is not in focus, the stream is independent and it will be alive as long as the widget is, which in your case, rebuilding the listening widget is the error.
import 'dart:async';
import 'package:flutter/material.dart';
void main() => runApp(App());
const String route_a = '/route_a';
const String route_b = '/route_b';
const String route_c = '/route_c';
class App extends StatelessWidget {
Stream<int> gen_nums() async* {
while (true) {
await Future.delayed(Duration(seconds: 1));
yield 1;
}
}
#override
Widget build(BuildContext ctx) {
return ReachableData(
child: MaterialApp(
initialRoute: route_a,
routes: <String, WidgetBuilder>{
route_a: (_) => Something(stream: gen_nums()),
route_b: (_) => FillerRoute(),
route_c: (_) => SetMount(),
},
),
);
}
}
class ReachableData extends InheritedWidget {
final data = ReachableDataState();
ReachableData({super.key, required super.child});
static ReachableData of(BuildContext ctx) {
final result = ctx.dependOnInheritedWidgetOfExactType<ReachableData>();
assert(result != null, 'Context error');
return result!;
}
#override
bool updateShouldNotify(ReachableData old) => false;
}
class ReachableDataState {
String? mount;
}
// route a
class Something extends StatefulWidget {
// If this widget needs to be disposed then use the other
// constructor and this call in the routes:
// Something(subscription: gen_nums().listen(null)),
// final StreamSubscription<int> subscription;
// Something({required this.subscription, super.key});
final Stream<int> stream;
Something({required this.stream, super.key});
#override
State<Something> createState() => _Something();
}
class _Something extends State<Something> {
int count = 0;
void increment_by(int i) => setState(
() => count += i,
);
#override
void initState() {
super.initState();
widget.stream.listen(increment_by);
// To avoid any funny errors you should set the subscription
// on pause or the callback to null on dispose
// widget.subscription.onData(increment_by);
}
#override
Widget build(BuildContext ctx) {
var mount = ReachableData.of(ctx).data.mount ?? 'No mount';
return Scaffold(
body: InkWell(
child: Text('[$count] Push Other / $mount'),
onTap: () {
ReachableData.of(ctx).data.mount = null;
Navigator.of(ctx).pushNamed(route_b);
},
),
);
}
}
// route b
class FillerRoute extends StatelessWidget {
const FillerRoute({super.key});
#override
Widget build(BuildContext ctx) {
return Scaffold(
body: InkWell(
child: Text('Go next'),
// Option 1: go to the next route
// onTap: () => Navigator.of(ctx).pushNamed(route_c),
// Option 2: go to the next route and extend the pop
onTap: () => Navigator.of(ctx)
.pushNamedAndRemoveUntil(route_c, ModalRoute.withName(route_a)),
),
);
}
}
// route c
class SetMount extends StatelessWidget {
const SetMount({super.key});
#override
Widget build(BuildContext ctx) {
return Scaffold(
body: InkWell(
child: Text('Set Mount'),
onTap: () {
ReachableData.of(ctx).data.mount = 'Mounted';
// Option 1: pop untill reaches the correct route
// Navigator.of(ctx).popUntil(ModalRoute.withName(route_a));
// Option 2: a regular pop
Navigator.of(ctx).pop();
},
),
);
}
}
I am having problems with a stateful widget in my flutter app which has an accept button to make an http request. I want it to be able to disable the button when pressed until the http request completes, but it doesn't seem to work when setting the state before the request.
I have put together a demo below with a dummy request method to simulate what is happening. When the button is pressed it isn't disabled until after the call to _acceptRequest completes. My expectation is for it to disable before that because set state is called before.
There are some examples of this on here, but it is not working for me and I seem to be doing the same thing as suggested on those posts.
import 'package:flutter/material.dart';
import 'dart:io';
class StatefulButtonDisable extends StatefulWidget {
const StatefulButtonDisable({ Key? key }) : super(key: key);
#override
_StatefulButtonDisableState createState() => _StatefulButtonDisableState();
}
class _StatefulButtonDisableState extends State<StatefulButtonDisable> {
bool _enableButton = true;
#override
Widget build(BuildContext context) {
Future<bool> _acceptRequest(id) async {
print('accpeting request $id');
sleep(Duration(seconds: 5));
return true;
}
_sendRequest(id) async {
setState(() {
_enableButton = false;
});
await _acceptRequest(id);
}
return Scaffold(
appBar: AppBar(
title: Text('Disable Button'),
),
body: Container(
child: ElevatedButton(
onPressed: _enableButton
? () { _sendRequest(7); }
: null,
child: Text('Press Me'),),
),
);
}
}
Future<bool> _acceptRequest(id) async {
print('accpeting request $id');
sleep(Duration(seconds: 5));
return true;
}
I know it returns a future and you have marked it as async, but this is not actually an asyncronous function. It does not do anything asynchonously. It does not use the await keyword. The sleep function will just completely block the currect thread, no questions asked, no prisoners taken.
The documentation for sleep says
Use this with care, as no asynchronous operations can be processed in a isolate while it is blocked in a sleep call.
If you want your application to work properly, the method marked async should use a way of waiting in line with the async/await model, for example Future.delayed:
Future<bool> _acceptRequest(id) async {
print('accpeting request $id');
await Future.delayed(Duration(seconds: 5));
return true;
}
I have noticed a new lint issue in my project.
Long story short:
I need to use BuildContext in my custom classes
flutter lint tool is not happy when this being used with aysnc method.
Example:
MyCustomClass{
final buildContext context;
const MyCustomClass({required this.context});
myAsyncMethod() async {
await someFuture();
# if (!mounted) return; << has no effect even if i pass state to constructor
Navigator.of(context).pop(); # << example
}
}
UPDATE: 17/September/2022
It appears that BuildContext will soon have a "mounted" property
So you can do:
if (context.mounted)
It basically allows StatelessWidgets to check "mounted" too.
Reference: Remi Rousselet Tweet
Update Flutter 3.7+ :
mounted property is now officially added to BuildContext, so you can check it from everywhere, whether it comes from a StatefulWidget State, or from a Stateless widget.
While storing context into external classes stays a bad practice, you can now check it safely after an async call like this :
class MyCustomClass {
const MyCustomClass();
Future<void> myAsyncMethod(BuildContext context) async {
Navigator.of(context).push(/*waiting dialog */);
await Future.delayed(const Duration(seconds: 2));
if (context.mounted) Navigator.of(context).pop();
}
}
// Into widget
#override
Widget build(BuildContext context) {
return IconButton(
onPressed: () => const MyCustomClass().myAsyncMethod(context),
icon: const Icon(Icons.bug_report),
);
}
// Into widget
Original answer
Don't stock context directly into custom classes, and don't use context after async if you're not sure your widget is mounted.
Do something like this:
class MyCustomClass {
const MyCustomClass();
Future<void> myAsyncMethod(BuildContext context, VoidCallback onSuccess) async {
await Future.delayed(const Duration(seconds: 2));
onSuccess.call();
}
}
class MyWidget extends StatefulWidget {
#override
_MyWidgetState createState() => _MyWidgetState();
}
class _MyWidgetState extends State<MyWidget> {
#override
Widget build(BuildContext context) {
return IconButton(
onPressed: () => const MyCustomClass().myAsyncMethod(context, () {
if (!mounted) return;
Navigator.of(context).pop();
}),
icon: const Icon(Icons.bug_report),
);
}
}
Use context.mounted*
In StatefulWidget/StatelessWidget or in any class that has BuildContext:
void foo(BuildContext context) async {
await someFuture();
if (!context.mounted) return;
Navigator.pop(context); // No warnings now
}
* If you're in a StatefulWidget, you can also use just mounted instead of context.mounted
If your class can extend from StatefulWidget then adding
if (!mounted) return;
would work!
EDIT
I had this issue again and again and here's the trick - use or declare variables using context before using async methods like so:
MyCustomClass{
const MyCustomClass({ required this.context });
final buildContext context;
myAsyncMethod() async {
// Declare navigator instance (or other context using methods/classes)
// before async method is called to use it later in code
final navigator = Navigator.of(context);
await someFuture();
// Now use the navigator without the warning
navigator.pop();
}
}
EDIT END
As per Guildem's answer, he still uses
if (!mounted) return;
so what's the point of adding more spaghetti code with callbacks? What if this async method will have to pass some data to the methods you're also passing context? Then my friend, you will have even more spaghetti on the table and another extra issue.
The core concept is to not use context after async bloc is triggered ;)
If you want to use mounted check in a stateless widget its possible by making an extension on BuildContext
extension ContextExtensions on BuildContext {
bool get mounted {
try {
widget;
return true;
} catch (e) {
return false;
}
}
}
and then you can use it like this
if (context.mounted)
Inspiration taken from GitHub PR for this feature and it passes the same tests in the merged PR
you can use this approach
myAsyncMethod() async {
await someFuture().then((_){
if (!mounted) return;
Navigator.of(context).pop();
}
});
In Flutter 3.7.0 BuildContext has the property mounted. It can be used both in StatelessWidget and StatefulWidgets like this:
void bar(BuildContext context) async {
await yourFuture();
if (!context.mounted) return;
Navigator.pop(context);
}
Just simpliy creat a function to call the navigation
void onButtonTapped(BuildContext context) {
Navigator.of(context).pop();
}
To avoid this in StatelessWidget you can refer to this example
class ButtonWidget extends StatelessWidget {
final String title;
final Future<String>? onPressed;
final bool mounted;
const ButtonWidget({
super.key,
required this.title,
required this.mounted,
this.onPressed,
});
#override
Widget build(BuildContext context) {
return Row(
children: [
const SizedBox(height: 20),
Expanded(
child: ElevatedButton(
onPressed: () async {
final errorMessage = await onPressed;
if (errorMessage != null) {
// This to fix: 'Do not use BuildContexts across async gaps'
if (!mounted) return;
snackBar(context, errorMessage);
}
},
child: Text(title),
))
],
);
}
}
I handle it with converting the function become not async and using then
Future<void> myAsyncMethod(BuildContext context) {
Navigator.of(context).push(/*waiting dialog */);
Future.delayed(const Duration(seconds: 2)).then(_) {
Navigator.of(context).pop();
});
}
just save your navigator or whatever needs a context to a variable at the beginning of the function
myAsyncMethod() async {
final navigator = Navigator.of(context); // 1
await someFuture();
navigator.pop(); // 2
}
DO NOT use BuildContext across asynchronous gaps.
Storing BuildContext for later usage can easily lead to difficult to diagnose crashes. Asynchronous gaps are implicitly storing BuildContext and are some of the easiest to overlook when writing code.
When a BuildContext is used from a StatefulWidget, the mounted property must be checked after an asynchronous gap.
So, I think, you can use like this:
GOOD:
class _MyWidgetState extends State<MyWidget> {
...
void onButtonTapped() async {
await Future.delayed(const Duration(seconds: 1));
if (!mounted) return;
Navigator.of(context).pop();
}
}
BAD:
void onButtonTapped(BuildContext context) async {
await Future.delayed(const Duration(seconds: 1));
Navigator.of(context).pop();
}
Flutter Hooks useEffect Docs
I dispatch an API request in my onSubmit event that has a side effect of turning signupHelper.state.success to true. I would like to navigate to another screen when success == true. Instead I get an error for setState() or markNeedsBuild() called during build
My current workaround is to wait 50 milliseconds before navigation to make sure there is no rebuild going on.
My code looks like this
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import '../hooks/use_signup_helper.dart'
class SignupForm extends HookWidget {
const SignupForm({Key key}) : super(key: key);
#override
Widget build(BuildContext context) {
// this wraps useReducer and gives me access to state and dispatch
final signupHelper = useSignupHelper();
useEffect(() {
if (signupHelper.state.success) {
// this is my workaround - delay 50 milliseconds to avoid rebuild conflict
// Future<void>.delayed(const Duration(milliseconds: 50))
// .then((_) => Navigator.pushNamed(context, '/home'));
Navigator.pushNamed(context, '/home'));
}
return null;
}, [signupHelper.state.success]);
return ... // UI goes here
I have same issues and I came across wrap the Navigator.push with Future.microtask
useEffect(() {
if (condition) {
Future.microtask(() async {
PageResult result = await Navigator.push(
context,
MaterialPageRoute(
builder: (context) => NextPage(),
),
);
if (result == null) {
Navigator.of(context).pop();
} else {
// ...
}
});
}
return;
}, [value]);
It looks like we can use the ScheduleBinding and SchedulerPhase class. It is imported like this -
import 'package:flutter/scheduler.dart';
And the new useEffect function looks like this -
useEffect(() {
if (signupHelper.state.success) {
if (SchedulerBinding.instance.schedulerPhase != SchedulerPhase.idle)
SchedulerBinding.instance.endOfFrame.then((_) {
Navigator.pushNamed(context, '/home');
});
else
Navigator.pushNamed(context, '/home');
}
return null;
}, [signupHelper.state.success]);
I have an API that returns content and I put this content in a GridView.builder to allow pagination.
I have architected the page in such a way that I have a FutureBuilder on a stateless widget and when the snapshot is done I then pass the snapshot data to a stateful widget to build the grid.
It is all working fine, however I want now to implement a functionality that allows me to reload the widget by placing a reload icon when snapshot has error and on click reloading widget. How can I accomplish this?
The following is my FutureBuilder on my Stateless widget:
return new FutureBuilder<List<Things>>(
future: apiCall(),
builder: (context, snapshot) {
if (snapshots.hasError)
return //Reload Icon
switch (snapshots.connectionState) {
case ConnectionState.waiting:
return Center(child: CircularProgressIndicator());
case ConnectionState.done:
return StatefulWidhet(things: snapshot.data);
default:
}
});
}
You'll need to lift the state up. The whole loading concept is abstracted by the FutureBuilder, but because you don't want to do one-time-loading, that's not the right abstraction layer for you. That means, you'll need to implement the "waiting for the future to complete and then build stuff" yourself in order to be able to trigger the loading repeatedly.
For example, you could put everything in a StatefulWidget and have isLoading, data and error properties and set these correctly.
Because this is probably a recurring task, you could even create a widget to handle that for you:
import 'package:flutter/material.dart';
class Reloader<T> extends StatefulWidget {
final Future<T> Function() loader;
final Widget Function(BuildContext context, T data) dataBuilder;
final Widget Function(BuildContext context, dynamic error) errorBuilder;
const Reloader({
Key key,
this.loader,
this.dataBuilder,
this.errorBuilder,
}) : super(key: key);
#override
State<StatefulWidget> createState() => ReloaderState<T>();
static of(BuildContext context) =>
context.ancestorStateOfType(TypeMatcher<ReloaderState>());
}
class ReloaderState<T> extends State<Reloader<T>> {
bool isLoading = false;
T data;
dynamic error;
#override
void initState() {
super.initState();
reload();
}
Future<void> reload() async {
setState(() {
isLoading = true;
data = null;
error = null;
});
try {
data = await widget.loader();
} catch (error) {
this.error = error;
} finally {
setState(() => isLoading = false);
}
}
#override
Widget build(BuildContext context) {
if (isLoading) {
return Center(child: CircularProgressIndicator());
}
return (data != null)
? widget.dataBuilder(context, data)
: widget.errorBuilder(context, error);
}
}
Then, you can just do
Reloader(
loader: apiCall,
dataBuilder: (context, data) {
return DataWidget(things: data);
},
errorBuilder: (context, error) {
return ...
RaisedButton(
onPressed: () => Reloader.of(context).reload(),
child: Text(reload),
),
...;
},
)
Also, I wrote a package for that case which has some more features built-in and uses a controller-based architecture instead of searching the state through Reload.of(context): flutter_cached
With it, you could just do the following:
In a state, create a CacheController (although you don't need to cache things):
var controller = CacheController(
fetcher: apiCall,
saveToCache: () {},
loadFromCache: () {
throw 'There is no cache!';
},
),
Then, you could use that controller to build a CachedBuilder in the build method:
CachedBuilder(
controller: controller,
errorScreenBuilder: (context, error) => ...,
builder: (context, items) => ...,
...
),
When the reload button is pressed, you can simply call controller.fetch(). And you'll also get some cool things like pull-to-refresh on top.