I have a stateful widget called AuthenticatingScreen where I'm trying to perform the following flow...
Output message letting the user know we are logging them in
Get user oAuth token (calls to service file)
Update the message to let the user know we are loading their details
Fetch the users details and redirect them away
The problem is that at step three, I'm rebuilding the state, which is in turn causing the build method to be fired again and calling the service again, which triggers an exception.
import 'package:flutter/material.dart';
import 'package:testing/services/auth_service.dart';
class AuthenticatingScreen extends StatefulWidget {
final String token;
AuthenticatingScreen(this.token);
#override
State<AuthenticatingScreen> createState() => _AuthenticatingScreenState();
}
class _AuthenticatingScreenState extends State<AuthenticatingScreen> {
// step 1) our default message
String _message = 'Please wait while we log you in...';
Future<void> _fetchUserDetails() {
return Future.delayed(const Duration(seconds: 3), () {
// ToDo: fetch user details from the server
});
}
#override
Widget build(BuildContext context) {
// step 2) get our oAuth token
AuthService.handleCallback(widget.token).then((accessCode) async {
// step 3) update our message
setState(() => _message = 'We\'re just getting your details');
// step 4) retrieve our user details and redirect away
_fetchUserDetails().then((_) {
Navigator.of(context).pushNamedAndRemoveUntil(
'/home',
(Route<dynamic> route) => false,
);
});
});
/// output our authenticating screen.
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Padding(
padding: EdgeInsets.only(bottom: 20.0),
child: CircularProgressIndicator(),
),
Text(_message),
],
),
),
);
}
}
My question being: How can I work around this / extract this logic to only fire when the widget is created, while still having access to the build context for navigation?
I've tried making the widget itself stateless and extracting the message and spinner into a separate widget, but changing the input argument alone still doesn't force a rebuild.
you can do it this way, i usually use getx & controller to achieve this.
separate the UI class & service class preferably in a controller
make the UI class statefull
call the API in onInit() method,as it called only once it will trigger the
service class
in API method when you get the result 200, initiate the UI transition
Ok, so I have figured out the solution. It seems making service calls within the build() method is a bad idea.
Moving my service calls into a void function which can then be called within the initState() method seems to be the way to go.
import 'package:flutter/material.dart';
import 'package:testing/screens/home.dart';
import 'package:testing/services/auth_service.dart';
class AuthenticatingScreen extends StatefulWidget {
final String token;
AuthenticatingScreen(this.token);
#override
State<AuthenticatingScreen> createState() => _AuthenticatingScreenState();
}
class _AuthenticatingScreenState extends State<AuthenticatingScreen> {
/// the default message to display to the user.
String _message = 'Please wait while we log you in...';
void _authenticateUser(String token) {
AuthService.handleCallback(widget.token).then((accessCode) async {
// we've got the users token, now we need to fetch the user details
setState(() => _message = 'We\'re just getting your details');
// after fetching the user details, push them to the home screen
_fetchUserDetails().then((_) {
Navigator.of(context).pushNamedAndRemoveUntil(
HomeScreen.name,
(Route<dynamic> route) => false,
);
});
});
}
Future<void> _fetchUserDetails() {
return Future.delayed(const Duration(seconds: 3), () {
// ToDo: fetch user details from the server
});
}
#override
void initState() {
super.initState();
_authenticateUser(widget.token);
}
#override
Widget build(BuildContext context) {
/// output our authenticating screen.
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Padding(
padding: EdgeInsets.only(bottom: 20.0),
child: CircularProgressIndicator(),
),
Text(_message),
],
),
),
);
}
}
And this way when the build() method is called again for the rebuild, very little details have to be redrawn.
Related
So I have this block of code in a widget that navigates to another screen:
screen_one.dart
class ScreenOne extends StatefulWidget {
const ScreenOne({ super.key });
#override
State<ScreenOne> createState() => _ScreenOneState();
}
class _ScreenOneState extends State<ScreenOne> {
List<String> state = [''];
#override
Widget build(BuildContext context) {
return Column(
MaterialButton(
onPressed: () => Navigator.pushNamed(context, '/screen-two'),
child: Text('Click here.')
),
Text(state[0]),
);
}
}
screen_two.dart
class ScreenTwo extends StatelessWidget {
const ScreenTwo({ super.key });
#override
Widget build(BuildContext context) {
return Container();
}
}
Basically I need to pass the state variable from ScreenOne to ScreenTwo and then update it there (in ScreenTwo)
ScreenTwo needs to display the same thing as ScreenOne and add() a new item to the state list when some button is clicked which should show on both the screens.
Its just one simple List so I am trying to avoid using provider.
Is it possible to do though?
I'm currently just passing it through the Navigator:
screen_one.dart
Navigator.pushNamed(
context,
'/post-info',
arguments: state,
),
screen_two.dart
Widget build(BuildContext context) {
final List<String> post = ModalRoute.of(context)!.settings.arguments as List<String>;
// ...
}
first I want to recommend you when things go bigger and more complex, it's better to use a state management approach, However since you did say that you have only one List you can simply use a ValueNotifier, with ValueListenableBuilder:
// this should be outside widget classes, maybe in a custom-made class or just in a global scope.
ValueNotifier stateNotifier = ValueNotifier([""]);
now in the places you want to use that state, you can use ValueListenableWidget like this:
ValueListenableBuilder(
valueListenable: stateNotifier,
builder: (context, value, child) {
return Column(
children: [
Text('${state[0]}'),
MaterialButton(
onPressed: () {
Navigator.pushNamed(context, '/screen-two'),
},
child: Text('click'),
),
],
);
},
);
}
}
and any other place where you want to see that state get updates, you need to use ValueListenableWidget.
Now, for executing a method like add() on the List and notify the widgets, you need to assign a new value for it like this:
void addInTheList(String elem) {
List current = stateNotifier.value;
current.add(elem);
// this exactly what will be responsible for updating.
stateNotifier.value = current;
}
now, you can just call addInTheList and expect it to update in all of them:
addInTheList("Example");
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();
},
),
);
}
}
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(),
),
],
),
),
);
}
}
I have defined the following cubit.
#injectable
class AuthCubit extends Cubit<AuthState> {
final IAuthService _authService;
AuthCubit(this._authService) : super(const AuthState.initial());
void authCheck() {
emit(_authService.signedInUser.fold(
() => AuthState.unauthenticated(none()),
(user) => AuthState.authenticated(user),
));
}
}
But the BlocListener which listens to this bloc is not getting invoked even after emit is called. But everything works as expected when I add a zero delay before the emit call.
Future<void> authCheck() async {
await Future.delayed(Duration.zero);
emit(_authService.signedInUser.fold(
() => AuthState.unauthenticated(none()),
(user) => AuthState.authenticated(user),
));
}
I tried out this delay because for other events which made some backend call (with some delay) emit worked perfectly. But I'm pretty sure this is not how it should work. Am I missing something here?
EDIT:
Adding the SplashPage widget code which uses BlocListener.
class SplashPage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return BlocListener<AuthCubit, AuthState>(
listener: (context, state) {
print(state);
},
child: Scaffold(
body: Center(
child: CircularProgressIndicator(),
),
),
);
}
}
Place where authCheck() is called,
class App extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MultiBlocProvider(
providers: [
BlocProvider<AuthCubit>(
create: (_) => getIt<AuthCubit>()..authCheck(),
),
],
child: MaterialApp(
....
),
);
}
}
and the AuthState is a freezed union
#freezed
abstract class AuthState with _$AuthState {
const factory AuthState.initial() = _Initial;
const factory AuthState.authenticated(User user) = _Authenticated;
const factory AuthState.unauthenticated(Option<AuthFailure> failure) = _Unauthenticated;
const factory AuthState.authInProgress() = _AuthInProgress;
}
Also, when I implemented a bloc (instead of Cubit) with the same functionality, everything worked as expected.
Without the delay the emit is called directly from the create method of the provider. This means that the listener is not (completely) built yet and thus there is no listener to be called when you emit the state.
So by adding the delay you allow the listener to subscribe to the stream first and thus it gets called when you emit the new state.
For me, the delay does not work perfectly. So I found this solution, maybe help someone:
#override
void initState() {
super.initState();
WidgetsBinding.instance?.addPostFrameCallback((_) async {
await myCubit.doSomethingFun();
});
}
And #Pieter is right, listener only be invoked when the widget is built.
I'm Building An Flutter Application which requires image changes after a period of time. I thought using while loop with a sleep method inside may solve the problem. But It didn't, Image is only getting change after the loop ends. Application UI also gets froze.
So, I used the async Task which I can't control with a Button.
Desired Output: Image should be changed after every 10 seconds and the user can pause or resume method execution.
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: Center(
child: Test(
),
),
)
);
}}
class Test extends StatefulWidget {
#override
_TestState createState() => _TestState();
}
class _TestState extends State<Test> {
int imgnumber=1;
int varToCheckButtonPress = 0;
String BtnTxt = "START";
void inc(){
while(imgnumber<10)
{
print(imgnumber);
await Future.delayed(const Duration(seconds: 10));
setState(() {
imgnumber++;
});
}
}
#override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
Expanded(flex: 1,
child: Container(
child: Image.asset('images/'+imgnumber.toString()+'.png'),
height: 500,
width:500,
color: Colors.green,
),
),
FlatButton(
child: Text(BtnTxt),
onPressed: (){
if (varToCheckButtonPress == 0) {
setState(() {
inc();
BtnTxt = 'PAUSE';
varToCheckButtonPress = 1;
});
} else if (varToCheckButtonPress == 1) {
setState(() {
BtnTxt = 'RESUME';
varToCheckButtonPress = 0;
});
}
},
)
],
);
}
}
I want the user to control the UI with a single button behave as START, PAUSE and RESUME.
Can we Use normal function To implement this functionality?
You should make use of Bloc pattern to manage your states, e.g: StreamBuilder, Providers, and make a timer to push new imageUrl to the sink and let the streamBuilder receive the latest imageUrl.
As for your button, all it controls is the timer. When u hit the play button, new imageUrl will keep pushing to the sink, while you press paused, simply stop the timer, and new image Url will not be pushing new imageUrl to the sink, and of course, reset the timer when you hit the stop button.
Here is a very detail Bloc pattern tutorial you can follow: Medium
The shortcut to achieve this is :
You can probably hold a function in async loop and call setState method on tap to change it's state.
For example :
call this function in desired location
while (_isPaused) {
await Future.delayed(Duration(milliseconds: 500));
}
and then call set state method from onTap, just like this
onTap:(){
setState((){
_isPaused? _isPaused=false: _isPaused=true;
});
}