Unable to Disable Button in Stateful Widget Until Request has Completed - flutter

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

Related

How to fetch and keep updated data with flutter_hooks?

I'm studying flutter_hooks library. My aim is to fetch data into model and be able to refresh it any time. Also I'd like to refresh data from different screens. Currently I do it this way:
Page class:
class NewPage extends HookWidget{
#override
Widget build(BuildContext context) {
final holder=useState<ModelHolder>(ModelHolder()).value;
final notifier=useListenable(holder.notifier);
final model=notifier.value;
final refresh=useState<bool>(false);
useEffect(() {
print('refetching');
holder.fetch();
},[refresh.value]);
print('model:$model, ${model.hashCode}');
return Scaffold(
appBar: AppBar(
title: Text('Page 2'),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
refresh.value=!refresh.value;//trigger refresh
},
child: const Icon(Icons.add),
),
);
}
}
ModelHolder class:
class ModelHolder {
final ValueNotifier<Model?> notifier = ValueNotifier<Model?>(null);
void fetch() async {
await Future.delayed(const Duration(seconds: 2));//do server call
notifier.value=Model('data');
}
}
Model class:
class Model{
final String value;
Model(this.value);
}
Basically this code works as expected. The only thing I'm confused is refresh variable in the NewPage. Due to this I can't extract hook's logic into custom hook. This is a typical task. I suspect that I'm inventing a wheel. What is the right way of fetching/updating data with flutter_hooks?
UPD: I moved refresh property into ModelHolder and now able to isolate everything in a custom hook.
holder:
class ModelHolder {
final ValueNotifier<bool> refresher;
final ValueNotifier<Model?> notifier = ValueNotifier<Model?>(null);
ModelHolder(this.refresher);
void fetch() async {
await Future.delayed(const Duration(seconds: 2));//do server call
notifier.value=Model('data');
}
}
Custom hook:
ModelHolder useModel(){
final refresh=useState<bool>(false);
final holder=useState(ModelHolder(refresh)).value;
useListenable(holder.notifier);
useEffect(() {
print('refetching');
holder.fetch();
},[refresh.value]);
return holder;
}
Usage:
Widget build(BuildContext context) {
final holder=useModel();
...
}
And I can pass holder to other routes to update it from there.

Do not use BuildContexts across async gaps

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: Dipose HTTP request on close controller

Original Answer
I'm using the Getx State Management on Flutter.
Simplifying as much as possible:
I build a GetxController to control my Page, and in this controller i have a StatefulWidget instance that evoque http requests.
class MyController extends GetxController {
Player player;
}
class Player extends StatefulWidget {
PlayerState state;
#override
PlayerState createState() {
state = PlayerState();
return state;
}
}
class PlayerState extends State<Player> {
void methodName async() {
futureRequest().then((data) {
// when the error ocurrs
setState(() {});
});
}
}
The problem occurs when the user closes the mobile page, triggering the controller's close method, before the end of the request.
That way, when setState is triggered, there is no more page instance and the error occurs.
I believe that the solution would be to interrupt all requests related to this GetxController and "delete" this instance of StatefulWidget at the moment the controller close method was called.
I don't know if this would be right, and if it's how to do it ..
==================================================================
Updated Answer
The main problem was that the async request in getDetails() method, return a response even after the controller is disposed, even using GetBuilder, and this response carried a url from a video that is started by the videoPlayerController (a video_player plugin instance).
So, the user is in another screen but keep listen to the video that is playing on background.
As a workaround and thinking in apply good practices to the code, i make a refactor to use only stateless widgets, following the GetX rules. I solved the problem, but i had to convert the Future's to Stream's
The binding is being created with Get.lazyPut() to perform dependencies injection:
class Binding implements Bindings {
Get.lazyPut<PlayerController>(() {
return PlayerController(videoRepository: VideoRepository(VideoProvider(Dio())));
});
}
This binding is linked to the page router, based on GetX documentation.
class AppPages {
static final routes = [
GetPage(name: Routes.MyRoute, page: () => MyPage(), binding: MyBinding()),
];
}
To prevent the controller to make actions even before it is disposed, i have to created a Stream and cancel it on controller dispose.
class MyController extends GetxController {
MyController({#required this.repository}) : assert(repository != null);
StreamSubscription<bool> stream;
// Instance of plugin video_player
VideoPlayerController videoPlayerController;
#override
void onClose() {
if (streamGetVideo != null) streamGetVideo.cancel();
super.onClose();
if (videoPlayerController != null) videoPlayerController?.dispose();
}
// This is the method called by the user on screen
void loadVideo() {
stream = getDetails().asStream().listen((bool response) {
// This code is canceled on onClose() method by the stream
if (response) update();
});
}
Future<bool> getDetails() async {
return await repository.getDetails().then((data) async {
videoPlayerController = VideoPlayerController.network(data);
initFuture = videoPlayerController.initialize();
await initFuture.whenComplete(() { return true; });
});
}
}
I think that Flutter/GetX should have a better way to do this, without these workarounds that i made. If anyone has a better approach or a hint, i'm open to suggestions.
One solution could be to wrap your setState with
if(mounted){
setState(() {});
}
GetBuilder + update()
In GetX using a GetBuilder with update() takes care of that lifecycle checking / handling so you don't have to do it.
Below is an example of a screen/route being closed prior to an HTTP call finishing & calling setState(), without an exception thrown.
(On the 2nd screen, click the Go Back! button fast to simulate an already disposed StatefulWidget.)
Below, an update() call is used to update the screen, instead of setState(), but they are the same in a GetBuilder. GetBuilder is (extends) a StatefulWidget.
GetBuilder adds listeners to the Controller you pass it, either through init: constructor arg or via the GetBuilder<Type> parameter if the Controller was initialized elsewhere/earlier.
That listener will be disposed if the StatefulWidget (i.e. GetBuilder) is disposed.
(See GetBuilder's dispose() function for some wizardry. While adding a listener, the returned value from adding that listener, is a function to dispose/unsubscribe from that listen. Pretty clever.)
So the GetBuilder/StatefulWidget will never have its update() / setState() called if that widget has been disposed because the listener for those calls has been disposed. So a slow returning HTTP call won't attempt to update/setState a widget that no longer exists in the widget tree.
import 'package:flutter/material.dart';
import 'package:get/get.dart';
class HttpX extends GetxController {
String slowValue = 'loading...';
#override
void onInit() {
slowCall();
}
/// Simulate a slow, long running HTTP call
Future<void> slowCall() async {
slowValue = 'Slow call STARTED!';
print(slowValue);
update(); // update the screen to show started message
await Future.delayed(Duration(seconds: 5), () {
slowValue = 'Slow call FINISHED!';
print(slowValue);
update(); // won't call setState() if GetBuilder is disposed
});
}
}
class GetXDisposePage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('GetX Dispose'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('awaiting http call to finish'),
RaisedButton(
child: Text('Go Call Page'),
onPressed: () => Get.to(SlowCallPage()),
// using Get.to ↑ requires GetMaterialApp in place of MaterialApp in MyApp
)
],
),
),
);
}
}
class SlowCallPage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('GetX Dispose - Go Back!'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
GetBuilder<HttpX>(
init: HttpX(), // fake slow http call starts on init
builder: (hx) => Text(hx.slowValue),
),
RaisedButton(
child: Text('Go Back!'),
onPressed: () => Get.back(),
),
],
),
),
);
}
}

Get a stream of permissions from a future

I'm trying to stream two types of location package based data:
final _location = Location();
runApp(
MultiProvider(
providers: [
StreamProvider<PermissionStatus>(create: (_) => _location.hasPermission().asStream()),
StreamProvider<bool>(create: (_) => _location.serviceEnabled().asStream()),
],
child: MaterialApp();
)
)
When I 'stream' the data, it loads the initial value and streams that. It is not continuously listening to changes which is what I want to do. I have tried abstracting both futures into their own class and creating an async* stream that yields the current status, both of which give the same problem.
My use case involves continuously listening to the permission status and location on/off and shut down certain parts of the UI when these are modified in between tasks.
Simplified usage:
class LocationWidget extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Consumer2<PermissionStatus, bool>(
builder: (_, permission, isLocationEnabled, __) => _LocationWidget(
permission: permission, isLocationEnabled: isLocationEnabled));
}
}
class _LocationWidget extends StatelessWidget {
const _LocationWidget({this.permission, this.isLocationEnabled})
: assert(permission != null);
final PermissionStatus permission;
final bool isLocationEnabled;
#override
Widget build(BuildContext context) {
return Container(
child: Center(
child: (() {
if (!isLocationEnabled) // Check Bool and show different text
return Text(
"Off",
);
else
return Text("On");
}())));
}
}
Add updateShouldNotify: (_, __) => true to your StreamProvider
By default, StreamProvider considers that the Stream listened uses immutable data. As such, it will not rebuild dependents if the previous and the new value are ==. To change this behavior, pass a custom updateShouldNotify.
ikerfah is right, you're creating a Stream from a Future, meaning the Stream will only contain a single event when the Future is completed (basically, it's not a real "stream" in the true sense of the word).
FutureBuilder won't work either, since the Future only gets completed once, so it will only trigger a single state change too.
If this is the plugin you're using, it seems the author hasn't implemented anything to expose a "real" Stream for permission change events. I wouldn't hold my breath for that either, because as far as I know neither iOS nor Android broadcast an event if/when permissions are changed.
If you need to disable/enable something based on whether permissions have changed, you'll just need to set a periodic Timer in a StatefulWidget to poll for changes.
class _LocationWidget extends StatefulWidget {
#override
State<StatefulWidget> createState() {
return _LocationWidgetState();
}
}
class _LocationWidgetState extends State<_LocationWidget> {
PermissionStatus permission;
bool isLocationEnabled = false;
Timer _timer;
#override
void initState() {
_timer = Timer.periodic(Duration(seconds:5), (_) {
var permission = await _location.hasPermission();
var isLocationEnabled = await _location.serviceEnabled();
if(permission != this.permission || isLocationEnabled != this.isLocationEnabled)
setState(() {});
});
}
#override
void dispose() {
_timer.cancel();
}
#override
Widget build(BuildContext context) {
return Container(
child: Center(
child: (() {
if (!isLocationEnabled) // Check Bool and show different text
return Text(
"Off",
);
else
return Text("On");
}())));
}
It's up to you whether 5 seconds is an appropriate interval. initState() should probably also set the initial isLocationEnabled/permission values when the state is initialized, too, rather than waiting 5 seconds for the timer to kick in.
I believe checking your permissions in didChangeApplifecycle would do the job, As the user most certainly had to put the app in the background to change the permissions
Example
#override
void didChangeAppLifecycleState(AppLifecycleState state) async {
final loc = Location();
final isPermitted = await loc.hasPermission();
final isServiceEnabled = await loc.serviceEnabled()
// request permissions
}

How to access data in Bloc's state from another bloc

I am developing a Flutter application using Bloc pattern. After success authentication, UserSate has User object. In all other Blocs, I need to access User object in UserState. I tried with getting UserBloc on other Bloc's constructor parameters and accessing User object. But it shows that User object is null. Anyone have a better solution?
class SectorHomeBloc extends Bloc<SectorHomeEvent, SectorHomeState> {
final OutletRepository outletRepository;
UserBloc userBloc;
final ProductRepository productRepository;
final ProductSubCategoryRepository productSubCategoryRepository;
final PromotionRepository promotionRepository;
final ProductMainCategoryRepository mainCategoryRepository;
SectorHomeBloc({
#required this.outletRepository,
#required this.userBloc,
#required this.productSubCategoryRepository,
#required this.productRepository,
#required this.promotionRepository,
#required this.mainCategoryRepository,
});
#override
SectorHomeState get initialState => SectorHomeLoadingState();
#override
Stream<SectorHomeState> mapEventToState(SectorHomeEvent event) async* {
try {
print(userBloc.state.toString());
LatLng _location = LatLng(
userBloc.state.user.defaultLocation.coordinate.latitude,
userBloc.state.user.defaultLocation.coordinate.longitude);
String _token = userBloc.state.user.token;
if (event is GetAllDataEvent) {
yield SectorHomeLoadingState();
List<Outlet> _previousOrderedOutlets =
await outletRepository.getPreviousOrderedOutlets(
_token, _location, event.orderType, event.sectorId);
List<Outlet> _featuredOutlets =
await outletRepository.getFeaturedOutlets(
_token, _location, event.orderType, event.sectorId);
List<Outlet> _nearestOutlets = await outletRepository.getOutletsNearYou(
_token, _location, event.orderType, event.sectorId);
List<Product> _newProducts = await productRepository.getNewItems(
_token, _location, event.orderType, event.sectorId);
List<Product> _trendingProducts =
await productRepository.getTrendingItems(
_token, _location, event.orderType, event.sectorId);
List<Promotion> _promotions = await promotionRepository
.getVendorPromotions(_token, event.sectorId);
yield SectorHomeState(
previousOrderedOutlets: _previousOrderedOutlets,
featuredOutlets: _featuredOutlets,
nearByOutlets: _nearestOutlets,
newItems: _newProducts,
trendingItems: _trendingProducts,
promotions: _promotions,
);
}
} on SocketException {
yield SectorHomeLoadingErrorState('could not connect to server');
} catch (e) {
print(e);
yield SectorHomeLoadingErrorState('Error');
}
}
}
The print statement [print(userBloc.state.toString());] in mapEventToState method shows the initial state of UserSate.
But, at the time of this code executing UserState is in UserLoggedInState.
UPDATE (Best Practice):
please refer to the answer here enter link description here
so the best way for that is to hear the changes of another bloc inside the widget you are in, and fire the event based on that.
so what you will do is wrap your widget in a bloc listener and listen to the bloc you want.
class SecondPage extends StatelessWidget {
const SecondPage({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return BlocListener<FirstBloc, FirstBlocState>(
listener: (context, state) {
if(state is StateFromFirstBloc){
BlocProvider.of<SecondBloc>(context).add(SecondBlocEvent());}//or whatever you want
},
child: ElevatedButton(
child: Text('THIS IS NEW SCREEN'),
onPressed: () {
BlocProvider.of<SecondBloC>(context).add(SecondBloCEvent());
},
),
);
}
}
the lovely thing about listener is that you can listen anywhere to any bloc and do whatever you want
here is the official documentation for it
OLD WAY (NOT Recommended)
there is an official way to do this as in the documentation, called Bloc-to-Bloc Communication
and here is the example for this as in the documentation
class MyBloc extends Bloc {
final OtherBloc otherBloc;
StreamSubscription otherBlocSubscription;
MyBloc(this.otherBloc) {
otherBlocSubscription = otherBloc.listen((state) {
// React to state changes here.
// Add events here to trigger changes in MyBloc.
});
}
#override
Future<void> close() {
otherBlocSubscription.cancel();
return super.close();
}
}
sorry for the late update for this answer and thanks to #MJ studio
The accepted answer actually has a comment in the above example in the official docs saying "No matter how much you are tempted to do this, you should not do this! Keep reading for better alternatives!"!!!
Here's the official doc link, ultimately one bloc should not know about any other blocs, add methods to update your bloc and these can be triggered from blocListeners which listen to changes in your other blocs: https://bloclibrary.dev/#/architecture?id=connecting-blocs-through-domain
class MyWidget extends StatelessWidget {
const MyWidget({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return BlocListener<WeatherCubit, WeatherState>(
listener: (context, state) {
// When the first bloc's state changes, this will be called.
//
// Now we can add an event to the second bloc without it having
// to know about the first bloc.
BlocProvider.of<SecondBloc>(context).add(SecondBlocEvent());
},
child: TextButton(
child: const Text('Hello'),
onPressed: () {
BlocProvider.of<FirstBloc>(context).add(FirstBlocEvent());
},
),
);
}
}