I'm working on an app where I display markers over a map like so:
The way it works is markers are rendered "over" the Map widget as Stack. My problem is that currently, the markers 'absorbs' the gestures used to control the map underneath (if the gesture starts on the marker).
I was therefore wondering, is there a way to pass through all gestures events between two widgets in a stack? Ideally, the marker would ignore (and pass through) all events except onTap (as I still want to be able to click on the markers).
Here my specific tree:
Cheers!
When a widget that is (visually) on top of another widget in the same stack is hit, the stack will stop any further hit testing. So, in your case, the second child of the stack containing the GoogleMap widget must be made to report that it is not hit, so the stack will give GoogleMap a chance to react to pointer events. IgnorePointer can do that, however that widget will also not hit test its child, so its child gesture detectors will never be involved in any gesture. In simple cases, that can be worked-around by swapping the order of IgnorePointer and GestureDetector while setting the latter's behavior property to HitTestBehaviour.translucent. For example:
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) => MaterialApp(
home: Stack(
fit: StackFit.expand,
children: [
GestureDetector(
onDoubleTap: () => print("double red"),
child: Container(color: Colors.red),
),
Positioned(
top: 100,
left: 100,
right: 100,
bottom: 100,
child: GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: () => print("green"),
child: IgnorePointer(
child: Container(color: Colors.green),
),
),
),
],
),
);
}
Your case is more complicated though. A more generic approach would be to create a new widget like IgnorePointer (let's call it TransparentPointer) that can act to it parent as if it is never hit, while still doing hit testing on its child. Here I've copied IgnorePointer and changed the behavior in that way (the only change with respect to RenderIgnorePointer is in RenderTransparentPointer.hitTest):
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) => MaterialApp(
home: Stack(
fit: StackFit.expand,
children: [
GestureDetector(
onDoubleTap: () => print("double red"),
child: Container(color: Colors.red),
),
TransparentPointer(
transparent: true,
child: Stack(
children: [
Positioned(
top: 100,
left: 100,
right: 100,
bottom: 100,
child: GestureDetector(
onTap: () => print("green"),
child: Container(color: Colors.green),
),
)
],
),
),
],
),
);
}
class TransparentPointer extends SingleChildRenderObjectWidget {
const TransparentPointer({
Key key,
this.transparent = true,
Widget child,
}) : assert(transparent != null),
super(key: key, child: child);
final bool transparent;
#override
RenderTransparentPointer createRenderObject(BuildContext context) {
return RenderTransparentPointer(
transparent: transparent,
);
}
#override
void updateRenderObject(BuildContext context, RenderTransparentPointer renderObject) {
renderObject
..transparent = transparent;
}
#override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(DiagnosticsProperty<bool>('transparent', transparent));
}
}
class RenderTransparentPointer extends RenderProxyBox {
RenderTransparentPointer({
RenderBox child,
bool transparent = true,
}) : _transparent = transparent,
super(child) {
assert(_transparent != null);
}
bool get transparent => _transparent;
bool _transparent;
set transparent(bool value) {
assert(value != null);
if (value == _transparent) return;
_transparent = value;
}
#override
bool hitTest(BoxHitTestResult result, {#required Offset position}) {
// forward hits to our child:
final hit = super.hitTest(result, position: position);
// but report to our parent that we are not hit when `transparent` is true:
return !transparent && hit;
}
#override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(DiagnosticsProperty<bool>('transparent', transparent));
}
}
I have published this code as a small package: https://pub.dev/packages/transparent_pointer
Related
Recently I have downloaded the Louis Vuitton App. I found a strange type of horizontal scroll of product items in listview. I tried card_swiper package but couldnot get through it. How can I achieve such scroll as in gif below?
the trick here is to use a stack and:
Use a page view to display every element except the first one
Use a left aligned FractionallySizedBox which displays the first item and grows with the first item offset
It took me a few tries but the result is very satisfying, I'll let you add the bags but here you go with colored boxes ;) :
import 'dart:math';
import 'package:flutter/material.dart';
void main() {
runApp(MaterialApp(home: FunList()));
}
class FunList extends StatefulWidget {
#override
State<FunList> createState() => _FunListState();
}
class _FunListState extends State<FunList> {
/// The colors of the items in the list
final _itemsColors = List.generate(
100,
(index) => Color((Random().nextDouble() * 0xFFFFFF).toInt()).withOpacity(1.0),
);
/// The current page of the page view
double _page = 0;
/// The index of the leftmost element of the list to be displayed
int get _firstItemIndex => _page.toInt();
/// The offset of the leftmost element of the list to be displayed
double get _firstItemOffset => _controller.hasClients ? 1 - (_page % 1) : 1;
/// Controller to get the current position of the page view
final _controller = PageController(
viewportFraction: 0.25,
);
/// The width of a single item
late final _itemWidth = MediaQuery.of(context).size.width * _controller.viewportFraction;
#override
void initState() {
super.initState();
_controller.addListener(() => setState(() {
_page = _controller.page!;
}));
}
#override
void dispose() {
_controller.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Center(
child: Stack(
children: [
Positioned.fill(
child: Align(
alignment: Alignment.centerLeft,
child: SizedBox(
width: _itemWidth,
child: FractionallySizedBox(
widthFactor: _firstItemOffset,
heightFactor: _firstItemOffset,
child: PageViewItem(color: _itemsColors[_firstItemIndex]),
),
),
),
),
SizedBox(
height: 200,
child: PageView.builder(
padEnds: false,
controller: _controller,
itemBuilder: (context, index) {
return Opacity(
opacity: index <= _firstItemIndex ? 0 : 1,
child: PageViewItem(color: _itemsColors[index]),
);
},
itemCount: _itemsColors.length,
),
),
],
),
);
}
}
class PageViewItem extends StatelessWidget {
final Color color;
const PageViewItem({
Key? key,
required this.color,
}) : super(key: key);
#override
Widget build(BuildContext context) {
return Container(
margin: EdgeInsets.all(10),
color: color,
);
}
}
WORK :
I am Creating App for book and I am linking images and swipe to next page
I am using Gesture Detector for swipe next page
and I am using InteractiveViewer for zoom page
PROBLEM :
The problem is when I use pinch to zoom-in its successfully work but when Drag the page for seeing more words . It detect gesture Detector and It goes to other page ..
WHAT I WANT :
I want to disable the gestureDetector when I am using InteractiveViewer,
Like when I am in Zoom-in mode so the gestureDetector Disable and when I Zoom out
the GestureDetector enable.
import 'package:flutter/material.dart';
class Screen2 extends StatefulWidget {
const Screen2({ Key key }) : super(key: key);
#override
_Screen2State createState() => _Screen2State();
}
class _Screen2State extends State<Screen2> {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: appbarr(),
body: GestureDetector(
onHorizontalDragUpdate: (details) {
// Note: Sensitivity is integer used when you don't want to mess up vertical drag
int sensitivity = 8;
int senElse = -8;
if (details.delta.dx > sensitivity) {
Navigator.pop(context);
}
else if (details.delta.dx < senElse )
{
Navigator.push(context, MaterialPageRoute(builder: (context) => Screen3()));
}
},
child: InteractiveViewer(
panEnabled: true,
minScale: 0.5,
maxScale: 5,
child: Container(
width: MediaQuery.of(context).size.width,
height: MediaQuery.of(context).size.height,
decoration: BoxDecoration(
image: DecorationImage(
image: AssetImage("assets/2.jpg"),
fit: BoxFit.fill,
),
),
),
),
)
);
}
}
You need to check if the is the user has zoomed in or not. If the user has zoomed in we not render a GestureDetector. We only make use of the GestureDetector is the user hasn't zoomed. To check whether the user has zoomed in or not, we use a TranformationController and compare it's current value with the identity matrix.
Here is the working code:
class Screen2 extends StatefulWidget {
const Screen2({ Key? key }) : super(key: key);
#override
_Screen2State createState() => _Screen2State();
}
class _Screen2State extends State<Screen2> {
final transformationController = TransformationController();
bool get userHasZoomedIn => (Matrix4.identity() - transformationController.value).infinityNorm() > 0.000001;
#override
Widget build(BuildContext context) {
final interactiveImage = InteractiveViewer(
panEnabled: true,
minScale: 0.5,
maxScale: 5,
transformationController: transformationController,
onInteractionEnd: (details) => setState((){}),
child: Container(
width: MediaQuery.of(context).size.width,
height: MediaQuery.of(context).size.height,
decoration: BoxDecoration(
image: DecorationImage(
image: AssetImage("assets/2.jpg"),
fit: BoxFit.fill,
),
),
),
);
final appBar = AppBar(title: Text('Title'));
if(userHasZoomedIn){
return Scaffold(
appBar: appBar,
body: interactiveImage,
);
}
return Scaffold(
appBar: appBar,
body: GestureDetector(
onHorizontalDragUpdate: (details) {
// Note: Sensitivity is integer used when you don't want to mess up vertical drag
int sensitivity = 8;
int senElse = -8;
if (details.delta.dx > sensitivity) {
Navigator.pop(context);
}
else if (details.delta.dx < senElse )
{
Navigator.push(context, MaterialPageRoute(builder: (context) => Screen3()));
}
},
child: interactiveImage,
)
);
}
}
Note that we call setState in the InteractionViewer.onInteractionEnd. Another approach would be to add a listener to the TransformationController:
transformationController.addListener(() {
setState(() {});
});
You should not do that because that will fire setState() way too often which causes a bad user experience and high CPU and GPU load.
Im sorry if the title seems off, I couldn't find a better way to frame it. I have a grid of containers on the screen, and I want to be able to draw on the screen selecting and dragging across the screen. I read about the GestureDetector class but it only detects gesture that starts in one widget, I can do a onPanDown, onPanUpdate,onPanEnd but that just gives me the co-ordinates of the cursor, and I didn't feel like it was the most elegant way to do it.(I might be wrong). The Block Class:
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
class Block extends StatefulWidget {
#override
_BlockState createState() => _BlockState();
}
class _BlockState extends State<Block> {
Color boxColor = Colors.white;
#override
Widget build(BuildContext context) {
return InkWell(
onTap: () {
if (boxColor == Colors.blueGrey[900])
boxColor = Colors.white;
else
boxColor = Colors.blueGrey[900];
setState(() {});
},
child: Container(
height: 20,
width: 20,
decoration: BoxDecoration(
color: boxColor,
border: Border.all(
color: Colors.grey,
width: 1,
)),
),
);
}
}
PathFinding Class: (Painting the blocks in a grid):
import 'package:flutter/material.dart';
import 'Block.dart';
class PathFinding extends StatefulWidget {
#override
_PathFindingState createState() => _PathFindingState();
}
class _PathFindingState extends State<PathFinding> {
#override
Widget build(BuildContext context) {
List<List<Widget>> grid = [
...List.generate(
40, (index) => [...List.generate(40, (index) => Block())])
];
return Scaffold(
body: Column(
// crossAxisAlignment: CrossAxisAlignment.center,
children: [
...grid.map((e) => Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [...e],
))
],
),
);
}
}
Just pass a callback as a parameter from your _PathFindingState, whenevr you are creating a Block.
First, add 2 extra params to your Block that can be passed while creating it.
class Block extends StatefulWidget {
final void onTap; // The function from the parent to be called
final int id; // An id that is unique to this Block
Block({ this.onTap, this.id });
#override
_BlockState createState() => _BlockState();
}
Then, in your _BlockState, whenever a tap is detected, call the new function to inform the Block class, which will then inform the _PathFindingState class.
InkWell(
onTap: () {
if (boxColor == Colors.blueGrey[900])
boxColor = Colors.white;
else
boxColor = Colors.blueGrey[900];
setState(() {});
widget.onTap(widget.id); // This line will call the `onTap` function that is present in the `Block`
},
Finally, in your _PathFindingState,
class _PathFindingState extends State<PathFinding> {
void onTap (int id) {
// A Block with `id` = id has been tapped,
}
#override
Widget build(BuildContext context) {
List<List<Widget>> grid = [
...List.generate(
40, (index) => [...List.generate(40,
(index) => Block(id: index, onTap: onTap) // Pass index as id and the onTap function
)])
];
This architecture can be followed for any Gesture that has been detected on any Block and you will receive the callback in the _PathFindingState class and you can do whatever you want with it.
With the code below
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
const MyApp({Key key}) : super(key: key);
#override
Widget build(BuildContext context) => MaterialApp(
home: const MyHomePage(),
);
}
class MyHomePage extends StatelessWidget {
const MyHomePage({Key key}) : super(key: key);
#override
Widget build(BuildContext context) => DefaultTabController(
length: 2,
child: Scaffold(
appBar: AppBar(
title: const Center(
child: Text('use the mouse wheel to scroll')),
bottom: TabBar(
tabs: const [
Center(child: Text('ScrollView')),
Center(child: Text('PageView'))
],
),
),
body: TabBarView(
children: [
SingleChildScrollView(
child: Column(
children: [
for (int i = 0; i < 10; i++)
Container(
height: MediaQuery.of(context).size.height,
child: const Center(
child: FlutterLogo(size: 80),
),
),
],
),
),
PageView(
scrollDirection: Axis.vertical,
children: [
for (int i = 0; i < 10; ++i)
const Center(
child: FlutterLogo(size: 80),
),
],
),
],
),
),
);
}
You can see, running it on dartpad or from this video,
that using the mouse wheel to scroll a PageView provides a mediocre experience (at best),
This is a known issue #35687 #32120, but I'm trying to find a workaround
to achieve either smooth scrolling for the PageView or at least prevent the "stutter".
Can someone help me out or point me in the right direction?
I'm not sure the issue is with PageScrollPhysics;
I have a gut feeling that the problem might be with WheelEvent
since swiping with multitouch scroll works perfectly
The problem arises from chain of events:
user rotate mouse wheel by one notch,
Scrollable receives PointerSignal and calls jumpTo method,
_PagePosition's jumpTo method (derived from ScrollPositionWithSingleContext) updates scroll position and calls goBallistic method,
requested from PageScrollPhysics simulation reverts position back to initial value, since produced by one notch offset is too small to turn the page,
another notch and process repeated from step (1).
One way to fix issue is perform a delay before calling goBallistic method. This can be done in _PagePosition class, however class is private and we have to patch the Flutter SDK:
// <FlutterSDK>/packages/flutter/lib/src/widgets/page_view.dart
// ...
class _PagePosition extends ScrollPositionWithSingleContext implements PageMetrics {
//...
// add this code to fix issue (mostly borrowed from ScrollPositionWithSingleContext):
Timer timer;
#override
void jumpTo(double value) {
goIdle();
if (pixels != value) {
final double oldPixels = pixels;
forcePixels(value);
didStartScroll();
didUpdateScrollPositionBy(pixels - oldPixels);
didEndScroll();
}
if (timer != null) timer.cancel();
timer = Timer(Duration(milliseconds: 200), () {
goBallistic(0.0);
timer = null;
});
}
// ...
}
Another way is to replace jumpTo with animateTo. This can be done without patching Flutter SDK, but looks more complicated because we need to disable default PointerSignalEvent listener:
import 'dart:async';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
class PageViewLab extends StatefulWidget {
#override
_PageViewLabState createState() => _PageViewLabState();
}
class _PageViewLabState extends State<PageViewLab> {
final sink = StreamController<double>();
final pager = PageController();
#override
void initState() {
super.initState();
throttle(sink.stream).listen((offset) {
pager.animateTo(
offset,
duration: Duration(milliseconds: 200),
curve: Curves.ease,
);
});
}
#override
void dispose() {
sink.close();
pager.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Mouse Wheel with PageView'),
),
body: Container(
constraints: BoxConstraints.expand(),
child: Listener(
onPointerSignal: _handlePointerSignal,
child: _IgnorePointerSignal(
child: PageView.builder(
controller: pager,
scrollDirection: Axis.vertical,
itemCount: Colors.primaries.length,
itemBuilder: (context, index) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: Container(color: Colors.primaries[index]),
);
},
),
),
),
),
);
}
Stream<double> throttle(Stream<double> src) async* {
double offset = pager.position.pixels;
DateTime dt = DateTime.now();
await for (var delta in src) {
if (DateTime.now().difference(dt) > Duration(milliseconds: 200)) {
offset = pager.position.pixels;
}
dt = DateTime.now();
offset += delta;
yield offset;
}
}
void _handlePointerSignal(PointerSignalEvent e) {
if (e is PointerScrollEvent && e.scrollDelta.dy != 0) {
sink.add(e.scrollDelta.dy);
}
}
}
// workaround https://github.com/flutter/flutter/issues/35723
class _IgnorePointerSignal extends SingleChildRenderObjectWidget {
_IgnorePointerSignal({Key key, Widget child}) : super(key: key, child: child);
#override
RenderObject createRenderObject(_) => _IgnorePointerSignalRenderObject();
}
class _IgnorePointerSignalRenderObject extends RenderProxyBox {
#override
bool hitTest(BoxHitTestResult result, {Offset position}) {
final res = super.hitTest(result, position: position);
result.path.forEach((item) {
final target = item.target;
if (target is RenderPointerListener) {
target.onPointerSignal = null;
}
});
return res;
}
}
Here is demo on CodePen.
Quite similar but easier to setup:
add smooth_scroll_web ^0.0.4 to your pubspec.yaml
...
dependencies:
...
smooth_scroll_web: ^0.0.4
...
Usage:
import 'package:smooth_scroll_web/smooth_scroll_web.dart';
import 'package:flutter/material.dart';
import 'dart:math'; // only for demo
class Page extends StatefulWidget {
#override
PageState createState() => PageState();
}
class PageState extends State<Page> {
final ScrollController _controller = new ScrollController();
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("SmoothScroll Example"),
),
body: SmoothScrollWeb(
controller: controller,
child: Container(
height: 1000,
child: ListView(
physics: NeverScrollableScrollPhysics(),
controller: _controller,
children: [
// Your content goes here, thoses children are only for demo
for (int i = 0; i < 100; i++)
Container(
height: 60,
color: Color.fromARGB(1,
Random.secure().nextInt(255),
Random.secure().nextInt(255),
Random.secure().nextInt(255)),
),
],
),
),
),
);
}
}
Thanks you hobbister !
Refer to flutter's issue #32120 on Github.
I know that it has been almost 1.5 year from this question, but I found a way that works smoothly. Maybe this will be very helpful whoever read it. Add a listener to your pageview controller with this code (You can make adjustments on duration or nextPage/animateToPage/jumpToPage etc.):
pageController.addListener(() {
if (pageController.position.userScrollDirection == ScrollDirection.reverse) {
pageController.nextPage(duration: const Duration(milliseconds: 60), curve: Curves.easeIn);
} else if (pageController.position.userScrollDirection == ScrollDirection.forward) {
pageController.previousPage(duration: const Duration(milliseconds: 60), curve: Curves.easeIn);
}
});
The issue is with the user settings, how the end-user has set the scrolling to happen with his mouse. I have a Logitech mouse that allows me to turn on or off the smooth scrolling capability via Logitech Options. When I enable smooth scrolling it works perfectly and scrolls as required but in case of disabling the smooth scroll it gets disabled on the project as well. The behavior is as set by the end-user.
Still, if there's a requirement to force the scroll to smooth scroll than can only be done by setting relevant animations. There's no direct way as of now.
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.