I have a situation where I have a listview of containers, and I would like when I double tap each container another container pops up below with information. Currently what I am trying to do is wrap each container within a column and do something like:
onDoubleTap() {showBox = true}, and in the column have code:
children: [post(), showbox == true? infobox() : container()] but I am not sure of the correct implementation. Any help would be great!
you should maintain a list of containers:
class ContainerAdder extends StatefulWidget {
const ContainerAdder({Key? key}) : super(key: key);
#override
_ContainerAdderState createState() => _ContainerAdderState();
}
class _ContainerAdderState extends State<ContainerAdder> {
List<Widget> containers = <Widget>[];
Random random = Random();
List<Color> colors = [
Colors.blue,
Colors.green,
Colors.red,
Colors.orange,
Colors.purple,
Colors.pink,
Colors.teal,
Colors.yellow,
];
addContainer() {
setState(() {
int r = random.nextInt(colors.length);
containers.add(
InkWell(
onDoubleTap: () => addContainer(),
child: Container(
margin: const EdgeInsets.only(bottom: 1.0),
height: 50.0,
color: colors[r],
),
),
);
});
}
#override
void initState() {
super.initState();
addContainer();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: ListView(
children: [...containers],
),
);
}
}
As you can notice, the method addContainer() adds a container which is wrapped in an InkWell to have the tap listener. The doubleTap calls the method addContainer().
I simply spread the containers list inside ListView widget.
In the addContainer() method, I wrap the codes inside setState() so as to refresh the tree. You can use any other state management architecture if you so wish.
For the first time, I call addContainer() inside initState(), in order to populate the list with the first element.
Related
Actually, I have a parent widget, and It has some of the child widgets in its Column.
like this
Container(
width: double.infinity,
color: Colors.white,
padding: EdgeInsets.fromLTRB(20.0, 50.0, 20.0, 0.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Title(),
SizedBox(height: 80.0,),
confirmLoginType(),
SizedBox(height: 10.0),
LoginButton(),
PolicyTips(
key: IndexGlobalKey.policyTipsKey,
updateState: receiveMessageUpdateState
),
Bottom()
],
),
),
in the PolicyTips, I pass a key to it, and I want to get the key in the LoginButton , but It has always been null when I get currentState.
The code is below:LoginButton
class LoginButton extends StatefulWidget {
LoginButton({Key key}) : super(key: key);
#override
_LoginButtonState createState() => _LoginButtonState();
}
class _LoginButtonState extends State<LoginButton> {
#override
Widget build(BuildContext context) {
policyTipsKey = IndexGlobalKey.policyTipsKey.currentState;
return Container(
child: Text()
)
}
}
what can I do? help me please, thanks.
This is IndexGlobalKey code.
class IndexGlobalKey {
static final GlobalKey<_PolicyTipsState> policyTipsKey = GlobalKey<_PolicyTipsState>();
static GlobalKey<_FormState> phoneLoginKey = GlobalKey<_FormState>();
static GlobalKey<_FormForIdCardLoginState> idCardLoginKey = GlobalKey<_FormForIdCardLoginState>();
}
Build method of _LoginButtonState runs before PolicyTips renders and before IndexGlobalKey.policyTipsKey is actually set. The reason is LoginButton goes before PolicyTips in column. Thats why you get null when you call IndexGlobalKey.policyTipsKey.currentState from build of _LoginButtonState.
To solve this you need to call IndexGlobalKey.policyTipsKey.state right where you use it. For example, when you need to get policy tips state on button tap just use it inside onPressed callback:
class _LoginButtonState extends State<LoginButton> {
#override
Widget build(BuildContext context) {
// An example of your button
return TextButton(
onPressed: () {
final policyTipsState = IndexGlobalKey.policyTipsKey.currentState;
// Here you can use policyTipsState
},
child: Text('button'),
);
}
}
I have a TabBarView with two tabs in main widget. First tab includes gridview with cards. Cards use parent widget (MyHomePage) as listener to listen in-card button clicks.
When i click on button in some card, listener impl. must open second Tab and pass selected Excursion to it. But when I do it, at first iteration, ExcursionEditor(currentExcursion) says, that argument is null, but parent build says, that it is not. If I resize my browser, it calls global rebuild and currentExcursion reach last build value.
So, i cant understand, why MyHomePage build doesn't affect on TabBarView content with arguments passed by constructor
class MyHomePage
import 'package:flutter/material.dart';
import 'package:questbuilder/api/content_manager.dart';
import 'package:questbuilder/model/excursion.dart';
import 'package:questbuilder/pages/tab_editor.dart';
import 'package:questbuilder/pages/tab_my_excursions.dart';
import 'package:questbuilder/widgets/excursion_preview_card.dart';
import 'package:logger/logger.dart';
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage>
with TickerProviderStateMixin
implements ExcursionCardInteractionListener {
Logger logger = Logger();
Excursion currentExcursion;
TabController tabController;
#override
void initState() {
super.initState();
print("INIT STATE FOR HOME PAGE");
tabController = TabController(vsync: this, length: 2);
}
#override
Widget build(BuildContext context) {
var screenSize = MediaQuery.of(context).size;
print("HOME PAGE BUILD currentExcursion = ${currentExcursion?.toJson()}");
return Scaffold(
extendBodyBehindAppBar: true,
appBar: PreferredSize(
preferredSize: Size(screenSize.width, 1000),
child: Container(
color: Colors.black,
child: Padding(
padding: EdgeInsets.fromLTRB(10, 10, 30, 0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(children: [
Padding(
padding: EdgeInsets.fromLTRB(10, 0, 10, 10),
child: Text('QUESTBUILDER',
style: TextStyle(color: Colors.white))),
SizedBox(width: screenSize.width / 20),
Container(
width: screenSize.width / 6,
child: TabBar(
labelPadding: EdgeInsets.fromLTRB(10, 0, 10, 10),
indicatorColor: Colors.white,
controller: tabController,
tabs: [
Tab(text: "Мои экскурсии"),
Tab(text: "Редактор"),
]))
]),
Padding(
padding: EdgeInsets.fromLTRB(0, 0, 0, 10),
child: Row(
children: [
FlatButton.icon(
label: Text("Создать экскурсию"),
icon: Icon(Icons.add),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(40.0)),
textColor: Colors.white,
color: Colors.green,
onPressed: () {
createExcursion();
}),
SizedBox(
width: 40,
),
InkWell(
onTap: () {},
child: Text(
'Вход',
style: TextStyle(color: Colors.white),
),
)
],
)),
],
),
),
),
),
body: Padding(
padding: EdgeInsets.all(15),
child: TabBarView(
controller: tabController,
children: [
// Set listener to cards in this widget to prerform 'edit' clicks
MyExcursionsTab(this),
ExcursionEditor(currentExcursion)
],
)));
}
// Here i call setState from cards
#override
void editExcursion(Excursion excursion) {
setState(() {
currentExcursion = excursion;
});
tabController.animateTo(1);
}
#override
void dispose() {
tabController.dispose();
super.dispose();
}
void createExcursion() {
ContentManager.client.createExcursion(0).then((value) {
currentExcursion = value;
editExcursion(currentExcursion);
});
}
}
class ExcursionEditor
import 'dart:typed_data';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:questbuilder/api/content_manager.dart';
import 'package:questbuilder/model/excursion.dart';
import 'package:questbuilder/model/excursion_content.dart';
import 'package:questbuilder/model/excursion_data.dart';
import 'package:questbuilder/model/picture.dart';
class ExcursionEditor extends StatefulWidget {
Excursion excursion;
ExcursionEditor(this.excursion);
#override
State<StatefulWidget> createState() => ExcursionEditorState();
}
class ExcursionEditorState extends State<ExcursionEditor> {
ExcursionData currentData;
ExcursionContent currentContent;
Excursion excursion;
List<Picture> pictures = [];
#override
void initState() {
super.initState();
print("INIT EDITOR widget.excrusion = ${widget.excursion?.toJson()}");
// At this point, after call setState() in HomePage widget.excrusion is always null
// until I resize browser, thereby calling global state reset
//
if (widget.excursion != null)
ContentManager.client.getPictureList(widget.excursion.id).then((value) {
pictures.addAll(value);
print(pictures);
});
}
#override
Widget build(BuildContext context) {
excursion = widget.excursion;
print("BUILD EDITOR excursion = ${widget.excursion?.toJson()}");
return excursion != null
? Container()
: Container(
child: Align(
alignment: Alignment.center,
child: Text("Выберите экскурсию для редактирования")));
}
}
Log of first launch and card click build sequence:
HOME PAGE BUILD currentExcursion = null
HOME PAGE BUILD currentExcursion = {id: 1}
INIT EDITOR widget.excrusion = null
BUILD EDITOR excursion = null
After browser window resize
HOME PAGE BUILD currentExcursion = {id: 1}
BUILD EDITOR excursion = {id: 1}
BUILD EDITOR excursion = {id: 1}
HOME PAGE BUILD currentExcursion = {id: 1}
BUILD EDITOR excursion = {id: 1}
After screen resize problem still appear, just replacing null value in editor with old Excursion. New clicks on cards doesn't have effect, setState in callback still not update.
I've tried to bind it on static stream listeners, on TabController listener - it just look like TabBarView late for 1 build cycle of arguments update. Maybe there are some similar questions, but i've done all from thouse answers and got nothing
I am not really sure, but it seems like race condition between setState and _tabController.animateTo(1); because they both try to rebuild the child ExcursionEditor(currentExcursion)
If you print the excursion in ExcursionEditor constructor, you will see the updated value. But at the end the value not reach the build function.
The simple workaround is changing editExcursion to the async function and add a small delay between this 2 actions. Otherwise you can try to use other way to pass data between widgets (like provider)
#override
Future editExcursion(Excursion excursion) async {
setState(() {
currentExcursion = excursion;
});
await Future.delayed(Duration(milliseconds:50));
tabController.animateTo(1);
}
I have created a ListView with container boxes as widgets. I want a specific container to expand onTap upto a specific screen height and width. I need help in implementing this in flutter. I have made a prototype on AdobeXD.
AdobeXD Prototype GIF
I am new to flutter, any kind of help is appreciated.
A flutter plugin called flutter swiper might help you achieve what you want to achieve.
Visit this pub dev and you can read documentation.
Here you go brother, Although its not blurring the background but I think it will get you going.
It's working something like this:
Below the code which you can copy paste. I have added comments in the code for understanding it in better way. Cheers :)
import 'package:flutter/material.dart';
void main() => runApp(new MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: HomeApp(),
);
}
}
class HomeApp extends StatefulWidget {
#override
_HomeAppState createState() => _HomeAppState();
}
class _HomeAppState extends State<HomeApp> {
// Items in the list --> Custom Widgets
List<Widget> arr = [
ListContainerHere(),
ListContainerHere(),
ListContainerHere(),
ListContainerHere(),
ListContainerHere(),
ListContainerHere(),
];
Widget getListWidget(List<Widget> items) {
List<Widget> list = new List<Widget>();
for (var i = 0; i <= items.length; i++) {
list.add(new ListContainerHere(
index: i,
));
}
return Row(children: list);
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Flutter App :)"),
),
body: Center(
// Using a 'Row' as Horizontal ListView
child: SingleChildScrollView(
scrollDirection: Axis.horizontal, child: getListWidget(arr)),
),
);
}
}
// Widgets that will be rendered in the Horizontal Row
class ListContainerHere extends StatefulWidget {
final int index;
ListContainerHere({this.index});
#override
_ListContainerHereState createState() => _ListContainerHereState();
}
class _ListContainerHereState extends State<ListContainerHere> {
// Varibale to change the height and width accordingly
// Initally no item will be expanded
bool isExpanded = false;
#override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: GestureDetector(
onTap: () {
// Changing the value of 'isExpanded' when an item is tapped in the List
setState(() {
isExpanded = !isExpanded;
});
},
// AnimatedContainer for slowing down the changing
child: AnimatedContainer(
duration: Duration(milliseconds: 150),
// Changing the width and height
height: isExpanded ? 250 : 150,
width: isExpanded ? 250 : 150,
// Decoration Portion of the Container
decoration: BoxDecoration(
color: Colors.blue, borderRadius: BorderRadius.circular(15.0)),
),
),
);
}
}
I would like to display the index at the bottom of the listView by utilizing the scrollController, the same way it's displayed in the follow image :
After the user scrolls down, or scrolls up, the count on the left, highlighted by red, gets increased/decreased based on the scroll direction of the user.
What I want to achieve is to automatically update the displayed item's index, indicated by red on the picture. So whenever the user scrolls down or up, this index gets updated by the displayed item's index.
The picture shows that I have reached the 26th item. Whenever I scroll down or up, this index gets updated.
I have tried using the offset that is getting emitted for the scrolling event with no luck.
The way is using the scroll controller like you were doing.
You need to use a known item size and a listener.
// Declaring the controller and the item size
ScrollController _scrollController;
final itemSize = 100.0;
// Initializing
#override
void initState() {
_scrollController = ScrollController();
_scrollController.addListener(_scrollListener);
super.initState();
}
// Your list widget (must not be nested lists)
ListView.builder(
controller: _scrollController,
itemCount: <Your list length>,
itemExtent: itemSize,
itemBuilder: (context, index) {
return ListTile(<your items>);
},
),
// With the listener and the itemSize, you can calculate which item
// is on screen using the provided callBack. Something like this:
void _scrollListener() {
setState(() {
var index = (_scrollController.offset / itemSize).round() + 1;
});
}
Adding a listener to a scrollController will call the callback provided every time the list is scrolled. You can handle many behaviours of the list using the same logic, including identifying the type of event that fired the listener, the direction of the scroll, etc.
There is a lib called scroll to index that could help you. You could take the $index to show inside your toast message. Example bellow is from the lib's author:
import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:scroll_to_index/scroll_to_index.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Scroll To Index Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'Scroll To Index Demo'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
static const maxCount = 100;
final random = math.Random();
final scrollDirection = Axis.vertical;
AutoScrollController controller;
List<List<int>> randomList;
#override
void initState() {
super.initState();
controller = AutoScrollController(
viewportBoundaryGetter: () => Rect.fromLTRB(0, 0, 0, MediaQuery.of(context).padding.bottom),
axis: scrollDirection
);
randomList = List.generate(maxCount, (index) => <int>[index, (1000 * random.nextDouble()).toInt()]);
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: ListView(
scrollDirection: scrollDirection,
controller: controller,
children: randomList.map<Widget>((data) {
return Padding(
padding: EdgeInsets.all(8),
child: _getRow(data[0], math.max(data[1].toDouble(), 50.0)),
);
}).toList(),
),
floatingActionButton: FloatingActionButton(
onPressed: _scrollToIndex,
tooltip: 'Increment',
child: Text(counter.toString()),
),
);
}
int counter = -1;
Future _scrollToIndex() async {
setState(() {
counter++;
if (counter >= maxCount)
counter = 0;
});
await controller.scrollToIndex(counter, preferPosition: AutoScrollPosition.begin);
controller.highlight(counter);
}
Widget _getRow(int index, double height) {
return _wrapScrollTag(
index: index,
child: Container(
padding: EdgeInsets.all(8),
alignment: Alignment.topCenter,
height: height,
decoration: BoxDecoration(
border: Border.all(
color: Colors.lightBlue,
width: 4
),
borderRadius: BorderRadius.circular(12)
),
child: Text('index: $index, height: $height'),
)
);
}
Widget _wrapScrollTag({int index, Widget child})
=> AutoScrollTag(
key: ValueKey(index),
controller: controller,
index: index,
child: child,
highlightColor: Colors.black.withOpacity(0.1),
);
}
https://medium.com/flutter-community/create-shop-list-with-flutter-d13d3c20d68b
Maybe this one can help you. Also source code is available. A simple math about item height might help.
I will post my projects minimum classes here that you can reproduce the faulty behavior.
The listing of the classes here goes mostly from the top of the flutter widget hierarchy down the rest...
main.dart
import 'package:TestIt/widgets/applicationpage.dart';
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
// This widget is the root of your application.
final ApplicationPage applicationPage =
ApplicationPage(title: 'Flutter Demo');
#override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Flutter Demo',
theme: ThemeData(
// This is the theme of your application.
primarySwatch: Colors.blue,
),
home: applicationPage);
}
}
applicationpage.dart
import 'package:flutter/material.dart';
import 'body.dart';
class ApplicationPage extends StatefulWidget {
ApplicationPage({Key key, this.title}) : super(key: key);
final String title;
#override
_ApplicationPageState createState() => _ApplicationPageState();
}
class _ApplicationPageState extends State<ApplicationPage> {
final Body body = new Body();
#override
Widget build(BuildContext context) {
return Scaffold(
body: body);
}
}
body.dart
import 'package:TestIt/viewmodels/excercise.dart';
import 'package:TestIt/viewmodels/workout.dart';
import 'package:flutter/material.dart';
import 'Excercises/ExcerciseListWidget.dart';
class Body extends StatelessWidget {
#override
Widget build(BuildContext context) {
var workouts = new List<Workout>();
var pullDay = new Workout("Pull day", new List<Excercise>());
workouts.add(pullDay);
return Container(
margin: EdgeInsets.all(5),
child: DefaultTabController(
// Added
length: workouts.length, // Added
initialIndex: 0, //Added
child: Scaffold(
appBar: PreferredSize(
// todo: add AppBar widget here again
preferredSize: Size.fromHeight(50.0),
child: Row(children: <Widget>[
TabBar(
indicatorColor: Colors.blueAccent,
isScrollable: true,
tabs: getTabs(workouts),
),
Container(
margin: EdgeInsets.only(left: 5.0),
height: 30,
width: 30,
child: FloatingActionButton(
heroTag: null,
child: Icon(Icons.add),
backgroundColor: Colors.red,
foregroundColor: Colors.white,
elevation: 5.0,
onPressed: () => print("add workout"))),
Container(
margin: EdgeInsets.only(left: 5.0),
height: 30,
width: 30,
child: FloatingActionButton(
heroTag: null,
child: Icon(Icons.remove),
backgroundColor: Colors.red,
foregroundColor: Colors.white,
elevation: 5.0,
onPressed: () => print("add workout"))),
])),
body: TabBarView(
children: getTabViews(workouts),
),
)));
}
List<ExcerciseListWidget> getTabViews(List<Workout> workouts) {
var tabViews = new List<ExcerciseListWidget>();
for (var i = 0; i < workouts.length; i++) {
tabViews.add(ExcerciseListWidget(workouts[i].excercises));
}
return tabViews;
}
List<Tab> getTabs(List<Workout> workouts) {
Color textColor = Colors.blueAccent;
return workouts
.map((w) => new Tab(
child: Text(w.name, style: TextStyle(color: textColor)),
))
.toList();
}
}
ExcerciseListWidget.dart
import 'package:TestIt/viewmodels/excercise.dart';
import 'package:flutter/material.dart';
import 'ExcerciseWidget.dart';
class ExcerciseListWidget extends StatefulWidget {
ExcerciseListWidget(this.excercises);
final List<Excercise> excercises;
#override
_ExcerciseListWidgetState createState() => _ExcerciseListWidgetState();
}
class _ExcerciseListWidgetState extends State<ExcerciseListWidget> {
#override
Widget build(BuildContext context) {
return Scaffold(
floatingActionButtonLocation: FloatingActionButtonLocation.endFloat,
floatingActionButton: FloatingActionButton(
onPressed: () {
setState(() {
widget.excercises.insert(
0,
new Excercise(widget.excercises.length + 1, "test",
widget.excercises.length * 10));
});
},
child: Icon(Icons.add),
backgroundColor: Colors.red,
foregroundColor: Colors.white,
elevation: 5.0,
),
body: Container(
padding: const EdgeInsets.all(2),
child: ReorderableListView(
onReorder: (index1, index2) => {
print("onReorder"),
},
children: widget.excercises
.map((excercise) => ExcerciseWidget(
key: ValueKey(excercise.id), excercise: excercise))
.toList())));
}
}
ExcerciseWidget.dart
import 'package:TestIt/viewmodels/excercise.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'ExcerciseDetailsWidget.dart';
class ExcerciseWidget extends StatefulWidget {
ExcerciseWidget({this.key, this.excercise}) : super(key: key);
final Excercise excercise;
final Key key;
#override
_ExcerciseWidgetState createState() => _ExcerciseWidgetState();
}
class _ExcerciseWidgetState extends State<ExcerciseWidget> {
#override
Widget build(BuildContext context) {
return Container(
margin: EdgeInsets.only(top: 3.0, bottom: 3.0),
// TODo: with this ink box decoration the scrolling of the excercises goes under the tabbar... but with the ink I have a ripple effect NOT under
// the element...
child: Ink(
decoration: BoxDecoration(
borderRadius: new BorderRadius.all(new Radius.circular(5.0)),
border: Border.all(color: Colors.orange),
color: Colors.green),
child: InkWell(
onTap: () => {navigateToEditScreen(context)},
child: Column(
children: <Widget>[
Container(
color: Colors.red, child: Text(widget.excercise.name)),
],
)),
));
}
navigateToEditScreen(BuildContext context) async {
final Excercise result = await Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
ExcerciseDetailsWidget(excercise: widget.excercise)));
setState(() {
widget.excercise.name = result.name;
});
}
}
ExcerciseDetailsWidget.dart
import 'package:TestIt/viewmodels/excercise.dart';
import 'package:flutter/material.dart';
class ExcerciseDetailsWidget extends StatefulWidget {
final Excercise excercise;
ExcerciseDetailsWidget({Key key, #required this.excercise}) : super(key: key);
#override
_ExcerciseDetailsWidgetState createState() => _ExcerciseDetailsWidgetState();
}
class _ExcerciseDetailsWidgetState extends State<ExcerciseDetailsWidget> {
final _formKey = GlobalKey<FormState>();
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.excercise.name),
),
body: Padding(
padding: EdgeInsets.only(left: 20, right: 20, bottom: 2, top: 2),
child: Form(
key: _formKey,
child: Column(children: <Widget>[
new RaisedButton(
elevation: 2,
color: Colors.blue,
child: Text('Save'),
onPressed: () {
setState(() {
widget.excercise.name = "new name";
});
Navigator.pop(context, widget.excercise);
}),
TextFormField(
decoration: InputDecoration(
//hintText: 'excercise name',
labelText: 'Excercise name',
),
initialValue: widget.excercise.name,
),
]))));
}
}
workout.dart
import 'excercise.dart';
class Workout{
Workout(this.name, this.excercises);
String name;
List<Excercise> excercises;
}
excercise.dart
class Excercise {
int id;
Excercise(this.id,this.name, this.restBetweenSetsInSeconds);
String name;
int restBetweenSetsInSeconds;
}
How to reproduce the faulty behavior to get the exception:
Click on the bottom-right floating action button to create an excercise test stub which is added to the only existing workout.
Click the newly added excercise
The ExcerciseDetailsWidget is loaded
Click Save in the ExcerciseDetailsWidget
Navigation goes back to the Initial screen and the Exception hits you in the face bam!
Exception
FlutterError (setState() called after dispose(): _ExcerciseWidgetState#bccdb(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().)
Question
Why is the formerly added and clicked ExcerciseWidget`s State disposed when I returned from the ExcerciseDetailsWidget ?
Check for is mounted and then call setState is no solution because in any case the excercise should NOT be disposed because I have to update it with the new excercise name.
If you know a flutter online site where I can put the project I will do so please let me know!
I am a flutter beginner maybe I do something completely wrong bear that in mind :-)
UPDATE
What I have done to workaround the problem is:
Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
ExcerciseDetailsWidget(excercise: widget.excercise)));
do not await the result of the Navigator.
Instead I do this in the Screen2:
onPressed: () {
if (_formKey.currentState.validate()) {
// WHY can I set here the new text WITHOUT setState but when I navigated back the new excercise name is reflected in the list of excercises. Actually that should not be the case right? That confuses me totally.
widget.excercise.name =
excerciseNameTextController.value.text;
Navigator.pop(context);
}
},
but this is really just a workaround that works in this special EDIT use case.
When I have an ADD use case I need to return something to add it to the list of excercises...
Could it be that the problem is that I await the result inside the excercise?
I guess I will try to await the result excercise on the context/level of the ExercerciseListWidget not inside the ExcerciseWidget.
UPDATE 2
Reading more about the navigator it seems or could be that when I am navigating back to the former route which is my initial/root that all the knowledge about the clicked excercise is gone? Do I need therefore kind of nested routing? like "/workouts/id/excercises/id" ?
Despite the downvotes, this is a legitimate question. After poking around a little bit, the reason seems to be the ReorderableListView. For some reason, even if you are providing keys to each child of the list, when the ReorderableListView is rebuilt, all of its children are disposed and reinitialized. Because of this, when you navigate back from ExcerciseDetailsWidget, you are calling setState within a state that has been disposed - this is why you are getting that specific exception.
Frankly, your current code makes it very difficult to figure out whether it's something you've done wrong or a bug related to ReorderableListView. The only thing that can be said for sure is that replacing the ReorderableListView with a regular ListView will fix it.
I highly recommend cleaning up your code first - my IDE lit up like a Christmas tree when I copied your code in. Get rid of the new keyword. Use const constructors. Fix the Excercise typo that repeats itself 60 times in 250 rows of code.
And most importantly, given that you are mutating and displaying a data object across multiple stateful widgets, start using Provider for state management.