Flutter extended_image: setState() or markNeedsBuild() called during build - flutter

I am using the package extended_image to load images from the network and display a shimmer on loading or on error.
I am getting this error setState() or markNeedsBuild() called during build when I am trying to call setState inside the loadStateChanged
In fact, I have two widgets, one VideoThumbnail responsible for loading a thumbnail from the network, and another one VideoDesc that should display the thumbnail description.
But I would like the description to display a shimmer when the image fails to load or is taking longer to load.
I created two states variables, on the VideoThumbnail widget, that should be passed to the VideoDesc widget
videoLoading = true;
videoError = false;
Here is my code following the repo example:
VideoThumbnail State
class _VideoThumbnailState extends State<VideoThumbnail>
with SingleTickerProviderStateMixin {
bool videoLoading;
bool videoError;
AnimationController _controller;
#override
void initState() {
videoLoading = true;
videoError = false;
_controller = AnimationController(
vsync: this,
duration: const Duration(seconds: 3),
);
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
print("Build Process Complete");
});
}
#override
void dispose() {
_controller.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Container(
width: widget.width,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
ClipRRect(
borderRadius: BorderRadius.circular(4.0),
child: ExtendedImage.network(
widget.videoUrl,
width: widget.width,
height: (widget.width) * 3 / 4,
loadStateChanged: (ExtendedImageState state) {
switch (state.extendedImageLoadState) {
case LoadState.loading:
_controller.reset();
setState(() {
videoError = false;
videoLoading = true;
});
return Shimmer.fromColors(
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(4.0),
),
),
baseColor: Colors.black12,
highlightColor: Colors.white24,
);
break;
case LoadState.completed:
_controller.forward();
setState(() {
videoError = false;
videoLoading = false;
});
return FadeTransition(
opacity: _controller,
child: ExtendedRawImage(
image: state.extendedImageInfo?.image,
width: widget.width,
height: (widget.width) * 3 / 4,
),
);
break;
case LoadState.failed:
_controller.reset();
state.imageProvider.evict();
setState(() {
videoError = true;
videoLoading = false;
});
return Container(
width: widget.width,
height: (widget.width) * 3 / 4,
decoration: BoxDecoration(
image: DecorationImage(
image: AssetImage("assets/img/not-found.png"),
fit: BoxFit.fill,
),
),
);
break;
default:
return Container();
}
},
),
),
VideoDesc(
desc: widget.desc,
videoError: videoError,
videoLoading: videoLoading,
)
],
),
);
}
}
Video widget
class VideoDesc extends StatelessWidget {
final String desc;
final bool videoLoading;
final bool videoError;
const VideoDesc({
Key key,
#required this.desc,
this.videoLoading = true,
this.videoError = false,
}) : super(key: key);
#override
Widget build(BuildContext context) {
return Container(
child: videoError || videoLoading
? Shimmer.fromColors(
baseColor: Colors.grey[700],
highlightColor: Colors.white24,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
SizedBox(height: 12.0),
Container(
width: double.infinity,
height: 8.0,
decoration: BoxDecoration(
color: Colors.grey[900],
borderRadius: BorderRadius.circular(2.0),
),
),
SizedBox(height: 12.0),
Container(
width: 80.0,
height: 8.0,
decoration: BoxDecoration(
color: Colors.grey[900],
borderRadius: BorderRadius.circular(2.0),
),
),
],
),
)
: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
SizedBox(height: 12.0),
Text(
desc,
style: TextStyle(
color: Colors.white,
fontSize: 11.0,
),
overflow: TextOverflow.ellipsis,
),
SizedBox(height: 5.0),
Text(
"361,143,203 views",
style: TextStyle(
color: Colors.white54,
fontSize: 12.0,
),
),
],
),
);
}
}
Can anyone help me with this problem? Or if there is a better way to get the extendedImageLoadState value and pass it to another widget without calling the setState inside loadStateChanged

You can't call setState during build process.
If you actually need to, you can do so by using instead:
WidgetsBinding.instance.addPostFrameCallback(() => setState((){}));
However, have in mind, that having this on your switch-case will schedule an infinite loop of rebuilds which you don't want as well.
I suggest you to re-structure your UI logic or at least make it conditional:
if(!videoLoading) {
WidgetsBinding.instance.addPostFrameCallback(() => setState((){
videoError = false;
videoLoading = true;
}));
}

Related

Update State without reloading a widget in Flutter

I have a widget on a screen that receives its data from API calls. The API call is made inside the init method of the Navigation Bar so that continuous API calls can be prevented when going back and forth between screens. Although this works fine, I'm facing a real challenge in trying to get the state of the widget updated when new data is added to that particular API that the widget relies on for displaying data. I would therefore need to know how to display the updated data that I added to the Database by making a post request on a different screen. The only way this happens now is by way of reloading the entire app or by killing it. Any help will be appreciated.
This is the NavBar where the API is getting called. I usually make all the API calls at once here and something I have done here too.
NavBar
class CustomBottomNavigationState extends State<CustomBottomNavigation> {
bool isLoading = true;
int index = 2;
final screens = [
MenuScreen(),
LeaveScreen(),
// TaskList(),
HomeScreen(),
// PaySlipScreen(),
TaskList(),
Claimz_category(),
// ClaimzScreen()
];
#override
void initState() {
// TODO: implement initState
Provider.of<LeaveRequestViewModel>(context, listen: false)
.getLeaveRequest()
.then((value) {
Provider.of<AnnouncementViewModel>(context, listen: false)
.getAllAnouncements()
.then((value) {
Provider.of<TodaysTaskList>(context, listen: false)
.getTodaysTasks() //This is the API call in question
.then((value) {
setState(() {
isLoading = false;
});
});
});
});
super.initState();
}
#override
Widget build(BuildContext context) {
final items = ['The icons are stored here'];
// TODO: implement build
return SafeArea(
child: Scaffold(
body: isLoading
? const Center(
child: CircularProgressIndicator(),
)
: screens[index],
extendBody: true,
bottomNavigationBar: Container(
decoration: const BoxDecoration(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(200),
topRight: Radius.circular(200)),
boxShadow: [
BoxShadow(
color: Colors.transparent,
blurRadius: 10,
offset: Offset(1, 2))
]),
child: CurvedNavigationBar(
items: items,
index: index,
height: 60,
color: const Color.fromARGB(255, 70, 70, 70),
backgroundColor: Colors.transparent,
onTap: (index) => setState(() {
this.index = index;
})),
),
),
);
}
}
ToDoList widget(This the widget where the updates never reflect without reloading)
class ToDoListState extends State<ToDoList> {
#override
Widget build(BuildContext context) {
final toDoList = Provider.of<TodaysTaskList>(context).getToDoList; //This is the getter method that stores the data after it has been fetched from API
// TODO: implement build
return ContainerStyle(
height: SizeVariables.getHeight(context) * 0.35,
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Padding(
padding: EdgeInsets.only(
top: SizeVariables.getHeight(context) * 0.015,
left: SizeVariables.getWidth(context) * 0.04),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Container(
// color: Colors.red,
child: FittedBox(
fit: BoxFit.contain,
child: Text(
'To do list',
style: Theme.of(context).textTheme.caption,
),
),
),
],
),
),
SizedBox(height: SizeVariables.getHeight(context) * 0.01),
Padding(
padding: EdgeInsets.only(
left: SizeVariables.getWidth(context) * 0.04,
top: SizeVariables.getHeight(context) * 0.005,
right: SizeVariables.getWidth(context) * 0.04),
child: SizedBox(
height: SizeVariables.getHeight(context) * 0.25,
child: Container(
// color: Colors.red,
child: toDoList['today'].isEmpty
? Center(
child: Lottie.asset('assets/json/ToDo.json'),
)
: ListView.separated(
physics: const NeverScrollableScrollPhysics(),
itemBuilder: (context, index) => Row(
children: [
Icon(Icons.circle,
color: Colors.white,
size:
SizeVariables.getWidth(context) * 0.03),
SizedBox(
width:
SizeVariables.getWidth(context) * 0.02),
FittedBox(
fit: BoxFit.contain,
child: Text(
toDoList['today'][index]['task_name'], //This is where it is used
overflow: TextOverflow.ellipsis,
style: Theme.of(context)
.textTheme
.bodyText1),
)
],
),
separatorBuilder: (context, index) => Divider(
height: SizeVariables.getHeight(context) * 0.045,
color: Colors.white,
thickness: 0.5,
),
itemCount: toDoList['today'].length > 4
? 4
: toDoList['today'].length),
),
),
)
],
),
);
}
}
The other widget where the date gets added
class _TaskListState extends State<TaskList> {
#override
Widget build(BuildContext context) {
var floatingActionButton;
return Scaffold(
backgroundColor: Colors.black,
floatingActionButton: Container(
....
....,
child: FloatingActionButton(
backgroundColor: Color.fromARGB(255, 70, 69, 69),
onPressed: openDialog, //This is the method for posting data
child: Icon(Icons.add),
),
),
),
body: Container(
....
....
....
),
);
}
Future<dynamic> openDialog() => showDialog(
context: context,
builder: (context) => AlertDialog(
backgroundColor: Color.fromARGB(255, 87, 83, 83),
content: Form(
key: _key,
child: TextFormField(
controller: taskController,
maxLines: 5,
style: Theme.of(context).textTheme.bodyText1,
decoration: InputDecoration(
border: InputBorder.none,
),
validator: (value) {
if (value!.isEmpty || value == '') {
return 'Please Enter Task';
} else {
input = value;
}
},
),
),
actions: [
InkWell(
onTap: () async {
showDatePicker(
context: context,
initialDate: DateTime.now(),
firstDate: DateTime(2010),
lastDate:
DateTime.now().add(const Duration(days: 365)))
.then((date) {
setState(() {
_dateTime = date;
});
print('Date Time: ${dateFormat.format(_dateTime!)}');
});
},
child: const Icon(Icons.calendar_month, color: Colors.white)),
TextButton(
child: Text(
"Add",
style: Theme.of(context).textTheme.bodyText1,
),
onPressed: () async {
Map<String, dynamic> _data = {
'task': taskController.text,
'task_date': dateFormat.format(_dateTime!).toString()
};
print(_data);
if (_key.currentState!.validate()) {
await Provider.of<ToDoViewModel>(context, listen: false)
.addToDo(_data, context) //This is the post method
.then((_) {
Navigator.of(context).pop();
Provider.of<TodaysTaskList>(context, listen: false)
.getTodaysTasks(); //I did this here again to re-initialize the data. I was under the impression that the new data would get initialized for the widget to reflect it on the other screen.
});
}
},
),
],
),
);
void add() {
Navigator.of(context).pop();
}
}
The Get API Call
class TodaysTaskList with ChangeNotifier {
Map<String, dynamic> _getToDoList = {};
Map<String, dynamic> get getToDoList {
return {..._getToDoList};
}
Future<void> getTodaysTasks() async {
SharedPreferences localStorage = await SharedPreferences.getInstance();
var response = await http.get(Uri.parse(AppUrl.toDoList), headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ${localStorage.getString('token')}'
});
if (response.statusCode == 200) {
_getToDoList = json.decode(response.body);
} else {
_getToDoList = {};
}
print('TO DO LIST: $_getToDoList');
notifyListeners();
}
}
Please let me know for additional input.
i think it's because you didn't call the provider to update your state correctly
as i see that you declare new variable to store your provider like this
final toDoList = Provider.of<TodaysTaskList>(context).getToDoList;
then you use it like this
Text(
toDoList['today'][index]['task_name'], //This is where it is used
overflow: TextOverflow.ellipsis,
style: Theme.of(context)
.textTheme
.bodyText1),
)
it's not updating the state, you should wrap the widget that need to be updated with Consumer
Consumer<TodaysTaskList>(
builder: (context, data, child) {
return _Text(
data.[your_list]['today'][index]['task_name'],
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.bodyText1),
);
},
);

tree rebuilds when keyboard shows

I'm using responsive_sizer package for my app..
my whole tree is rebuilt when the keyboard opens with a textfield.
Here the code of the textfield :
class ProfileNameTextField extends StatefulWidget {
const ProfileNameTextField({Key? key}) : super(key: key);
#override
_ProfileNameTextFieldState createState() => _ProfileNameTextFieldState();
}
class _ProfileNameTextFieldState extends State<ProfileNameTextField> {
TextEditingController? _controller;
String _previousName = "";
FocusNode? _focusNode;
final String _forbiddenCharacters = "1234567890&)°(+=/,;.£\$*€<>\_##";
Widget _subText = Container();
#override
void initState() {
// TODO: implement initState
_controller = TextEditingController();
_previousName = CloudUser.instance.username;
_controller!.text = CloudUser.instance.username;
_focusNode = FocusNode();
_focusNode!.addListener(() {
if(!_focusNode!.hasFocus) {
print("Focus on name textfield is lost");
_onSubmitted(_controller!.text);
}
});
super.initState();
}
#override
void dispose() {
// Clean up the focus node when the Form is disposed.
_focusNode!.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
Widget? _suffix;
switch(Provider.of<LoadingProvider>(context).state) {
case LoadingState.busy:
_suffix = SpinKitRing(
color: Theme
.of(context)
.primaryColor,
lineWidth: 2,
size: Theme.of(context).textTheme.subtitle1!.fontSize!
);
break;
case LoadingState.idle:
_suffix = Container();
break;
}
return CustomTextContainer(
child: InkWell(
onTap: _giveFocus,
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children:
[
Text(
"Prénom",
style: Theme.of(context).textTheme.bodyText2!.copyWith(
fontSize: Theme.of(context).textTheme.bodyText2!.fontSize!.sp
)
),
Container(height: Sizer().heightSmallSpace),
Container(height: Theme.of(context).textTheme.bodyText1!.fontSize,
child: Row(children: [
Expanded(
child: TextField(
keyboardType: TextInputType.name,
controller: _controller,
onSubmitted: _onSubmitted,
focusNode: _focusNode,
decoration: InputDecoration(
isDense: true,
contentPadding: EdgeInsets.zero,
),
style: Theme.of(context).textTheme.bodyText1!.copyWith(
color: Theme.of(context).primaryColor,
fontWeight: FontWeight.w600,
fontSize: Theme.of(context).textTheme.bodyText1!.fontSize!.sp
),
textAlign: TextAlign.start,
),),
_suffix,
]),
),
Container(height: Sizer().heightSmallSpace),
Row(children: [
Spacer(),
Container(
height: Theme.of(context).textTheme.subtitle1!.fontSize!*1.2,
child: Center(child: _subText)),
]),
Container(height: Sizer().heightSmallSpace),
]
)
)
);
}
_onSubmitted(String username) {
RegExp regExp = RegExp('[' + _forbiddenCharacters + ']');
if(!regExp.hasMatch(username)) {
if(_previousName != username) {
print("name is " + username);
_previousName = username;
setState(() {
_subText = Container();
});
Provider.of<LoadingProvider>(context, listen: false).update('username', username).then((result) {
if(result) {
CloudUser.instance.username = username;
setState(() {
_subText = Text(
"Enregistré",
style: Theme
.of(context)
.textTheme
.subtitle1!
.copyWith(
color: color.success,
fontSize: Theme.of(context).textTheme.subtitle1!.fontSize!.sp
),
);
});
}
else
setState(() {
_subText = Text(
"Erreur serveur",
style: Theme.of(context).textTheme.subtitle1!.copyWith(
color: Theme.of(context).errorColor,
fontSize: Theme.of(context).textTheme.subtitle1!.fontSize!.sp
),
);
});
});
}
} else {
setState(() {
_subText = Text(
"Caractères interdits",
style: Theme.of(context).textTheme.subtitle1!.copyWith(
color: Theme.of(context).errorColor,
fontSize: Theme.of(context).textTheme.subtitle1!.fontSize!.sp
),
textAlign: TextAlign.right,
);
});
}
}
_giveFocus() {
_focusNode!.requestFocus();
}
}
Within Sizer(), i have :
double padding = 2.h;
double widgetHeight = 8.h;
double iconButton = 4.h;
double radius = 15;
double lineWidth = 3.h;
double heightSpace = 3.h;
double heightSmallSpace = 0.9.h;
double gridSpacing = 0.3.h;
double widthSpace = 1.25.w;
ProfileNameTextField is included in
class _ProfileControllerState extends State<ProfileController> {
#override
Widget build(BuildContext context) {
return Container(
color: Theme.of(context).backgroundColor,
child: Column(
children: [
ProfileAppBar(
onSetting: _onSetting,
),
Flexible(
child: Container(
padding: EdgeInsets.symmetric(horizontal: Sizer().padding/3),
color: Theme.of(context).scaffoldBackgroundColor,
child: Scrollbar(
child: SingleChildScrollView(
physics: ClampingScrollPhysics(),
child: Container(
padding: EdgeInsets.symmetric(horizontal: Sizer().padding*2/3),
child: Column(children:
[
Container(height: Sizer().heightSpace),
SvgPicture.asset(
"assets/icons/phone_kisses.svg",
height: Sizer().widgetHeight*3,
width: Sizer().getCustomWidth(66),
fit: BoxFit.contain,
),
_space(),
ChangeNotifierProvider<LoadingProvider>(
create: (BuildContext context) => LoadingProvider(),
child: ProfileNameTextField(),
),
I have this problem since the import of the responsive_sizer... I do not understand where the problem can come from.
I tried resizetoavoidbottominset but nothing changed.
I found the problem. It's not coming from MediaQuery, but from the Responsive_sizer package.
When I open the keyboard, I actually update the height and width. But this package must necessarily encompass your MaterialApp in the following way:
MaterialApp(
home: ResponsiveSizer(
builder: (context, orientation, screenType) {
return const HomePage();
},
),
);
And that's the problem. Under my homepage, I have a stream to see if the user is logged in, which then leads to the profile page, among other things. I don't want it to reload, I just want the profile to reload.
My solution: I use MediaQuery in a similar way to Responsive_sizer.... instead of using the .h and .w package, I use MediaQuery.of(context).size.height and its counterpart. The same thing for the font size.
I hope this can help those who have the same problem as me on this package,
good evening.

A TextEditingController was used after being disposed. Called a reusable entry field class where i am passing this controller

I have created a reusable field that is called to display different fields in a form in different screen in my app i have also passed a controller however when dispose the controller it shows this error and if i go back to the same form screen it crashes.
class EntryField extends StatefulWidget {
#override
_EntryFieldState createState() => _EntryFieldState();
final String title;
final TextEditingController controller;
final TextInputType inputType;
final FilteringTextInputFormatter filter;
final hintText;
EntryField({#required this.title,this.hintText,#required this.controller,#required this.inputType,#required this.filter});
}
class _EntryFieldState extends State<EntryField> {
#override
void dispose() {
widget.controller.dispose();
print("anything");
super.dispose();
}
#override
Widget build(BuildContext context) {
return Container(
margin: EdgeInsets.symmetric(vertical: 10),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
this.widget.title,
style: GoogleFonts.quicksand(
fontSize: 18,
)
),
SizedBox(
height: 10,
),
TextFormField(
controller: this.widget.controller,
keyboardType: this.widget.inputType,
inputFormatters: <TextInputFormatter>[
this.widget.filter,
],
validator: (value){
if(value.isEmpty){
return "${this.widget.title} is a Required Field";
}
return null;
},
decoration: InputDecoration(
hintText: this.widget.hintText,
border: InputBorder.none,
fillColor: Color(0xfff3f3f4),
filled: true,
errorBorder: new OutlineInputBorder(
borderSide: new BorderSide(color: Colors.red),
),
errorStyle: TextStyle(
fontSize: 15,
),
),
),
],
),
);
}
}
and in this class i am passing it field values
final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
final _serviceTitleController = TextEditingController();
final _serviceCategoryController = TextEditingController();
final _servicePriceController = TextEditingController();
ToastErrorMessage _error = ToastErrorMessage();
ToastValidMessage _valid = ToastValidMessage();
class AddServices extends StatefulWidget {
#override
_AddServicesState createState() => _AddServicesState();
}
class _AddServicesState extends State<AddServices> {
int currentIndex;
String _cityName;
final WorkshopServiceQueries _add = WorkshopServiceQueries();
final _firebaseUser = FirebaseAuth.instance.currentUser;
#override
void initState() {
cityName();
super.initState();
currentIndex = 0;
}
#override
void dispose() {
print("hello");
super.dispose();
}
void clearControllerText(){
_serviceTitleController.clear();
_serviceCategoryController.clear();
_servicePriceController.clear();
}
Future cityName() async{
_cityName = await _add.getWorkshopCityName();
}
changePage(int index) {
setState(() {
currentIndex = index;
});
}
validateFields() async{
final ValidateWorkshopServices service = ValidateWorkshopServices();
final int _price = int.tryParse(_servicePriceController.text.trim());
if(!service.validateServiceCategory(_serviceCategoryController.text.trim()) && !service.validateServiceTitle(_serviceTitleController.text.trim()) && !service.validateServicePrice(_price)){
_error.errorToastMessage(errorMessage: "Enter Valid Data in Each Field");
}
else if(!service.validateServiceCategory(_serviceCategoryController.text.trim())){
_error.errorToastMessage(errorMessage: "Service Category Must Only contain Alphabets");
}
else if(!service.validateServiceTitle(_serviceTitleController.text.trim())){
_error.errorToastMessage(errorMessage: "Service Title Must Only contain Alphabets");
}
else if(!service.validateServicePrice(_price)){
_error.errorToastMessage(errorMessage: "Service Price must be less than or equal to 2000");
}
else{
await addService(_price);
}
}
Future<void> addService(int price) async{
try {
Services data = Services(title: _serviceTitleController.text.trim(), category: _serviceCategoryController.text.trim(), price: price, workshopCity: _cityName, workshopId: _firebaseUser.uid);
await _add.addWorkshopService(data);
if(WorkshopServiceQueries.resultMessage == WorkshopServiceQueries.completionMessage){
_valid.validToastMessage(validMessage: WorkshopServiceQueries.resultMessage);
clearControllerText();
Future.delayed(
new Duration(seconds: 2),
(){
Navigator.pop(context);
},
);
}
else{
_error.errorToastMessage(errorMessage: WorkshopServiceQueries.resultMessage);
}
}catch(e){
_error.errorToastMessage(errorMessage: e.toString());
}
}
#override
Widget build(BuildContext context) {
final height = MediaQuery.of(context).size.height;
int _checkboxValue;
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: Text(
'BIKERSWORLD',
style: GoogleFonts.quicksand(
color: Colors.white,
fontSize: 18,
),
),
backgroundColor: Color(0XFF012A4A),
leading: IconButton(icon:Icon(Icons.arrow_back, color: Colors.orange,),
onPressed:() => Navigator.pop(context),
)
),
body: Container(
height: height,
child: Stack(
children: <Widget>[
Container(
padding: EdgeInsets.symmetric(horizontal: 20),
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
SizedBox(height: 30,),
_title(),
SizedBox(height: 40),
_addServicesWidget(),
SizedBox(height: 20),
FlatButton(
child: Container(
padding: EdgeInsets.symmetric(vertical: 15),
alignment: Alignment.center,
decoration: BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(5)),
boxShadow: <BoxShadow>[
BoxShadow(
color: Colors.grey.shade200,
offset: Offset(2, 4),
blurRadius: 5,
spreadRadius: 2)
],
gradient: LinearGradient(
begin: Alignment.centerLeft,
end: Alignment.centerRight,
colors: [Color(0xfffbb448), Color(0xfff7892b)])),
child: Text(
'Register Now',
style: GoogleFonts.krub(
fontSize: 18,
color: Colors.white,
),
),
),
onPressed: (){
if(!_formKey.currentState.validate()){
return;
}
else{
validateFields();
}
},
),
SizedBox(height: 20),
],
),
),
),
],
),
),
),
);
}
}
Widget _addServicesWidget() {
return Form(
key: _formKey,
autovalidateMode: AutovalidateMode.disabled,
child: Column(
children: <Widget>[
EntryField(title: "Category",hintText: 'Mechanical',controller: _serviceCategoryController,inputType: TextInputType.text,filter: FilteringTextInputFormatter.allow(RegExp("[a-zA-Z ]"))),
SizedBox(height:15,),
EntryField(title: "Title",hintText: 'wheel barring',controller: _serviceTitleController,inputType: TextInputType.text,filter: FilteringTextInputFormatter.allow(RegExp("[a-zA-Z ]"))),
SizedBox(height:15,),
EntryField(title: "Price",hintText: 'price < 2000',controller: _servicePriceController,inputType: TextInputType.number,filter:FilteringTextInputFormatter.digitsOnly),
],
),
);
}
You shouldn't dispose the controller from within your widget, since you are creating it outside the widget and passing a reference to it into the widget.
It looks like your controllers are created in the global scope - if so, and if they are intended to be used throughout the lifetime of the app, you shouldn't dispose them.
So either
don't dispose the controllers if they are globals
or create and dispose them from the same "owner" object
for future comers, in my case i was using dispose twice for the same controller:
//error
void dispose() {
myController.dispose();
myController.dispose();
super.dispose();
}
//ok
void dispose() {
myController.dispose();
super.dispose();
}

GlobalKey<FormState>().currentState.save() is falling when I submit a form in Flutter

Using bloc from rxdart: ^0.24.1
I am trying to save object on mysql. The first try the object get saved succefully, the second try, with a new object, it falling on formKey.currentState.save(). I am using GlobalKey<FormState>() in order to validate the form with Stream
My code is
class DetailGamePage extends StatefulWidget {
#override
_DetailGameState createState() => _DetailGameState();
}
class _DetailGameState extends State<DetailGamePage> {
final formKey = GlobalKey<FormState>();
GameBloc gameBloc;
#override
void didChangeDependencies() {
super.didChangeDependencies();
if (gameBloc == null) {
gameBloc = Provider.gameBloc(context);
}
}
#override
Widget build(BuildContext context) {
Game _game = ModalRoute.of(context).settings.arguments;
if (_game == null) {
_game = Game(
color: "#000000",
description: "",
env: "",
isBuyIt: false,
isOnBacklog: false);
}
return Scaffold(
appBar: AppBar(
iconTheme: IconThemeData(color: Colors.black),
backgroundColor: Colors.white,
title: Text(
"Add Game",
style: TextStyle(color: Colors.black),
),
actions: [
FlatButton(
onPressed: () {
if (formKey.currentState.validate()) {
formKey.currentState.save();
Fluttertoast.showToast(msg: "Game saved");
setState(() {
gameBloc.saveOrUpdate(_game, gameBloc.name,
gameBloc.description, "listGame");
});
Navigator.pushReplacementNamed(context, "home");
}
},
child: Text(
(StringUtils.isNullOrEmpty(_game.id)) ? "Add" : "Update",
style: TextStyle(color: HexColor(_game.color), fontSize: 20),
))
],
),
body: Form(
key: formKey,
child: Stack(children: <Widget>[
_createBackground(context, _game),
_createFormGame(context, _game, gameBloc)
]),
));
}
Widget _createBackground(BuildContext context, Game game) {
final size = MediaQuery.of(context).size;
final gradientTop = Container(
height: size.height, //* 0.4,
width: double.infinity,
decoration: BoxDecoration(
gradient: LinearGradient(
colors: <Color>[HexColor(game.color), Colors.white])),
);
final circule = Container(
width: 100.0,
height: 100.0,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(100.0),
color: Color.fromRGBO(255, 255, 255, 0.1)),
);
return Stack(
children: <Widget>[
gradientTop,
Positioned(
child: circule,
top: 90,
left: 50,
),
Positioned(
child: circule,
top: -40,
right: -30,
),
Container(
padding: EdgeInsets.only(top: 80),
child: Column(
children: <Widget>[
SizedBox(
height: 10.0,
width: double.infinity,
),
],
),
)
],
);
}
Widget _createFormGame(BuildContext context, Game game, GameBloc gameBloc) {
final size = MediaQuery.of(context).size;
return SingleChildScrollView(
child: Column(
children: <Widget>[
SafeArea(
child: Container(
height: 80.0,
)),
Container(
width: size.width * 0.85,
padding: EdgeInsets.symmetric(vertical: 50.0),
margin: EdgeInsets.symmetric(vertical: 30.0),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(5.0),
boxShadow: <BoxShadow>[
BoxShadow(
color: Colors.black26,
blurRadius: 3.0,
offset: Offset(0.0, 5.0),
spreadRadius: 3.0)
]),
child: Column(
children: <Widget>[
Text("Foto", style: TextStyle(fontSize: 20.0)),
SizedBox(
height: 50.0,
),
_createNameImput(gameBloc, game),
_createDescriptionImput(gameBloc, game),
Divider(
height: 30,
color: HexColor(game.color),
indent: 30,
endIndent: 20,
),
_createWasGameImput(gameBloc, game),
Divider(
height: 30,
color: HexColor(game.color),
indent: 30,
endIndent: 20,
),
_createToTheBacklogImput(gameBloc, game),
SizedBox(height: 60),
_createDeleteButton(gameBloc, game),
SizedBox(height: 60),
],
))
],
),
);
}
#override
void dispose() {
gameBloc?.dispose();
super.dispose();
}
Widget _createWasGameImput(GameBloc gameBloc, Game game) {
return StreamBuilder(
builder: (BuildContext context, AsyncSnapshot snapshot) {
return Container(
padding: EdgeInsets.symmetric(horizontal: 20.0),
child: SwitchListTile(
activeColor: HexColor(game.color),
title: Text("Do you have it?"),
value: game.isBuyIt,
onChanged: (bool value) {
setState(() {
game.isBuyIt = value;
});
},
secondary: IconButton(
icon: Icon(Icons.shopping_cart),
onPressed: null,
color: HexColor(game.color),
),
));
},
);
}
Widget _createToTheBacklogImput(GameBloc gameBloc, Game game) {
return StreamBuilder(
builder: (BuildContext context, AsyncSnapshot snapshot) {
return Container(
padding: EdgeInsets.symmetric(horizontal: 20.0),
child: SwitchListTile(
activeColor: HexColor(game.color),
title: Text("To the backlog?"),
value: game.isOnBacklog,
onChanged: (bool value) {
setState(() {
game.isOnBacklog = true;
});
},
secondary: IconButton(
icon: Icon(Icons.list),
onPressed: null,
color: HexColor(game.color),
),
));
},
);
}
Widget _createNameImput(GameBloc gamebloc, Game game) {
return Column(children: [
Container(
padding: EdgeInsets.symmetric(horizontal: 20.0),
child: TextFormField(
textCapitalization: TextCapitalization.sentences,
initialValue: game.name,
onSaved: (value) {
gameBloc.setName(value);
},
keyboardType: TextInputType.text,
decoration: InputDecoration(
labelText: "Name",
icon: Icon(
Icons.games,
color: HexColor(game.color),
)),
),
),
Divider(
height: 30,
color: HexColor(game.color),
indent: 30,
endIndent: 20,
),
]);
}
Widget _createDescriptionImput(GameBloc gameBloc, Game game) {
return Container(
padding: EdgeInsets.symmetric(horizontal: 20.0),
child: TextFormField(
textCapitalization: TextCapitalization.sentences,
initialValue: game.description,
onSaved: (value) {
gameBloc.setDescription(value);
},
keyboardType: TextInputType.text,
decoration: InputDecoration(
labelText: "Description",
icon: Icon(
Icons.description,
color: HexColor(game.color),
)),
),
);
}
Widget _createDeleteButton(GameBloc gameBloc, Game game) {
if (StringUtils.isNotNullOrEmpty(game.id)) {
return FlatButton(
onPressed: () {
showDialog(
context: context,
builder: (context) {
return AlertDialog(
content: Text("Do you wan to remove the game"),
actions: <Widget>[
FlatButton(
onPressed: () {
setState(() {
gameBloc.remove(game, "listGame");
});
Navigator.pop(context);
Navigator.pop(context);
},
child: Text("Yes")),
FlatButton(
onPressed: () => Navigator.of(context).pop(),
child: Text("No"))
],
);
});
},
child: Text("Remove Game"));
} else {
return Container();
}
}
}
This is the bloc
class GameBloc extends Validators {
//Controller
final _allDataGames = BehaviorSubject<List<Game>>();
final _descriptionController = BehaviorSubject<String>();
final _nameController = BehaviorSubject<String>();
final _allMyListGamesByNameController = BehaviorSubject<List<Game>>();
//Services
GameService gameService = GameService();
//get Data from streams
Stream<List<Game>> get allGameData => _allDataGames.stream;
Stream<List<Game>> get allGameByNameList =>
_allMyListGamesByNameController.stream;
Stream<String> get getDescriptionStream =>
_descriptionController.stream.transform(validateDescription);
Stream<String> get getNameStream =>
_nameController.stream.transform(validName);
//Observable
Stream<bool> get validateDescriptionStream =>
Rx.combineLatest([getDescriptionStream], (description) => true);
Stream<bool> get validateNameStream =>
Rx.combineLatest([getNameStream], (name) => true);
//Set Stream
Function(String) get setDescription => _descriptionController.sink.add;
Function(String) get setName => _nameController.sink.add;
//Get Stream
//From repo
void allGames() async {
List<Game> games = await gameService.getAllDataGames();
_allDataGames.sink.add(games);
}
//From my setting
void allMyListGamesByName(String listName) async {
List<Game> games = await gameService.allMyListGamesByName(listName);
_allMyListGamesByNameController.sink.add(games);
}
void saveOrUpdate(
Game game, String name, String description, String listGame) {
game.name = name;
game.description = description;
if (StringUtils.isNullOrEmpty(game.id)) {
game.id = Uuid().v1();
gameService.add(game, listGame);
} else {
gameService.update(game);
}
}
void remove(Game game, String listGame) {
gameService.remove(game, listGame);
}
//Get Lastest stream value
String get name => _nameController.value;
String get description => _descriptionController.value;
dispose() {
_descriptionController?.close();
_allMyListGamesByNameController?.close();
_allDataGames?.close();
_nameController?.close();
}
}
The provider:
class Provider extends InheritedWidget {
static Provider _imstance;
final _gameBloc = GameBloc();
factory Provider({Key key, Widget child}) {
if (_imstance == null) {
_imstance = new Provider._internal(key: key, child: child);
}
return _imstance;
}
Provider._internal({Key key, Widget child}) : super(key: key, child: child);
static GameBloc gameBloc(BuildContext context) {
return (context.inheritFromWidgetOfExactType(Provider) as Provider)
._gameBloc;
}
#override
bool updateShouldNotify(InheritedWidget oldWidget) {
return true;
}
}
The error is:
════════ Exception caught by gesture ═══════════════════════════════════════════
Bad state: Cannot add new events after calling close
When I evaluate formKey.currentState.save(); I got:
formKey.currentState.save()
Unhandled exception:
Bad state: Cannot add new events after calling close
#0 _BroadcastStreamController.add (dart:async/broadcast_stream_controller.dart:249:24)
#1 Subject._add (package:rxdart/src/subjects/subject.dart:141:17)
#2 Subject.add (package:rxdart/src/subjects/subject.dart:135:5)
#3 _StreamSinkWrapper.add (package:rxdart/src/subjects/subject.dart:167:13)
I was reading about this error, it mention the error is on Bloc singleston scope or dispose method.
What is happen?
When you navigate to home with Navigator.pushReplacementNamed(context, "home"), the _DetailGamePage<State> is being disposed, calling gameBloc?.dispose. This leaves _gameBloc instantiated with all streams closed.
As you are using a Singleton Provider, when you navigate back to DetailGamePage, your save is trying to write to the closed streams.
What you need to do is move the closure of the streams farther up the widget tree so as not to close them before you are done with them, perhaps at the app level OR re-instantiate _gameBloc if the streams are closed, loading the data from the repo again.

No Animation with AnimatedSwitcher or AnimatedOpacity

I'm learning flutter and I'm having behavior with the animation system.
I created a Radio Button which is a circle that should get filled when it gets clicked.
I created a stateful widget class and a state class.
In the state class, I build a :
GestureDetector -> Container -> AnimatedSwitcher -> _animatedWidget
_animatedWidget is a Widget that changes when I click (in the GestureDetector onTap I do _changeSelect)
void _changeSelect(){
_isSelected = !_isSelected;
if(_isSelected){
setState(() {
_animatedWidget = Container(width: double.infinity,height: double.infinity,decoration: BoxDecoration(shape: BoxShape.circle, color: Colors.black));
});
}
else{
setState(() {
_animatedWidget = Container();
});
}
}
And this code is not working properly, it should fade in the full container and fade out the empty container but instead, it just pops in and pops out (like a classic change would do)
Here is the full code of my state class :
class _RadioButtonState extends State<RadioButton> {
Widget _animatedWidget = Container();
bool _isSelected = false;
bool isSelected(){
return _isSelected;
}
void _changeSelect(){
_isSelected = !_isSelected;
if(_isSelected){
setState(() {
_animatedWidget = Container(width: double.infinity,height: double.infinity,decoration: BoxDecoration(shape: BoxShape.circle, color: Colors.black));
});
}
else{
setState(() {
_animatedWidget = Container();
});
}
}
#override
Widget build(BuildContext context){
return GestureDetector(
onTap: _changeSelect,
child:
Container(
width: 16.0,
height: 16.0,
padding: EdgeInsets.all(2.0),
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(width: 2.0, color: Colors.black)
),
child: AnimatedSwitcher(
duration: Duration(seconds: 1),
child: _animatedWidget,
)
),
);
}
}
Note: I also tried with AnimatedOpacity instead of AnimatedSwitcher (with the full Container with a starting opacity of 0 increased to 1 when clicked) but it doesn't even change the view, however, the javascript looks to be working during the duration time
Is this what you're looking for?
Widget _animatedWidget = Container();
bool _isSelected = false;
bool isSelected() {
return _isSelected;
}
void _changeSelect() {
_isSelected = !_isSelected;
if (_isSelected) {
setState(() {
_animatedWidget = Container(
key: ValueKey(1),
width: double.infinity,
height: double.infinity,
decoration: BoxDecoration(shape: BoxShape.circle, color: Colors.black),
);
});
} else {
setState(() {
_animatedWidget = Container(
key: ValueKey(2),
);
});
}
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Center(
child: GestureDetector(
onTap: _changeSelect,
child: Container(
width: 66.0,
height: 66.0,
padding: EdgeInsets.all(2.0),
decoration: BoxDecoration(shape: BoxShape.circle, border: Border.all(width: 2.0, color: Colors.black)),
child: AnimatedSwitcher(
duration: Duration(seconds: 1),
child: _animatedWidget,
)),
),
),
);
}