I have a Stop Watch timer in a Flutter Screen which works as required in the beginning inside a FutureBuilder but when I add another futurebuilder to add information from an API it keeps looping indefinitely:
Initially when I opened the screen it was looping indefinelty then when I adjusted the code it looped endlessly only when I clicked the Start button, it continued looping even after clicking the stop button.
Here is the dart file:
class Screen extends StatefulWidget {
#override
_ScreenState createState() => _ScreenState();
}
class _ScreenState extends State<Screen> {
bool isStartButtonDisabled = false;
bool isStopButtonDisabled = true;
Stopwatch _stopwatch = Stopwatch();
String _elapsedTime = "00:00:00";
late Timer _timer;
void _startStopwatch() {
print("void _startStopwatch() { Starting stopwatch");
_stopwatch.start();
setState(() {
isStartButtonDisabled = true;
isStopButtonDisabled = false;
});
_timer = Timer.periodic(
const Duration(seconds: 1),
(Timer timer) {
if (mounted) {
setState(() {
_elapsedTime = _stopwatch.elapsed.toString().split(".")[0];
});
}
},
);
}
#override
void dispose() {
_timer?.cancel();
super.dispose();
}
Here is ui
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: ListView(
children: [
Container(
child: Column(
children: [
GFButton(
onPressed: isStartButtonDisabled ? null : startWorkout,
text: "Start Workout",
),
Text(
_elapsedTime,
),
Card(
child: Padding(
padding: const EdgeInsets.all(18.0), //change here
child: Column(
children: [
FutureBuilder<List<Model_No_1>>(
future: futureModel_No_1,
builder: (BuildContext context,
AsyncSnapshot<List<Model_No_1>> snapshot) {
if (snapshot.hasData) {
return Column(
children: List.generate(snapshot.data!.length,
(int index) {
String Some_VariableName =
snapshot.data![index].name;
return Column(
children: [
Text(
snapshot.data![index].name,
),
Builder(builder: (context) {
return Container(
child: SingleChildScrollView(
child: Column(
children: [
Card(
child: Row(
children: [
Expanded(
child: FutureBuilder<
Get_Old_Model_No>(
future: () {
final Map<String,dynamic> arguments =ModalRoute.of(context)!.settings.arguments as Map<String,dynamic>;final int id =arguments['id'] ??
0;print("This is the id $id");return APIService.Get_Old_Model_No(id);}(),builder:(context, snapshot) {print("Snapshot data:${snapshot.data}");print("Snapshot error:${snapshot.error}");
if (snapshot.hasData) {
return Column(
children: [
// Text(snapshot
// .data
// ?.endDate),
],
);
} else if (snapshot
.hasError) {
return Text(
"${snapshot.error}");
}
return CircularProgressIndicator();
},),),],),),
for (var breakdown in snapshot.data![index].breakdowns)
Form(child: Expanded(child: Column(children: [TextFormField(
keyboardType:TextInputType.number,
onChanged:(value) {
final int?parsedValue =
int.tryParse(value);if (parsedValue !=
null) {setState(
() {variable1 =parsedValue;});} else {}
},),],),),),
When I add the Expanded, which runs the APIService.Get_Old_Model_No(id);
My question is why is the indefinete looping happening? How can I fix it ?
i cannot reproduce your problem from your provided code, but having multiple encapsuled futureBuilders shouldn't be a problem.
Any chance that the setState gets called from within one of your future(Builder)s?
That would trigger a rebuild, reloading the future and then setting the state again, triggering a rebuilt, and so on
When you call setState the build method is called, this triggers the FutureBulder to rebuild, thereby calling the future again.
What I see happening in your case is that once the _startStopwatch is called, the timer is started and it calls setState every second. I can't see your stop function, but it seems you are not canceling the Timer.periodic when you stop the stopWatch.
To solve your problem, you need to manage your state in such a way that only the necessary widget rebuild when the state changes. To achieve this you will need more than setState. See a simple example below using ValueNotifier and ValueListenableBuilder.
class Screen extends StatefulWidget {
#override
_ScreenState createState() => _ScreenState();
}
class _ScreenState extends State<Screen> {
ValueNotifier<bool> isStartButtonDisabled = ValueNotifier(false);
ValueNotifier<bool> isStopButtonDisabled = ValueNotifier(true);
Stopwatch _stopwatch = Stopwatch();
ValueNotifier<String> _elapsedTime = ValueNotifier("00:00:00");
late Timer _timer;
void _startStopwatch() {
print("void _startStopwatch() { Starting stopwatch");
_stopwatch.start();
isStartButtonDisabled.value = true;
isStopButtonDisabled.value = false;
_timer = Timer.periodic(
const Duration(seconds: 1),
(Timer timer) {
if (mounted) {
_elapsedTime.value = _stopwatch.elapsed.toString().split(".")[0];
}
},
);
}
#override
void dispose() {
_timer?.cancel();
super.dispose();
}
Now for the widgets that need to rebuild when the state changes, just use ValueListenableBuilder, See below.
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: ListView(
children: [
Container(
child: Column(
children: [
ValueListenableBuilder<bool>(valueListenable: isStartButtonDisabled, builder: (context, value, child)=> GFButton(
onPressed:value ? null : startWorkout,
text: "Start Workout",
)),
ValueListenableBuilder<String>(valueListenable: _elapsedTime, builder: (context, value, child)=> Text(
value,
)),
In this way when _elapsedTime changes, only the Text widget is rebuilt, similarly, when isStartButtonDisabled changes, only GFButton is rebuilt.
Also remember to cancel the Timer when you stop the StopWatch.
You are defining and calling a function in your build method with is getting called every time you set state, which is occuring at every tick of your timer.
() {
final Map<String,dynamic> arguments = ModalRoute.of(context)!.settings.arguments as Map<String,dynamic>;final
int id =arguments['id'] ?? 0;
print("This is the id $id");
return APIService.Get_Old_Model_No(id);
}()
Related
Firstly, I want to ask is from which method of Stateful widget should I call state management methods. I need to choice two place init() method or build(). I don't exactly know which method is the appropriate method to call state management methods. Let me try to explain with examples to understand of my question.
I use rxdart for dependency injection, used Stream and build with bloc pattern. Then used global scope instead of single scope. So I build another class that extend InheritedWidget and predefine each of the of(context) from the ancestor widget. Then called bloc methods (state management) from each build() method of their needed UI class (Stateful or stateless).
In here, my problem is when every time I called from build method, some of the function build again and again, unless I need to do. So I fix with another way, that is declare, initialized the bloc class and call respective bloc class function with object from init method of stateful class. That is work really. But some of the article said, don't should call from init method and only should call from build method. I am anxiety with that principles and calling from init method not work as global scope (Ex: that init stream not work for another widgets). How should I do with that? please tell or guide me with something.
Here is my code flow to better understanding. I show with count down example bloc that work like When count down become 0, the button will appear and press again that button count down again and work same process again.
That is state management bloc
class OtpLoginModuleBloc {
bool isHideResendButton = true;
final _buttonTimerController = PublishSubject<bool>();
final _textTimerController = PublishSubject<int>();
Stream<bool> get buttonTimerStream => _buttonTimerController.stream;
Stream<int> get textTimerStream => _textTimerController.stream;
void toAppearResendButtonCountown({required int second}) {
var duration = const Duration(seconds: 1);
Timer.periodic(
duration,
(Timer timer) {
if (second == 0) {
isHideResendButton = !isHideResendButton;
_buttonTimerController.sink.add(isHideResendButton);
timer.cancel();
} else {
second--;
}
_textTimerController.sink.add(second);
},
);
}
}
Here is ancestor class
class _PreModuleState extends State<PreModule> {
#override
Widget build(BuildContext context) {
return LoginModuleProvider(
child: OtpLoginModuleProivder(
child: MaterialApp(
theme: themeData(),
home: const Scaffold(
body: SplashScreen(),
),
onGenerateRoute: RouteGenerator.route,
),
),
);
}
}
Here is provider class for global scope
class OtpLoginModuleProivder extends InheritedWidget {
final OtpLoginModuleBloc loginOtpModuleBloc;
OtpLoginModuleProivder({Key? key, required Widget child})
: loginOtpModuleBloc = OtpLoginModuleBloc(),
super(key: key, child: child);
#override
bool updateShouldNotify(OtpLoginModuleProivder oldWidget) => true;
static OtpLoginModuleBloc of(context) {
return (context.dependOnInheritedWidgetOfExactType<OtpLoginModuleProivder>()
as OtpLoginModuleProivder)
.loginOtpModuleBloc;
}
}
And my problem is here ...
Should I call like that
final OtpLoginModuleBloc otpLoginModuleBloc = OtpLoginModuleBloc();
#override
void initState() {
otpLoginModuleBloc.toAppearResendButtonCountown(second: otpCountingTime);
super.initState();
}
#override
Widget build(BuildContext context) {
return Stack(
children: [
Column(
children: [
const OtpLoginHeader(),
const SizedBox(
height: margin50,
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: margin100),
child: buildPinCodeTextField(context, otpLoginModuleBloc),
),
const SizedBox(
height: margin30,
),
StreamBuilder(
initialData: otpCountingTime,
stream: otpLoginModuleBloc.textTimerStream,
builder: (context, AsyncSnapshot snapshot) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: margin80),
child: snapshot.data == 0
? sendOtpButton(otpLoginModuleBloc, otpCountingTime)
: Text('Request new OTP in ${snapshot.data}'),
);
},
),
],
),
],
);
}
OR
Widget build(BuildContext context) {
final otpLoginModuleBloc = OtpLoginModuleProivder.of(context);
otpLoginModuleBloc.toAppearResendButtonCountown(second: otpCountingTime);
return Stack(
children: [
Column(
children: [
const OtpLoginHeader(),
const SizedBox(
height: margin50,
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: margin100),
child: buildPinCodeTextField(context, otpLoginModuleBloc),
),
const SizedBox(
height: margin30,
),
StreamBuilder(
initialData: otpCountingTime,
stream: otpLoginModuleBloc.textTimerStream,
builder: (context, AsyncSnapshot snapshot) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: margin80),
child: snapshot.data == 0
? sendOtpButton(otpLoginModuleBloc, otpCountingTime)
: Text('Request new OTP in ${snapshot.data}'),
);
},
),
],
),
],
);
}
That is my all of my question...
Please help me and guide me ...
Ofc you should go for the latter.
use like this. This way, you can use _otpLoginModuleBloc within the class.
class _ExampleWidgetState extends State<ExampleWidget> {
late OtpLoginModuleBloc _otpLoginModuleBloc
#override
Widget build(BuildContext context) {
_otpLoginModuleBloc = OtpLoginModuleProivder.of(context);
return Container();
}
}
In addition, if you need to run the otpLoginModuleBloc.toAppearResendButtonCountown(second: otpCountingTime); only ONCE, then use below.
class ExampleWidget extends StatefulWidget {
const ExampleWidget({super.key});
#override
State<ExampleWidget> createState() => _ExampleWidgetState();
}
class _ExampleWidgetState extends State<ExampleWidget> {
late OtpLoginModuleBloc _otpLoginModuleBloc
final _otpCountingTime = 1;
#override
void initState() {
WidgetsBinding.instance.addPostFrameCallback((_) {
_otpLoginModuleBloc.toAppearResendButtonCountown(second: _otpCountingTime);
});
super.initState();
}
#override
Widget build(BuildContext context) {
_otpLoginModuleBloc = OtpLoginModuleProivder.of(context);
return Container();
}
}
This should answer your questions. Thank you.
Oh and don't forget to add dispose method inside the OtpLoginModuleBloc that cancels Timer and closes both _buttonTimerController and _textTimerController in case you don't need the bloc anymore.
I am trying to implement bloc pattern in which I am using a repository class which consist all the methods which makes the calls with the API. On the other side I am implementing BlocBuilder to render the view based on bloc state however i am getting this error BlocBuilder<VehiclesBloc, VehiclesState>(dirty, dependencies: [_LocalizationsScope-[GlobalKey#df8d0]], state: _BlocBuilderBaseState<VehiclesBloc, VehiclesState>#dba40):
type 'Future' is not a subtype of type 'Widget?'
I am really not sure where the issues comes from. here are some snippets of the code.
this is the bloc class which causes the error
class VehiclesBloc extends Bloc<VehiclesEvent,VehiclesState>{
VehiclesBloc(VehiclesState initialState) : super(initialState);
#override
Stream<VehiclesState> mapEventToState(VehiclesEvent event) async* {
// TODO: implement mapEventToState
if(event is LoadVehiclesList){
yield* mapLoadEventToState(event);
}
}
Stream<VehiclesState> mapLoadEventToState(LoadVehiclesList event) async* {
if(event is LoadVehiclesList){
var response = await VehiclesService().getAll();
if(response.IsSuccess){
yield VehiclesLoaded(response.Data);
}else{
yield VehiclesLoadingFailed(response.ErrorList.toString());
}
}else{
yield VehiclesLoading();
}
}
}
here is the Statefull widget which implements the Bloc Builder
class VehicleList extends StatefulWidget {
const VehicleList({Key key}) : super(key: key);
static const String routeName = "/VehicleList";
//final ScrollController scrollController;
#override
_VehicleListState createState() => _VehicleListState();
}
class _VehicleListState extends State<VehicleList> {
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
VehiclesBloc vehiclesBloc =
VehiclesBloc(VehiclesLoading())..add(LoadVehiclesList());
#override
void initState() {
// TODO: implement initState
super.initState();
//VehiclesService().getAll();
}
#override
void dispose() {
// TODO: implement dispose
vehiclesBloc.close();
super.dispose();
}
#override
Widget build(BuildContext context) {
final isRtl = context.locale.languageCode == "ar";
return Scaffold(
key: _scaffoldKey,
backgroundColor: kBackgroundColor,
drawer: SideNavigationDrawer(),
body: Container(
child: Column(
children: [
SizedBox(
height: 15,
),
Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
IconButton(
onPressed: () {
_scaffoldKey.currentState.openDrawer();
},
icon: Icon(
Icons.menu,
size: 35,
color: Colors.black,
),
)
],
),
Expanded(
child: SingleChildScrollView(
child: Column(
children: [
BlocBuilder<VehiclesBloc,VehiclesState>(
builder: (context, state) {
if (state is VehiclesLoaded) {
// return BuildListVehicle(state.lsVehicle);
return Center();
} else if (state is VehiclesLoadingFailed) {
return Center(
child: CustomErrorWidget(),
);
} else {
return Center(
child: LoadingDialog.showLoadingDialog(context,
text: ""),
);
}
},
cubit: vehiclesBloc,
),
],
),
),
)
],
),
));
}
I think this code part causes the problem:
return Center(
child: LoadingDialog.showLoadingDialog(context,text: ""),
);
Possibly, LoadingDialog.showLoadingDialog does not return a Widget but is just a function that returns Future.
For side effects (e.g. you want to show the dialog), you should use listeners instead of executing such code inside the build method. Instead of BlocBuilder, just use BlocConsumer and add the listener:
BlocConsumer<VehiclesBloc,VehiclesState>(
listener: (context, state) {
if (state is {your loading state}) {
LoadingDialog.showLoadingDialog(context, text: "");
}
},
builder: ...,
),
Some more insights about your code:
Instead of creating BLoC as a variable in your stateful widget, use BlocProvider that would handle create/dispose part of your BLoC.
Yield the VehiclesLoading state before loading the data and not just as an "else" case. This way you could handle the loading behaviour easily in your UI.
To fix the above issues, just follow the documentation: https://bloclibrary.dev/
the error is thrown in two areas (and the app freezes (when the app is minimized, when phones back button is clicked, or when another app runs on top of the flutter app. Flutter version: 1.20.2 (previous versions did not have this issue): The two functions are:
#override
void initState() {
super.initState();
getItems();
}
getItems() async {
initClearVisibility();
initFilters();
setState(() {
loadingItems = true;
Visibility(visible: true, child: CircularProgressIndicator());
});
QuerySnapshot querySnapshot = await query.get();
items = querySnapshot.docs;
lastDocument = querySnapshot.docs[querySnapshot.docs.length - 1];
setState(() {
loadingItems = false;
Visibility(visible: false, child: CircularProgressIndicator());
});
}
initClearVisibility() {
if (Str.filterSelectCategory != Str.CATEGORY) {
clearCategoryVisible = true;
allCategoriesVisible = false;
categoryValue = Str.filterSelectCategory;
setState(() {});
}
}
initFilters() async {
filterDefaultItems();
}
filterDefaultItems() async {
query = _firestore
.collection(Str.ITEMS)
.where(Str.IS_ITEM_SOLD, isEqualTo: false)
.where(Str.ADDRESS, isEqualTo: userAddress1)
//.orderBy(Str.DATE_POSTED)
.limit(perPage);
}
Second area is on the following code where I am also getting: :
class FABBottomAppBarItem {
FABBottomAppBarItem({this.iconData, this.itemColor}); //, this.text});
IconData iconData;
var itemColor;
//String text;
}
class FABBottomAppBar extends StatefulWidget {
FABBottomAppBar({
this.items,
this.centerItemText,
this.height: 65.0,
this.iconSize: 24.0,
this.backgroundColor,
this.color,
this.selectedColor,
this.notchedShape,
this.onTabSelected,
}) {
assert(this.items.length == 2 || this.items.length == 4);
}
final List<FABBottomAppBarItem> items;
final String centerItemText;
final double height;
final double iconSize;
final Color backgroundColor;
final Color color;
final Color selectedColor;
final NotchedShape notchedShape;
final ValueChanged<int> onTabSelected;
#override
State<StatefulWidget> createState() => FABBottomAppBarState();
}
class FABBottomAppBarState extends State<FABBottomAppBar> {
//int _selectedIndex = 0;
int unreadCount = 0;
_updateIndex(int index) {
widget.onTabSelected(index);
setState(() {
//_selectedIndex = index;
});
}
#override
void initState() {
super.initState();
countDocuments();
}
#override
Widget build(BuildContext context) {
List<Widget> items = List.generate(widget.items.length, (int index) {
return _buildTabItem(
item: widget.items[index],
index: index,
onPressed: _updateIndex,
);
});
items.insert(items.length >> 1, _buildMiddleTabItem());
return BottomAppBar(
shape: widget.notchedShape,
child: Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: items,
),
color: widget.backgroundColor,
);
}
Widget _buildMiddleTabItem() {
return Expanded(
child: SizedBox(
height: MediaQuery.of(context).size.height * 0.075, //widget.height,
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
SizedBox(
height: MediaQuery.of(context).size.height * 0.04,
),
Text(
widget.centerItemText ?? '',
style: TextStyle(
color: BwerereTheme.bwerereRed,
fontSize: 14.0,
fontWeight: FontWeight.w900),
),
],
),
),
);
}
Widget _buildTabItem({
FABBottomAppBarItem item,
int index,
ValueChanged<int> onPressed,
})
{
return Expanded(
child: SizedBox(
height: MediaQuery.of(context).size.height * 0.065,
child: Material(
type: MaterialType.transparency,
child: InkWell(
onTap: () => onPressed(index),
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Stack(
children: <Widget>[
Icon(item.iconData,
color: item.itemColor,
size: IconTheme.of(context).size * 1.2),
index == 2 ? badge() : Container()
],
)
],
),
),
),
),
);
}
Widget badge() => unreadCount < 1
? Container()
: Container(
padding: EdgeInsets.all(4.0),
decoration: BoxDecoration(
color: BwerereTheme.bwerereRed, shape: BoxShape.circle),
child: Center(
child: RobotoFont(
text: "$unreadCount",
textSize: 12.0,
textColor: Colors.white,
fontWeight: FontWeight.w400),
));
void countDocuments() async {
final uid = await FetchUserData().getCurrentUserID();
QuerySnapshot _myDoc = await FirebaseFirestore.instance
.collection("userUnreadMessages")
.doc(uid)
.collection(Str.MESSAGE_COLLECTION)
.get();
List<DocumentSnapshot> _myDocCount = _myDoc.docs;
setState(() {
unreadCount = _myDocCount.length;
print('NOTIY LENGTH::: $unreadCount');
});
}
}
THE ERROR FROM FRAMEWORK.DART for FABBottomAppBarState.
The same error thrown on the getItems on HomePage()
Exception has occurred.
FlutterError (setState() called after dispose(): FABBottomAppBarState#250ac(lifecycle state: defunct, not mounted)
This error happens if you call setState() on a State object for a widget that no longer appears in the widget tree (e.g., whose parent widget no longer includes the widget in its build). This error can occur when code calls setState() from a timer or an animation callback.
The preferred solution is to cancel the timer or stop listening to the animation in the dispose() callback. Another solution is to check the "mounted" property of this object before calling setState() to ensure the object is still in the tree.
This error might indicate a memory leak if setState() is being called because another object is retaining a reference to this State object after it has been removed from the tree. To avoid memory leaks, consider breaking the reference to this object during dispose().)
Further investigation then shows that the app takes about 400MB of memory (Ram) for the phone which I find rather too high.
Help on figuring out the issue will really help. Thanks in advance.
Additional information:
Error occurs when running on android 7.0, flutter 1.20.2. See similar/related issue on https://github.com/flutter/flutter/issues/35900. Note that I upgraded to Flutter 1.20.2 adnd Downgrading to 1.7.5 will require a lot of changes I made after upgrading especially on Firestore (NOTE: https://firebase.flutter.dev/docs/migration which was recently updated).
After an await, your widget may not be mounted anymore. Doing setState gives you an exception at that time. This is actually a good thing, the code that follows should not be executing anyway, since you are somewhere else.
You have three options about the "setState() called after dispose()" exception:
Safely ignore it. The exception is saving your async function from continuing. You will see an exception in your logs that you can just ignore.
Place a if (!mounted) return; between each await and setState(). It may be a good habit to put it after each await. This also stops the async function and hides the exception, if you are allergic to it.
Replace your setState() calls with setStateIfMounted() and define it as:
void setStateIfMounted(f) {
if (mounted) setState(f);
}
However, if (mounted) setState() does not stop the async function, so this 3rd option is the worst between the three as discussed here.
I also explain these approaches in this video.
You can use:
if (this.mounted) { // check whether the state object is in tree
setState(() {
// make changes here
});
}
The mounted checks whether Whether this State object is currently in a tree.
mounted class
For beginners and easy to understand check bool isMount = true; when dispose() would be called bool isMount = false; and setState() won't be called.
class TotalBooks extends StatefulWidget {
TotalBooks({Key? key}) : super(key: key);
// code omitted..
bool isMount = true;
#override
_TotalBooksState createState() => _TotalBooksState();
}
class _TotalBooksState extends State<TotalBooks> {
#override
void initState() {
// code omitted..
if (widget.isMount) {
setState(() {
// code omitted..
});
}
});
super.initState();
}
#override
void dispose() {
// TODO: implement dispose
widget.isMount = false;
// code omitted.. super.dispose();
}
#override
Widget build(BuildContext context) {
return SizedBox();
}
}
#override
void setState(VoidCallback fn) {
if (!mounted) return;
super.setState(fn);
}
I found there is no that perfect and easy way! I have written a subclass extends the State, you can use VMState instead of State, then just call safeSetState instead of setState.
import 'package:flutter/widgets.dart';
class VMState<T extends StatefulWidget> extends State<T> {
bool didDispose = false;
#override
Widget build(BuildContext context) {
// TODO: implement build
throw UnimplementedError();
}
#override
void dispose() {
didDispose = true;
super.dispose();
}
void safeSetState(VoidCallback callback) {
if (!didDispose) {
setState(callback);
}
}
}
Someone says to use mounted but that brings another exception.
With this code, my error has been solved!
if (mounted) {
setState(() {
// make your changes here
});
}
Im developing a app that when the user enters the first screen is a loading screen where initialize somethings from the Provider, the user cant input anything in this screen, when the loading finishes i want to push a new screen without the user "clicking" for it.
In this code, im actually given a delay of 3 seconds for the _login.getStoredEmail() to run and set a variable inside LoginController which in the next screen i consume, but of course this wont work everytime, eventually will break.
class GeneralSplashScreen extends StatefulWidget {
#override
_GeneralSplashScreenState createState() => _GeneralSplashScreenState();
}
class _GeneralSplashScreenState extends State<GeneralSplashScreen> {
#override
void initState() {
Future.delayed(
Duration(
seconds: 3,
),
() {
Navigator.pushReplacementNamed(context, kRoutes.login);
},
);
super.initState();
}
#override
void dispose() {
super.dispose();
}
#override
Widget build(BuildContext context) {
final LoginController _login = Provider.of<LoginController>(context);
_login.getStoredEmail();
return Scaffold(
body: Padding(
padding: const EdgeInsets.symmetric(horizontal: 60),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
Image.asset(
'lib/assets/images/logo.png',
fit: BoxFit.contain,
),
Text(
"Business Mananger",
textAlign: TextAlign.end,
style: TextStyle(
fontFamily: kFontFamily.montserrat,
fontSize: 10,
),
),
LoadingBar(),
],
),
),
);
}
}
Navigate to a different screen inside the initState() method.
initState() {
initializeAndNavigate()
}
initializeAndNavigate() async {
await initializeSomething();
Navigator.of(context) ...
}
Declare initState
Future _future;
#override
void initState() {
// TODO: implement initState
super.initState();
_future = doStuff();
}
Use FutureBuilder
FutureBuilder(
future: _future,
builder: (_, dataSnapshot) {
if (dataSnapshot.connectionState == ConnectionState.waiting) {
return Center(child: CircularProgressIndicator()); // here add loading screen
} else {
return Dashboard();
}
},
)
How to solve the exception -
Unhandled Exception: 'package:flutter/src/widgets/page_view.dart': Failed assertion: line 179 pos 7: 'positions.isNotEmpty': PageController.page cannot be accessed before a PageView is built with it.
Note:- I used it in two screens and when I switch between screen it shows the above exception.
#override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) => _animateSlider());
}
void _animateSlider() {
Future.delayed(Duration(seconds: 2)).then(
(_) {
int nextPage = _controller.page.round() + 1;
if (nextPage == widget.slide.length) {
nextPage = 0;
}
_controller
.animateToPage(nextPage,
duration: Duration(milliseconds: 300), curve: Curves.linear)
.then(
(_) => _animateSlider(),
);
},
);
}
I think you can just use a Listener like this:
int _currentPage;
#override
void initState() {
super.initState();
_currentPage = 0;
_controller.addListener(() {
setState(() {
_currentPage = _controller.page.toInt();
});
});
}
I don't have enough information to see exactly where your problem is, but I just encountered a similar issue where I wanted to group a PageView and labels in the same widget and I wanted to mark active the current slide and the label so I was needing to access controler.page in order to do that. Here is my fix :
Fix for accessing page index before PageView widget is built using FutureBuilder widget
class Carousel extends StatelessWidget {
final PageController controller;
Carousel({this.controller});
/// Used to trigger an event when the widget has been built
Future<bool> initializeController() {
Completer<bool> completer = new Completer<bool>();
/// Callback called after widget has been fully built
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
completer.complete(true);
});
return completer.future;
} // /initializeController()
Widget build(BuildContext context) {
return Stack(
children: <Widget>[
// **** FIX **** //
FutureBuilder(
future: initializeController(),
builder: (BuildContext context, AsyncSnapshot<void> snap) {
if (!snap.hasData) {
// Just return a placeholder widget, here it's nothing but you have to return something to avoid errors
return SizedBox();
}
// Then, if the PageView is built, we return the labels buttons
return Column(
children: <Widget>[
CustomLabelButton(
child: Text('Label 1'),
isActive: controller.page.round() == 0,
onPressed: () {},
),
CustomLabelButton(
child: Text('Label 2'),
isActive: controller.page.round() == 1,
onPressed: () {},
),
CustomLabelButton(
child: Text('Label 3'),
isActive: controller.page.round() == 2,
onPressed: () {},
),
],
);
},
),
// **** /FIX **** //
PageView(
physics: BouncingScrollPhysics(),
controller: controller,
children: <Widget>[
CustomPage(),
CustomPage(),
CustomPage(),
],
),
],
);
}
}
Fix if you need the index directly in the PageView children
You can use a stateful widget instead :
class Carousel extends StatefulWidget {
Carousel();
#override
_HomeHorizontalCarouselState createState() => _CarouselState();
}
class _CarouselState extends State<Carousel> {
final PageController controller = PageController();
int currentIndex = 0;
#override
void initState() {
super.initState();
/// Attach a listener which will update the state and refresh the page index
controller.addListener(() {
if (controller.page.round() != currentIndex) {
setState(() {
currentIndex = controller.page.round();
});
}
});
}
#override
void dispose() {
controller.dispose();
super.dispose();
}
Widget build(BuildContext context) {
return Stack(
children: <Widget>[
Column(
children: <Widget>[
CustomLabelButton(
child: Text('Label 1'),
isActive: currentIndex == 0,
onPressed: () {},
),
CustomLabelButton(
child: Text('Label 2'),
isActive: currentIndex == 1,
onPressed: () {},
),
CustomLabelButton(
child: Text('Label 3'),
isActive: currentIndex == 2,
onPressed: () {},
),
]
),
PageView(
physics: BouncingScrollPhysics(),
controller: controller,
children: <Widget>[
CustomPage(isActive: currentIndex == 0),
CustomPage(isActive: currentIndex == 1),
CustomPage(isActive: currentIndex == 2),
],
),
],
);
}
}
This means that you are trying to access PageController.page (It could be you or by a third party package like Page Indicator), however, at that time, Flutter hasn't yet rendered the PageView widget referencing the controller.
Best Solution: Use FutureBuilder with Future.value
Here we just wrap the code using the page property on the pageController into a future builder, such that it is rendered little after the PageView has been rendered.
We use Future.value(true) which will cause the Future to complete immediately but still wait enough for the next frame to complete successfully, so PageView will be already built before we reference it.
class Carousel extends StatelessWidget {
final PageController controller;
Carousel({this.controller});
Widget build(BuildContext context) {
return Stack(
children: <Widget>[
FutureBuilder(
future: Future.value(true),
builder: (BuildContext context, AsyncSnapshot<void> snap) {
//If we do not have data as we wait for the future to complete,
//show any widget, eg. empty Container
if (!snap.hasData) {
return Container();
}
//Otherwise the future completed, so we can now safely use the controller.page
return Text(controller.controller.page.round().toString);
},
),
//This PageView will be built immediately before the widget above it, thanks to
// the FutureBuilder used above, so whenever the widget above is rendered, it will
//already use a controller with a built `PageView`
PageView(
physics: BouncingScrollPhysics(),
controller: controller,
children: <Widget>[
AnyWidgetOne(),
AnyWidgetTwo()
],
),
],
);
}
}
Alternatively
Alternatively, you could still use a FutureBuilder with a future that completes in addPostFrameCallback in initState lifehook as it also will complete the future after the current frame is rendered, which will have the same effect as the above solution. But I would highly recommend the first solution as it is straight-forward
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
//Future will be completed here
// e.g completer.complete(true);
});
use this widget and modify it as you want:
class IndicatorsPageView extends StatefulWidget {
const IndicatorsPageView({
Key? key,
required this.controller,
}) : super(key: key);
final PageController controller;
#override
State<IndicatorsPageView> createState() => _IndicatorsPageViewState();
}
class _IndicatorsPageViewState extends State<IndicatorsPageView> {
int _currentPage = 0;
#override
void initState() {
widget.controller.addListener(() {
setState(() {
_currentPage = widget.controller.page?.toInt() ?? 0;
});
});
super.initState();
}
#override
void dispose() {
widget.controller.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: List.generate(
3,
(index) => IndicatorPageview(isActive: _currentPage == index, index: index),
),
);
}
}
class IndicatorPageview extends StatelessWidget {
const IndicatorPageview({
Key? key,
required this.isActive,
required this.index,
}) : super(key: key);
final bool isActive;
final int index;
#override
Widget build(BuildContext context) {
return Container(
margin: const EdgeInsets.only(right: 8),
width: 16,
height: 16,
decoration: BoxDecoration(color: isActive ?Colors.red : Colors.grey, shape: BoxShape.circle),
);
}
}