Controlling state of a widget from another widget - flutter

I'm using the example in this link:
https://medium.com/#jb.padamchopra/implementing-a-bottom-app-bar-with-floating-action-button-flutter-463560e1324
Widget from the link
The floating action button animation is controlled by a bool clickedCenterFAB
In my code the Widgets inside the animated container are defined in a class and I want, after the user writes a text and presses the button, the animated container to collapse, so logically I want to call setState and assign clickedCenterFAB = false.
I'm new to flutter and I tried calling setState inside the uploadPage widget and try to change clickedCenterFAB but it didn't work. Any idea how to do it guys? thanks for the help in advance.
My widget
AnimatedContainer(
duration: Duration(milliseconds: 300),
//if clickedCentreFAB == true, the first parameter is used. If it's false, the second.
height: clickedCentreFAB
? MediaQuery.of(context).size.height
: 10.0,
width: clickedCentreFAB
? MediaQuery.of(context).size.height
: 10.0,
decoration: BoxDecoration(
borderRadius:
BorderRadius.circular(clickedCentreFAB ? 0.0 : 300.0),
color: Colors.blue,
),
child: clickedCentreFAB ? UploadPage() : null)
EDIT: After following with Michael's solution and keeping up with the comments, this is where I got
class UploadPage extends StatefulWidget {
final Function collapseContainer;
UploadPage(this.collapseContainer);
#override
_UploadPageState createState() =>
_UploadPageState(collapseContainer);
}
class _UploadPageState extends State<UploadPage> {
final Function collapseContainer;
_UploadPageState(this.collapseContainer);
...
...
CustomButton(
() {
if (_controller.text.trim().isNotEmpty) {
DatabaseServices().uploadPost(
FirebaseAuth.instance.currentUser.uid,
_controller.text,
!_isChecked);
collapseContainer;
}
},
'Post',
Colors.white,
),
CustomButton obviously is a widget class that i created to avoid redundency and in the params in a Function which I'm passing here

The reason you can't call setState directly from UploadPage is because setState is a class method, so it's always only associated with the state of the immediately enclosing class instance. If you want to change the state of a Widget that's higher up in the Widget tree, the standard method is to pass a callback to the child component.
In your case, you might add a new property to your UploadPage widget - something like, onButtonPressed:
class UploadPage extends StatelessWidget {
final Function onButtonPressed;
UploadPage({required this.onButtonPressed});
#override
Widget build(BuildContext context) {
return YourWidget(
// ...
child: RaisedButton(onPressed: onButtonPressed),
// ...
);
}
}
Then:
AnimatedContainer(
// ...
child: clickedCentreFAB
? UploadPage(
onButtonPressed: () {
setState(() { return clickedCentreFAB = false; });
},
)
: null,
)
)
As an aside, at a certain point, the state of your app may be complex enough to warrant solutions like bloc, redux, etc - However, for this particular scenario those might be overkill.
If you find yourself having to pass callbacks and properties through several levels of the widget tree, then provider can be immensely helpful.
If the mapping between actions, states, and views becomes very complex, then bloc or redux can help to impose structure onto your project and make it easier to reason about, though you will often need to write more code to get the same tasks done.

Related

Global key is not restoring state sometimes of child widget

Basically, in this example, I am using global key to restore value of by child,
This work,
But sometimes, it appears that the global key does not restore the value of the child items.
To make it clear, I have added a checkbox widget and a slider widget to the first and last pages of this code (green and red).
When I change the slider value on the first page and try to see it on the third page, it works fine.
Same thing if I change checkbox value (like true) on first page and then click on third page it shows mostly true, but sometimes does not.
I am unable to understand the reason behind this issue. Please refer to the following gif to better understand my question.
import 'package:flutter/material.dart';
void main() {
runApp(MaterialApp(home: MyApp()));
}
class MyApp extends StatelessWidget {
final _key = GlobalKey();
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("Global Key Example")),
body: PageView.builder(
itemCount: 3,
itemBuilder: (context, index) {
switch (index) {
case 0:
return Container(
color: Colors.green,
child: ActionPage(_key),
);
break;
case 1:
return Container(
color: Colors.blue,
child: Text("Blank Page"),
);
break;
case 2:
return Container(
color: Colors.red,
child: ActionPage(_key),
);
break;
default:
throw "Not found";
}
},
),
);
}
}
class ActionPage extends StatefulWidget {
#override
_ActionPageState createState() => _ActionPageState();
ActionPage(key) : super(key: key);
}
class _ActionPageState extends State<ActionPage> {
bool _switchValue = false;
double _sliderValue = 0.5;
#override
Widget build(BuildContext context) {
return Column(
children: [
Switch(
value: _switchValue,
onChanged: (v) {
setState(() => _switchValue = v);
},
),
Slider(
value: _sliderValue,
onChanged: (v) {
setState(() => _sliderValue = v);
},
)
],
);
}
}
I'd recommened you to not use a GlobalKey here. Make sure to pass the needed data not the widget sate. If you take a look at the documentation the following pitfalls are listed:
GlobalKeys should not be re-created on every build. They should
usually be long-lived objects owned by a State object, for example.
Creating a new GlobalKey on every build will throw away the state of
the subtree associated with the old key and create a new fresh subtree
for the new key. Besides harming performance, this can also cause
unexpected behavior in widgets in the subtree. For example, a
GestureDetector in the subtree will be unable to track ongoing
gestures since it will be recreated on each build.
Instead, a good practice is to let a State object own the GlobalKey,
and instantiate it outside the build method, such as in
State.initState.
So you have to make sure that two widgets with the same key are never rendered on the screen at the same time. With a PageView this obviously can happen since you can display (eventhough there might be a transition state) two of your widgets with the same key at the same time. This can mess things up. Also they changed quite a bit in the past implementation wise if I recall correctly.
You should either pass the value directly to your widget or use something like an InheritedWidget to be safe.
Generally speaking I'd always try to avoid using GlobalKeys because they are quite complex and can have side effects which might not be always fully comprehensible. Also there are good alternatives.

Best way to pass widgets to child widget in flutter

I'm new to flutter but I have a widget that wraps a custom painter. I am trying to get it to work so I can supply a Widget to this child widget's constructor and then use that widget as the child of the custom painter.
For example:
class MyPainterWrapper extends StatefulWidget {
Widget _childWidget;
SceneRender([this._childWidget]);
#override
State<StatefulWidget> createState() {
return new MyPainterWrapperState(_childWidget);
}
}
class MyPainterWrapperState extends State<SceneRender> {
Widget _childWidget;
MyPainterWrapperState(this._childWidget);
#override
Widget build(BuildContext context) {
return Column(
children: [
CustomPaint(painter: MyPainter(), child: _childWidget)
],
);
}
}
And in another widget (called testWidget):
bool _answerCorrect = false;
bool _answerInputted = false;
var _msgController = TextEditingController();
FocusNode _answerFieldFocus = new FocusNode();
DictionaryEntry _currentEntry;
void _checkIfCorrect(String answerGiven) {
setState(() {
_answerCorrect = false;
if (_currentEntry.Full_Word == answerGiven)
_answerCorrect = true;
else if (_currentEntry.dictReadings.isNotEmpty) {
for (AlternateDictionaryEntryReading entryReading in _currentEntry
.dictReadings) {
if (entryReading.Alternate_Reading == answerGiven) {
_answerCorrect = true;
break;
}
}
}
_answerInputted = true;
_msgController.clear();
});
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('test'),
),
body: MyPainterWrapper(Center(Container(Column(children: <Widget>[
if (_answerCorrect && _answerInputted) Text('CORRECT!'),
if (!_answerCorrect && _answerInputted) Text('WRONG:'),
if (_answerInputted)
Text(_currentEntry.Full_Word),
if (_answerInputted)
for(AlternateDictionaryEntryReading reading in _currentEntry.dictReadings)
Text(reading.Alternate_Reading),
Container(
constraints: BoxConstraints.expand(
height: 100,
width: 1000
),
child: SingleChildScrollView(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
for (DictionaryTranslation translation in _currentEntry.dictTranslations)
Text(translation.Translation),
],
)
),
),
Text('Enter Answer:',),
TextField(
controller: _msgController,
focusNode: _answerFieldFocus,
onSubmitted: (String value) {
_checkIfCorrect(value);
_answerFieldFocus.requestFocus();
},
)
This works to render the first time correctly, but any setState calls from checkIfCorrect from testWidget do not force the child widget to rebuild. I've tried testing it this way and it works, so that leads me to believe that I'm passing the widget incorrectly to have it redrawn via setState
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('test'),
),
body: CustomPaint(painter: TestPainter(), child: Center(
child: Container(...))
Your MyPainterWrapperState class reads like you are creating a new _childWidget property inside your state (which has a default value of null). You are then using it to initialize a new instance of MyPainterWrapperState, then throwing away the instance of MyPainterWrapper() that you just created.
You're not actually using the stateful part of your stateful widget at all; You're just calling a method that returns something once.
That said, your approach is basically right, but the implementation is off a little.
My advice:
First, you can use named properties to supply constructor arguments to your class. I've made that change in the code snippet shown below.
The State class of a stateful widget supplies a widget property that should be used to reference the properties of the widget that created it. The State widget should also have a solitary initializer that accepts no arguments.
Also good to know is that the State class provides an initState() method that you can override to initialize any class local variables that you declare. This should be used to give a value to your _childWidget property.
Finally, anything you expect to be rebuilt should be inside the MyPainterWrapperState() class. Your SceneRender() method doesn't appear in your code, but you might want to move it into MyPainterWrapperState() if you expect the scene to change based on the value of the child.
I suggest these changes.
Pass arguments to MyPainterWrapper via named arguments.
Remove the argument to MyPainterWrapperState() and reference the child through the widget property supplied to the state.
Initialize _childWidget by overriding initState()
If SceneRender() does anything that depends on the value of _childWidget, move it to the build() method of MyPainterWrapperState().
The Flutter docs are great, and the Flutter team has created a ton of helpful YouTube videos that explain in a couple of minutes examples of how to use dozens of them. For a better understanding of StatefulWidget, you can read about it here.
If you make these changes, your solution would look something like the code snippet below.
Presuming you make those changes, you would alter your call to MyPainterWrapper() to use named properties.
Change this line
body: MyPainterWrapper(Center(Container(Column(children: <Widget>[
To this
body: MyPainterWrapper(child: Center(Container(Column(children: <Widget>[
This won't get you to done, but it will get you closer. I also haven't run this through a compiler, so there are probably errors in the snippet, but this should serve to illustrate the approach.
class MyPainterWrapper extends StatefulWidget {
MyPainterWrapper(
{
#required child: this.childWidget,
}
);
final Widget childWidget;
// Not sure what this does, but I'm pretty sure that it doesn't
// provide anything into the widget tree.
// If it mutates its arguments, then you might still need it.
// SceneRender([this._childWidget]);
#override
State<StatefulWidget> createState() {
// Note no arguments.
return new MyPainterWrapperState();
}
}
class MyPainterWrapperState extends State<MyPainterWrapper> {
// This is an uninitialized variable inside this class.
Widget _childWidget;
// MyPainterWrapperState(this._childWidget);
// Initialize state variables here.
#override initState() {
// Assigns the widget class initializer to your state variable.
_childWidget = widget.childWidget;
}
#override
Widget build(BuildContext context) {
return Column(
children: [
CustomPaint(painter: MyPainter(), child: _childWidget)
],
);
}
}```

Pass parent state to generic child widget

I'm building a flutter app, and I have built a customized AppBar widget for it. This appbar has a SearchBar widget, which calls whatever callback is passed to it onChange. Now, there are multiple screens that use this SearchBar, and each of them will do something different with the user input. But I've noticed that on each of the screens that use the appbar, I'd have to use a state to control the SearchBar inputted text. So, I'm trying to not have to create the state for every screen, and have a Widget that wraps my screens, and controls the input in it's state, and passes it's state down to the child I provide to this apps. This would be similar to React's Higher Order Components, which wrap another component and can pass props to it.
This seems to me like a good design pattern, but I don't know how to implement it. Since the child widget that I would pass to this second order component that would wrap my screens, won't be getting any info from it, the child is simply passed as a widget (note: the code is also doing other stuff, working as a general wraper to replace similar repetitive code in all of my screens):
class CustomScaffold extends StatefulWidget {
final ScreenInfo appBarInfo;
final Builder child;
final Widget bottomBarApp;
final Widget appBar;
final EdgeInsetsGeometry padding;
const CustomScaffold({
#required this.child,
Key key,
this.bottomBarApp,
this.appBar,
this.padding,
this.appBarInfo,
}) : super(key: key);
#override
_CustomScaffoldState createState() => _CustomScaffoldState();
}
class _CustomScaffoldState extends State<CustomScaffold> {
String searchTerm = '';
#override
Widget build(BuildContext context) {
return Scaffold(
body: SingleChildScrollView(
child: Column(
children: [
widget.appBar ??
GradientAppBar(
title: widget.appBarInfo.label,
searchBar: SearchBar(
callback: (String input) {
setState(() {
searchTerm = input;
});
},
placeholder: widget.appBarInfo.searchPlaceholder,
),
),
Padding(
padding: widget.padding,
child: widget.child,
),
],
),
),
bottomNavigationBar: widget.bottomBarApp ?? CustomBottomBarNavigator(),
);
}
}
I'm thinking that I could passa builder instead of a widget as the child, but I'm not sure how this would work. Also, I'm still learning bloc in general, so I'm not sure if it would be a good idea to use bloc for this. I'm guessing bloc's purpose is a little different, and would complicate this specific pattern.
Does this idea make sense? What would be the best way to implement it?
Thanks in advance.

flutter hover-like touch handling - detect touch from different widgets without lifting the finger

Assume I have 3 Containers on the screen that react touching by changing their color. When user's finger is on them, they should change color and when touching ends they should turn back to normal. What I want is that those containers to react when the user finger/pointer on them even if the touch started on a random area on the screen, outside of the containers. So basically what i am looking for is something just like css hover.
If i wrap each container with GestureDetector seperately, they will not react to touch events which start outside of them. On another question (unfortunately i dont have the link now) it is suggested to wrap all the containers with one GestureDetector and assign a GlobalKey to each to differ them from each other.
Here is the board that detects touch gestures:
class MyBoard extends StatefulWidget {
static final keys = List<GlobalKey>.generate(3, (ndx) => GlobalKey());
...
}
class _MyBoardState extends State<MyBoard> {
...
/// I control the number of buttons by the number of keys,
/// since all the buttons should have a key
List<Widget> makeButtons() {
return MyBoard.keys.map((key) {
// preapre some funcitonality here
var someFunctionality;
return MyButton(key, someFunctionality);
}).toList();
}
#override
Widget build(BuildContext context) {
return GestureDetector(
onTapDown: (tapDownDetails) {
handleTouch(context, tapDownDetails.globalPosition);
},
onTapUp: (tapUpDetails) {
finishTouch(context);
},
onPanUpdate: (dragUpdateDetails) {
handleTouch(context, dragUpdateDetails.globalPosition);
},
onPanEnd: (panEndDetails) {
finishTouch(context);
},
onPanCancel: () {
finishTouch(context);
},
child: Container(
color: Colors.green,
width: 300.0,
height: 600.0,
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: makeButtons(),
),
),
);
}
Here is the simple MyButton:
class MyButton extends StatelessWidget {
final GlobalKey key;
final Function someFunctionality;
MyButton(this.key, this.someFunctionality) : super(key: key);
#override
Widget build(BuildContext context) {
return Consumer<KeyState>(
builder: (buildContext, keyState, child) {
return Container(
width: 100.0,
height: 40.0,
color: keyState.touchedKey == this.key ? Colors.red : Colors.white,
);
},
);
}
}
In the _MyBoardState that's how i handle detecting which MyButton is touched:
MyButton getTouchingButton(Offset globalPosition) {
MyButton currentButton;
// result is outside of [isTouchingMyButtonKey] for performance
final result = BoxHitTestResult();
bool isTouchingButtonKey(GlobalKey key) {
RenderBox renderBox = key.currentContext.findRenderObject();
Offset offset = renderBox.globalToLocal(globalPosition);
return renderBox.hitTest(result, position: offset);
}
var key = MyBoard.keys.firstWhere(isTouchingButtonKey, orElse: () => null);
if (key != null) currentButton = key.currentWidget;
return currentButton;
}
I am using provider package and instead of rebuilding the whole MyBoard with setState only the related part of related MyButton will be rebuilded. Still each button has a listener which rebuilds at every update and I am using GlobalKey. On the flutter docs it says:
Global keys are relatively expensive. If you don't need any of the features listed above, consider using a Key, ValueKey, ObjectKey, or UniqueKey instead.
However if we look at getTouchingButton method I need GlobalKey to get renderBox object and to get currentWidget. I also have to make a hitTest for each GlobalKey. This computation repeats when the onPanUpdate is triggered, which means when the user's finger moves a pixel.
UI is not updating fast enough. With a single tap (tapDown and tapUp in regular speed) you usually do not see any change on the MyButtons.
If I need to sum up my question, How can i detect same single touch (no lifting) from different widgets when finger is hoverin on them in more efficient and elegant way?
Since no one answered yet, I am sharing my own solution which I figured out lately. Now I can see the visual reaction everytime i touch. It is fast enough, but still it feels like there is a little delay on the UI and it feels a little hacky instead of a concrete solution. If someone can give better solution, I can mark it as accepted answer.
My solution:
First things first; since we have to detect Pan gesture and we are using onPanUpdate and onPanEnd, I can erase onTapDown and onTapUp and instead just use onPanStart. This can also detect non-moving simple tap touches.
I also do not use Provider and Consumer anymore, since it rebuilds all the Consumers at every update. This is really a big problem when the the number of MyButtons increase. Instead of keeping MyButton simple and dummy, I moved touch handling work into it. Each MyButton hold the data of if they are touched at the moment.
Updated button is something like this:
class NewButton extends StatefulWidget {
NewButton(Key key) : super(key: key);
#override
NewButtonState createState() => NewButtonState();
}
class NewButtonState extends State<NewButton> {
bool isCurrentlyTouching = false;
void handleTouch(bool isTouching) {
setState(() {
isCurrentlyTouching = isTouching;
});
// some other functionality on touch
}
#override
Widget build(BuildContext context) {
return Container(
width: 100.0,
height: 40.0,
color: isTouched ? Colors.red : Colors.white,
);
}
}
Notice that NewButtonState is not private. We will be using globalKey.currentState.handleTouch(bool) where we detect the touch gesture.

Animated Container is not animating when list changed

import "package:flutter/material.dart";
import "dart:async";
class JoinScreen extends StatefulWidget {
#override
_JoinScreenState createState() {
return _JoinScreenState();
}
}
class _JoinScreenState extends State<JoinScreen> {
List<Widget> widgetList = [];
#override
void initState() {
new Timer(const Duration(milliseconds: 100), () {
print('timeout');
setState(() {
widgetList.add(secondHalf());
});
});
new Timer(const Duration(milliseconds: 1000), () {
print('timeout');
setState(() {
widgetList.add(firstHalf());
});
});
super.initState();
}
#override
Widget build(BuildContext context) {
return AnimatedContainer(
duration: Duration(seconds: 2),
child: Column(
children: widgetList,
),
);
}
Widget firstHalf() {
return Expanded(
child: Container(
decoration: BoxDecoration(color: Colors.blueAccent),
),
);
}
Widget secondHalf() {
return Expanded(
child: Container(
decoration: BoxDecoration(color: Colors.pinkAccent),
),
);
}
}
If i changed the width and height of the container with the help of timer and setstate, it animates. But when adding two new list of widgets to the build function, nothing animates.
I want to have a expanding animation. Because i am using expanded, i am not able to give specific height which is meaningless with expanded.
How do i do this?
Use AnimatedSize instead of AnimatedContainer.
for AnimatedSize to work we need to use a mixin called SingleTickerProviderStateMixin on the _JoinScreenState and set the vsync property to the current instance (this) after that the AnimatedSize will look for changes on its child and animate accordingly,
Update:
TickerProvider vsync is deprecated after Flutter v2.2.0-10.1.pre. It is now implemented in the widget (AnimatedSize) itself. for reference check the source code.
here is your code
import "package:flutter/material.dart";
import "dart:async";
class JoinScreen extends StatefulWidget {
#override
_JoinScreenState createState() {
return _JoinScreenState();
}
}
class _JoinScreenState extends State<JoinScreen>
with SingleTickerProviderStateMixin {
List<Widget> widgetList = [];
#override
void initState() {
new Timer(const Duration(milliseconds: 100), () {
print('timeout');
setState(() {
widgetList.add(secondHalf());
});
});
new Timer(const Duration(milliseconds: 1000), () {
print('timeout');
setState(() {
widgetList.add(firstHalf());
});
});
super.initState();
}
#override
Widget build(BuildContext context) {
return AnimatedSize(
vsync: this,
duration: Duration(seconds: 2),
child: Column(
children: widgetList,
),
);
}
Widget firstHalf() {
return Expanded(
child: Container(
decoration: BoxDecoration(color: Colors.blueAccent),
),
);
}
Widget secondHalf() {
return Expanded(
child: Container(
decoration: BoxDecoration(color: Colors.pinkAccent),
),
);
}
}
This is almost certainly a duplicate as I think I've answered this before, but I can't find the question easily so I'll just answer it again.
To understand why this isn't working, first you have to understand how Dart does comparisons on objects. If an object is a primitive, simple, or has comparison functions/operators defined (i.e. int, boolean, String, etc) dart can compare the objects. If an object is more complicated and doesn't define compareTo, operator=, operator< or operator>, dart doesn't know how to do that type of comparison on it. Instead, a comparison turns into "is object a the same object as object b".
This is important because Flutter is lazy. It doesn't want to have to rebuild widgets unless it absolutely has to. So when you change the state with setState, flutter then comes along and takes a look at your State to see if it has actually changed. With the cases of height or width it's easy - it can check that those have changed.
The reason it doesn't work when you mutate your list is exactly that; you're mutating an existing list. So when dart checks whether oldState.widgetList == newState.widgetList, it's not actually comparing whether each element of the list is the same but rather checking whether the list is the same. Since it's the same object, the list shows as being the same so flutter moves on to the next step without rebuilding.
There are three main ways to get around this. The first is to make a copy of the list each time you edit it. Depending on how many elements are in the list, this could be a bad idea - when you copy the elements you're not actually copying each little bit of information, but it still is an O(n) operation.
The second is to maintain a separate variable in the State. The reason this helps is because if any part of the state has changed, it triggers a rebuild of all the widgets i.e. calls the build function, whether or not the actual property that each widget uses is 'changed' (mostly because it would be very difficult to manage keeping track of that for each and every widget that is built). I personally do it this way - I maintain an integer counter that I just increment each time a change happens in the list. It might not be the cleanest solution, but it is performant and pretty simple!
The last way would be to implement your own list that does do a 'deep' comparison (i.e. checks whether number of elements are the same, and then possibly checks whether each element is the same). This isn't done by default in dart's lists because it would be easy to cause performance issues if you start comparing strings without realizing each element in the list could be used as part of the comparison.