FlutterError (setState() called after dispose(): (lifecycle state: defunct, not mounted) - flutter

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

Related

Flutter Looping Endlessly when adding a FutureBuilder Inside another FutureBuilder

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

Is it okay to use an onPressed function in a ChangeNotifier child class

I am using provider for state management, and I've given onTap a value in a function in the ChangeNotifier child class but my app is unresponsive, I mean, when i tap on the widget, it doesn't update state, however, it does change the values i need it to change tho, i know this coz I am debugPrinting in the onTap function and when i tap, it actually prints that the button got tapped, but state doesn't update, widget remains the same until i hot restart, then it updates everything, even hot reload doesn't update it, here's the function
class Storage extends ChangeNotifier{
static const _storage = FlutterSecureStorage();
static const _listKey = 'progress';
List _dataMaps = [];
List<DayTile> dayTileMain = [];
void createDataMap() {
for (int i = 1; i < 101; i++) {
final data = Data(number: i).toJson();
_dataMaps.add(data);
}
}
void createDayTiles() {
for(Map<String, dynamic> data in _dataMaps) {
bool isDone = data['i'];
final dayTile = DayTile(
number: data['n'],
isDone: isDone,
// This is where i need to rebuild the tree
onTap: () async {
data['i'] = true;
notifyListeners();
print(data['i']);
print(isDone);
await writeToStorage();
},
);
dayTileMain.add(dayTile);
}
print('data tiles created');
}
}
and here is the DayTile class
class DayTile extends StatelessWidget {
const DayTile({
Key? key,
required this.number,
required this.isDone,
required this.onTap,
}) : super(key: key);
final int number;
final VoidCallback onTap;
final bool isDone;
#override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
child: Container(
height: 50,
width: MediaQuery.of(context).size.width * .15,
decoration: BoxDecoration(
color: !isDone
? const Color(0xffedecea)
: const Color(0xffedecea).withOpacity(0.1),
borderRadius: BorderRadius.circular(5),
),
child: Center(
child: Stack(
alignment: Alignment.center,
children: [
Center(
child: Text(
number.toString(),
style: const TextStyle(
color: Color(0xff576aa4),
),
),
),
Visibility(
visible: isDone,
child: const Divider(
color: Colors.black,
),
),
],
),
),
),
);
}
}
here is where I listen for the change
Wrap(
spacing: 13,
runSpacing: 13,
children: Provider.of<Storage>(context).dayTileMain,
),
when data['i'] is true, it should update the current instance of DayTile() that it's on in the loop, and in DayTile() I use the value of data['i'] to set the value of bool isDone and depending on whether isDone is true or false, the color of the widget changes and some other things, BUT, they don't change onTap, but they change after I hot restart, when it's read the storage and restored the saved data, could the secureStorage writing to storage at the same time be affecting it?
I solved it, turns out it's not a good idea to listen for events in the model class, it won't listen, so instead of generating the list of widgets in the model class, I moved it outta there, and instead generated it inside the wrap widget, and instead of listening for a list in the model class, i just had the list there in my wrap, if it was a listview i was tryna generated, i would've done this initially with a ListView.builder() i didn't know you could generate a list inside the children of the wrap widget, so i just stuck to defining it in the model, I came across this stack question Flutter: How to use Wrap instead of ListView.builder?
and that's how i knew how to build widgets inside a children property, i was actually just looking for a ListView.builder() version for the Wrap widget, all said, this is what my stuff is looking like
Model
class Storage extends ChangeNotifier {
static const _storage = FlutterSecureStorage();
static const _listKey = 'progress';
List _dataMaps = [];
List<DayTile> dayTileMain = [];
void createDataMap() {
for (int i = 1; i < 101; i++) {
final data = Data(number: i).toJson();
_dataMaps.add(data);
}
}
int get listLength {
return _dataMaps.length;
}
UnmodifiableListView get dataMaps {
return UnmodifiableListView(_dataMaps);
}
void pressed(Map<String, dynamic> map) async {
map['i'] = true;
await writeToStorage();
notifyListeners();
}
Future writeToStorage() async {
final value = json.encode(_dataMaps);
await _storage.write(key: _listKey, value: value);
}
Future<void> getTasks() async {
print('getTasks called');
final value = await _storage.read(key: _listKey);
final taskList = value == null ? null : List.from(jsonDecode(value));
if (taskList == null) {
print('getTasks is null');
createDataMap();
// createDayTiles();
} else {
print('getTasks is not null');
print(taskList);
_dataMaps = taskList;
// createDayTiles();
}
}
Future readFromStorage() async {
await getTasks();
notifyListeners();
}
}
Wrap Builder
class DayTiles extends StatelessWidget {
const DayTiles({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Consumer<Storage>(
builder: (_, storageData, __) => Wrap(
spacing: 13,
runSpacing: 13,
children: [
for(Map<String, dynamic> data in storageData.dataMaps)
DayTile(
number: data['n'],
onTap: () {
storageData.pressed(data);
},
isDone: data['i'],
),
],
),
);
}
}
and instead of using a wrap and listening for changes to it's children in the screen class, i just directly use the DayTiles() custom widget i created

implementing bloc pattern with API cause an exception "type 'Future<dynamic>' is not a subtype of type 'Widget?'"

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/

didChangeDependencies hook in Flutter Widget includes not accurate data of the class

In my code below, I am struggling with LifeCyrles in Flutter where I can update my State in Provider, APPARENTLY, only in didChangeDependencies hook or in a template widget (via events hung up on buttons or so).
Alright, I don't mind that only didChangeDependencies hook works for me BUT when my logic in earlier mentioned hook depends on some class properties I am having problems with the accuracy of the class data.
I get data one step behind (since it's called before build hook I guess).
I cannot run this logic in the build hook because it includes a request to change the state in Provider. If I try to change the state there I've got either this error:
setState() or markNeedsBuild() called during build.
or this one
The setter 'lastPage=' was called on null.
Receiver: null
Tried calling: lastPage=true
What I want to do: I've got a wrapper widget which holds three other widgets: footer, header and pageViewer.
When I reach the last page I need to notify my wrapper widget about that so it reacts accordingly and hides header and footer.
I would appreciate any help here!
The focused code:
Here is the problem and must be solution
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:ui_flutter/screens/welcome/welcome_bloc.dart';
import 'package:flutter/scheduler.dart';
class _FooterState extends State<Footer> {
#override
void initState() {
super.initState();
}
#override
void didChangeDependencies() {
super.didChangeDependencies();
final WelcomeBloc _welcome = Provider.of<WelcomeBloc>(context);
_welcomeBloc = _welcome;
// this._detectLastPage();
}
#override
Widget build(BuildContext context) {
return Container(
alignment: Alignment.bottomCenter,
padding: EdgeInsets.symmetric(vertical: 30.0, horizontal: 30.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
this.stepper,
this.nextArrow,
],
),
);
}
_detectLastPage() {
// Here I've got inaccurate data
print(this.widget.currentStep);
}
}
I have already tried some other hooks like Scheduler but maybe I did something wrong there.
SchedulerBinding.instance
.addPostFrameCallback((_) => this._detectLastPage());
It's called only once at the first build-up round and that's it.
I lack an Angular hook here AfterViewInit. It would be handy here.
or Mounted in VueJS
That's the rest of my code if you'd like to see the whole picture.
If you have any suggestions on the architecture, structure or something else you are welcome. It's highly appreciated since I'm new to Flutter.
main.dart
import 'package:flutter/material.dart';
import 'package:ui_flutter/routing.dart';
import 'package:provider/provider.dart';
import 'screens/welcome/welcome_bloc.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider(create: (_) => WelcomeBloc()),
],
child: MaterialApp(
debugShowCheckedModeBanner: false,
theme: ThemeData(
primarySwatch: Colors.blue,
),
initialRoute: '/welcome',
onGenerateRoute: RouteGenerator.generateRoute,
),
);
}
}
welcome.dart (my wrapper)
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:provider/provider.dart';
import 'package:ui_flutter/screens/welcome/welcome_bloc.dart';
import './footer.dart';
import './viewWrapper.dart';
import './header.dart';
// import 'package:ui_flutter/routing.dart';
class Welcome extends StatefulWidget {
#override
_WelcomeState createState() => _WelcomeState();
}
class _WelcomeState extends State<Welcome> {
WelcomeBloc _welcomeBloc;
#override
Widget build(BuildContext context) {
final WelcomeBloc _welcome = Provider.of<WelcomeBloc>(context);
this._welcomeBloc = _welcome;
print('Welcome: _welcome.currentPage - ${this._welcomeBloc.lastPage}');
return Scaffold(
body: SafeArea(
child: Stack(
children: <Widget>[
ViewerWrapper(),
Footer(
currentStep: _welcomeBloc.currentPage,
totalSteps: 3,
activeColor: Colors.grey[800],
inactiveColor: Colors.grey[100],
),
WelcomeHeader,
],
),
),
);
}
}
welcomeBloc.dart (my state via Provider)
import 'package:flutter/material.dart';
class WelcomeBloc extends ChangeNotifier {
PageController _controller = PageController();
int _currentPage;
bool _lastPage = false;
bool get lastPage => _lastPage;
set lastPage(bool value) {
_lastPage = value;
notifyListeners();
}
int get currentPage => _currentPage;
set currentPage(int value) {
_currentPage = value;
notifyListeners();
}
get controller => _controller;
nextPage(Duration duration, Curves curve) {
controller.nextPage(duration: duration, curve: curve);
}
}
footer.dart (that's where I've problems with data at the very bottom of the code - _detectLastPage method)
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:ui_flutter/screens/welcome/welcome_bloc.dart';
import 'package:flutter/scheduler.dart';
class Footer extends StatefulWidget {
final int currentStep;
final int totalSteps;
final Color activeColor;
final Color inactiveColor;
final Duration duration;
final Function onFinal;
final Function onStart;
Footer({
this.activeColor,
this.inactiveColor,
this.currentStep,
this.totalSteps,
this.duration,
this.onFinal,
this.onStart,
}) {}
#override
_FooterState createState() => _FooterState();
}
class _FooterState extends State<Footer> {
final double radius = 10.0;
final double distance = 4.0;
Container stepper;
Container nextArrow;
bool lastPage;
WelcomeBloc _welcomeBloc;
#override
void didChangeDependencies() {
super.didChangeDependencies();
final WelcomeBloc _welcome = Provider.of<WelcomeBloc>(context);
_welcomeBloc = _welcome;
this._detectLastPage();
}
#override
Widget build(BuildContext context) {
this._makeStepper();
this._makeNextArrow();
return Container(
alignment: Alignment.bottomCenter,
padding: EdgeInsets.symmetric(vertical: 30.0, horizontal: 30.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
this.stepper,
this.nextArrow,
],
),
);
}
_makeCirle(activeColor, inactiveColor, position, currentStep) {
currentStep = currentStep == null ? 0 : currentStep - 1;
Color color = (position == currentStep) ? activeColor : inactiveColor;
return Container(
height: this.radius,
width: this.radius,
margin: EdgeInsets.only(left: this.distance, right: this.distance),
decoration: BoxDecoration(
color: color,
border: Border.all(color: activeColor, width: 2.0),
borderRadius: BorderRadius.circular(50.0)),
);
}
_makeStepper() {
List<Container> circles = List();
for (var i = 0; i < widget.totalSteps; i++) {
circles.add(
_makeCirle(this.widget.activeColor, this.widget.inactiveColor, i,
this.widget.currentStep),
);
}
this.stepper = Container(
child: Row(
children: circles,
),
);
}
_makeNextArrow() {
this.nextArrow = Container(
child: Padding(
padding: const EdgeInsets.only(right: 8.0),
child: GestureDetector(
onTap: () {
_welcomeBloc.controller.nextPage(
duration: this.widget.duration ?? Duration(milliseconds: 500),
curve: Curves.easeInOut,
);
},
child: Icon(
Icons.arrow_forward,
)),
),
);
}
_onLastPage() {
if (this.widget.onFinal != null) {
this.widget.onFinal();
}
}
_onFirstPage() {
if (this.widget.onStart != null) {
this.widget.onStart();
}
}
_detectLastPage() {
// Here I've got inaccurate data
int currentPage =
this.widget.currentStep == null ? 1 : this.widget.currentStep;
if (currentPage == 1 && this.widget.currentStep == null) {
this._onFirstPage();
} else if (currentPage == this.widget.totalSteps) {
print('lastPage detected');
setState(() {
this.lastPage = true;
});
_welcomeBloc.lastPage = true;
this._onLastPage();
} else {
setState(() {
this.lastPage = false;
});
_welcomeBloc.lastPage = false;
}
}
}
Thanks in advance!
I am new to flutter as well, But I have learned about a few architecture patterns that have helped me build some apps.
Here is how I do it:
Create a Provider which holds the data for you in runtime. (It can be a Bloc in your case). Stick to one architecture, don't try to put providers and blocs in the same project. Both are used for state management and only using one would be a great practice.
Second, Register the providers using ChangeNotificationProvider or any other widgets which does a similar job of rebuilding the child widget when a data gets changed.
Third, Get the provider in the build method of the widget that is supposed to change when the value of the variable provider changes. This way only the concerned widget is redrawn.
For your case,
If you want to hide the header and footer once you reach the last page, you can declare a variable, let's say isLastPage set to false by default in your provider.
Next, wrap the widget, i.e. header and footer with ChangeNotificationListner
Now, let that widget decide what it has to do based on the value of isLastPage, either hide itself or show.
I hope this helps!
At the long run, I seem to have found Mounted lifecycle hook in Flutter which is implemented with the help of Future.microtask. Unlike .addPostFrameCallback:
SchedulerBinding.instance
.addPostFrameCallback((_) => this._detectLastPage());
Which is triggered only once like InitState (but only at the end of the build execution), Future.microtask can be placed inside build block and be invoked after every change and state update.
It doesn't solve the problem with the inaccurate state in didChangeDependencies hook but provides another way to perform post-build executions.
Credits for the current solution to #Abion47
example
Future.microtask(() => this._detectLastPage());

Flutter : PageController.page cannot be accessed before a PageView is built with it

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