I have 2 instances of the same widget(w1 and w2) with a button and an onPressed function. I know to disable the onPress by setting that up with a null value, so far so good. The issue in hand is to solve how to disable w2 onPress when clicking w1 once and enable it again if it is clicked once again.
Even though I send a variable containing if widget has been pressed, disabling never happens because I can not trigger from outside(Widget containing my 2 instances) the setState of each widget separately.
You have a couple of solutions. You could use a more sophisticated state management system (such as Provider or Bloc), but it might be simpler to instead try "lifting state".
"Lifting state" refers to pulling state out of the children and moving it to the parent.
Instead of having the children be stateful, they can become stateless, and the widget that contains them will be made stateful, and will keep track of which buttons are enabled and which are not.
// example button, actual implementation may be different
class MyButton extends StatelessWidget {
final VoidCallback? onPressed;
final String text;
const MyButton({Key? key, this.onPressed, required this.text,}) : super(key: key);
#override
Widget build(BuildContext context) => ElevatedButton(
onPressed: onPressed,
child: Text(text),
);
}
class ButtonContainer extends StatefulWidget {
// boilerplate
}
class ButtonContainerState extends State<ButtonContainer> {
bool isSecondButtonEnabled = true;
void toggleSecondButton() => setState(() => isSecondButtonEnabled = !isSecondButtonEnabled);
#override
Widget build(BuildContext context) => Row(
children: [
MyButton(
text: "Button 1",
onPressed: toggleSecondButton,
),
MyButton(
text: "Button 2",
onPressed: isSecondButtonEnabled ? someOtherFunction : null,
)
],
);
}
Related
I have a class called ApiWidget which accepts a child, this child is going to be one which has onPressed in it, for example, an ElevatedButton.
I want to show this onPressed type child widget in this class but I want to perform some action when that button is pressed. How can I do that? I think provider is just not suitable here.
In other words, I just want to use the child button (for appearance) but I want to perform the operation in this class only.
class ApiWidget extends StatelessWidget {
final Widget child;
ApiWidget(this.child);
#override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () {}, // I want to perform some operation here when this child is pressed.
child: child,
);
}
}
You can get onPressed with the dataType Function.
Example:
import 'package:flutter/material.dart';
class GradientButtonWidget extends StatelessWidget {
final String title;
final VoidCallback onPress;
GradientButtonWidget({
required this.title,
required this.onPress,
});
#override
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: onTap,
child: FittedBox(
child: Text(title),
),
);
}
}
The code you wrote looks like this:
class ApiWidget extends StatelessWidget {
final Widget child;
final VoidCallback onPress;
ApiWidget({
required this.child,
required this.onPress,
});
#override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onPress, //This is how it is called
child: child,
);
}
}
The easiest solution is to wrap your child in AbsorbPointer or IgnorePointer widget. Doing so will ignore the onPressed of your button and you're free to perform the logic within that class.
If you also want to make sure your button receives onPressed event, you can do (child as ElevatedButton).onPressed?.call() in the onTap callback.
GestureDetector(
onTap: () {
// Perform your operation...
},
child: AbsorbPointer(
child: child, // Your passed button
),
)
I try to create custom dropdown for my app. And the dropdown can have three duplicate on the same screen. When each dropdown tapped, there is a variable called isDropdownOpened set to true. The case, when one of the dropdown opened then I wan't the others have to set it's isDropdownOpened variable to false again. So, how to change the isDropdownOpened value automatically when other instances of dropdown tapped?
should i use state management like provider, or bloc and cubit? Or even i can do it with setState.
here is the code.
class SearchDropdownButton extends StatefulWidget {
const SearchDropdownButton({
Key? key,
required this.text,
}) : super(key: key);
final String text;
#override
State<SearchDropdownButton> createState() => _SearchDropdownButtonState();
}
class _SearchDropdownButtonState extends State<SearchDropdownButton> {
late OverlayEntry _categoryBottomBar;
bool isDropdownOpened = false;
#override
Widget build(BuildContext context) {
return Expanded(
child: ElevatedButton(
onPressed: () {
setState(() {
if (isDropdownOpened) {
_categoryBottomBar.remove();
} else {
Overlay.of(context)!.insert(_categoryBottomBar);
}
isDropdownOpened = !isDropdownOpened;
});
},
and the instances on a row.
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: const [
SizedBox(width: 20),
SearchDropdownButton(text: "Consume"),
SizedBox(width: 20),
SearchDropdownButton(text: "Gadget"),
SizedBox(width: 20),
SearchDropdownButton(text: "Fashion"),
SizedBox(width: 20),
],
),
the complete code : https://pastebin.com/QtfDfXzU
Your case is not specific to flutter (React is same). Basic way to do this is moving isDropdownOpened state to parent stateful widget. Corresponding react tutorial is here.
If you want to do this in implicit way then yes, you should use state management library for inter-component state sharing.
I want to keep child widget state using GlobalKey after parent's state is changed. There is a workaround by using Opacity in order to solve the problem, but I wonder why GlobalKey doesn't work as expected in this scenario.
import 'dart:async';
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Retrieve Text Input',
home: MainScreen(),
);
}
}
class MainScreen extends StatefulWidget {
#override
_MainScreenState createState() => _MainScreenState();
}
class _MainScreenState extends State<MainScreen> {
final _key = GlobalKey();
bool _showTimer = true;
#override
void initState() {
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Title'),
centerTitle: false,
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
TextButton(
onPressed: () => setState(() {
_showTimer = !_showTimer;
}),
child: Text('show/hide')),
_showTimer ? TimerWidget(key: _key) : Container()
],
),
));
}
}
class TimerWidget extends StatefulWidget {
const TimerWidget({Key key}) : super(key: key);
#override
_TimerWidgetState createState() => _TimerWidgetState();
}
const int TIME_REMINDING_SECONDS = 480;
class _TimerWidgetState extends State<TimerWidget> {
Timer _timer;
int _start = TIME_REMINDING_SECONDS;
#override
Widget build(BuildContext context) {
return Text(
'${(_start ~/ 60).toString().padLeft(2, '0')}:${(_start % 60).toString().padLeft(2, '0')}',
style: TextStyle(
color: _start > 10 ? Colors.amber : Colors.red, fontSize: 20));
}
#override
initState() {
super.initState();
_startTimer();
}
#override
void dispose() {
_timer.cancel();
super.dispose();
}
_startTimer() {
const oneSec = const Duration(seconds: 1);
_timer = new Timer.periodic(
oneSec,
(Timer timer) => setState(
() {
if (_start < 1) {
timer.cancel();
} else {
_start = _start - 1;
}
},
),
);
}
}
You will see the timer restarts to initial value every times the parent's state is changed. I tried with the solutions here but didn't work.
as an option you can skip GlobalKey and simple use Offstage widget
Offstage(offstage: !_showTimer, child: TimerWidget()),
another answer mentioned Visibility with maintainState parameter.
This is pointless because it uses Offstage under the hood.
By Every time in the previous code every time the state changes it creates a new instance of timer so GlobalKey won't take effect there since its new instance.
Global keys uniquely identify elements. Global keys provide access to
other objects that are associated with those elements, such as
BuildContext. For StatefulWidgets, global keys also provide access to
State.
https://api.flutter.dev/flutter/widgets/GlobalKey-class.html
By the Above statement, the global key is used to access the state within the widgget.
So in your case when TimerWidget() switches it's disposed of its state and not gonna preserve that's why its timer getting reset every time you change state.
--- Update ---
Instead of _showTimer ? TimerWidget(key: _key) : Container()
Use below code:
Visibility(
visible: _showTimer,
maintainState: true,
child: page
)
Here, maintain state is keeping the state of the widget.
Update
The following code moves the scope of a globally unique key so that it will maintain its state while the app lives. When adding this key to an Offset widget, you can show/hide the timer while retaining its state. Without this step, the timer widget would continue to reset as the timer widget is removed and re-added to the rendering tree. I also added the late modifier to the state class _timer variable.
Removing the timer widget from the tree will normally call the dispose method; so one alternative is to use Offstage which is designed to temporarily remove widgets based on state. This seems to be precisely what you are attempting to do. However, the Visibility widget does this same behavior without having to maintain a Global Key (but your focus seemed to be on wanting to leverage a key). Note the other widgets discussed in Visibility notes may provide other alternatives.
Some important considerations:
Animations continue to run when using Offstage widget.
From the docs (on the Offstage widget):
A widget that lays the child out as if it was in the tree, but without
painting anything, without making the child available for hit testing,
and without taking any room in the parent.
Offstage children are still active: they can receive focus and have
keyboard input directed to them.
Animations continue to run in offstage children, and therefore use
battery and CPU time, regardless of whether the animations end up
being visible.
Offstage can be used to measure the dimensions of a widget without
bringing it on screen (yet). To hide a widget from view while it is
not needed, prefer removing the widget from the tree entirely rather
than keeping it alive in an Offstage subtree.
From the docs (on the Visibility widget):
By default, the visible property controls whether the child is
included in the subtree or not; when it is not visible, the
replacement child (typically a zero-sized box) is included instead.
A variety of flags can be used to tweak exactly how the child is
hidden. (Changing the flags dynamically is discouraged, as it can
cause the child subtree to be rebuilt, with any state in the subtree
being discarded. Typically, only the visible flag is changed
dynamically.)
These widgets provide some of the facets of this one:
Opacity, which can stop its child from being painted. Offstage, which can stop its child from being laid out or painted.
TickerMode, which can stop its child from being animated. ExcludeSemantics, which can hide the child from accessibility tools. IgnorePointer, which can disable touch interactions with
the child. Using this widget is not necessary to hide children. The
simplest way to hide a child is just to not include it, or, if a
child must be given (e.g. because the parent is a StatelessWidget)
then to use SizedBox.shrink instead of the child that would
otherwise be included.
import 'dart:async';
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
//create a key that will persist in app scope
var timerKey = GlobalKey();
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Retrieve Text Input',
home: MainScreen(),
);
}
}
class MainScreen extends StatefulWidget {
const MainScreen({Key? key}) : super(key: key);
#override
_MainScreenState createState() => _MainScreenState();
}
class _MainScreenState extends State<MainScreen> {
bool _showTimer = true;
#override
void initState() {
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Title'),
centerTitle: false,
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
TextButton(
onPressed: () => {
setState(() {
_showTimer = !_showTimer;
})
},
child: Text('show/hide')),
//reuse the current timer logic to show/hide the time
Offstage(
offstage: _showTimer,
child: TimerWidget(
key: (timerKey),
),
)
],
),
));
}
}
class TimerWidget extends StatefulWidget {
const TimerWidget({Key? key}) : super(key: key);
#override
_TimerWidgetState createState() => _TimerWidgetState();
}
const int TIME_REMINDING_SECONDS = 480;
class _TimerWidgetState extends State<TimerWidget> {
late Timer _timer;
int _start = TIME_REMINDING_SECONDS;
#override
Widget build(BuildContext context) {
return Text(
'${(_start ~/ 60).toString().padLeft(2, '0')}:${(_start % 60).toString().padLeft(2, '0')}',
style: TextStyle(
color: _start > 10 ? Colors.amber : Colors.red, fontSize: 20));
}
#override
initState() {
super.initState();
_startTimer();
}
#override
void dispose() {
_timer.cancel();
super.dispose();
}
_startTimer() {
const oneSec = const Duration(seconds: 1);
_timer = new Timer.periodic(
oneSec,
(Timer timer) => setState(
() {
if (_start < 1) {
timer.cancel();
} else {
_start = _start - 1;
}
},
),
);
}
}
Nota Bene
Visibility does not require a key at all.
Visibility(
visible: _showTimer,
maintainState: true,
child: TimerWidget(),
),
Original
Review my related question here. You will want to ensure that a Unique Key is available to the parent widget before you start to use the child. My example is pretty in-depth; let me know if you have follow-up issues.
There is a signup section in My App with a main page and 3 widgets form inside it. form 1 and form 2 contain some TextFormField and on the last page, I get some pictures from the user. the user, enter data and click the next button on the main page, now should be validate entered data, and if the data was without any problem, finally send a request with the entered data to a server and step++ and go to the next form.
my question is how can I do this job and get child form data on the main page?
You can use callback function. eg void Function(String)
class MainScreen extends StatelessWidget {
const MainScreen({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
children: [
YourFormWidget(
onFirstNameChanged: (String firstName) {
print(firstName);
},
onLastNameChanged: (String lastName) {
print(lastName);
},
),
TextButton(
onPressed: () {},
child: Text("Submit"),
)
],
),
);
}
}
class YourFormWidget extends StatelessWidget {
const YourFormWidget({
Key? key,
required this.onFirstNameChanged,
required this.onLastNameChanged,
}) : super(key: key);
final void Function(String) onFirstNameChanged;
final void Function(String) onLastNameChanged;
#override
Widget build(BuildContext context) {
return Column(
children: [
TextField(onChanged: onFirstNameChanged),
TextField(onChanged: onFirstNameChanged),
],
);
}
}
This is called state management. https://flutter.dev/docs/development/data-and-backend/state-mgmt/options
I suggest provider. Create the object in the main page and update in one of it's children Widget.
An example with Provider https://flutter.dev/docs/development/data-and-backend/state-mgmt/simple
I was always wondering, why I have to "lift the state up" of some widget down in the tree, to reflect changes in the widget's UI..
Can't I just simply have multiple stateful Widgets? and for example, import the lowest stateful widget down the tree into my top-level Widget, and from there could I not just call some method of my widget which triggers its setState() method and just updates that part in the DOM tree that is concerned with my widget?
And secondly, I would then have to move my properties and other important parts from the lower widget also up into my higher state widget, risking to clutter that class with unrelated functions and properties at some time, in my opinion, React solves that way better by just passing method callbacks down as it suits...
And is there always only one stateful widget in my Flutter app at the highest level?
Parents don't have access to child widgets/states, only the other way around. So you can not "import the lowest stateful widget down the tree into my top-level Widget"
You only have to "lift the state up" when you want to share that state between different branches of the widget tree. Then the children can lookup the parent and access the shared state.
Don't be afraid of StatefulWidget - you can use as many as you need
Research Flutter state management solutions which exist exactly for the purpose of keeping things separated (ChangeNotifier, Provider, BLOC, Redux, etc.)
Can I have multiple widgets with state in my Widgets tree?
The answer is, yes you can create multiple StatefulWidget in your widget. You can also create a callback function from the lowest StatefulWidget with Function(yourcallback). In my opinion, flutter also support component base model, and make as dynamic as possible to customize our own widget.
For example:
child widget
child widget has it's state.
class Child extends StatefulWidget {
final int counter;
final Function(int childCounter) callback; //here is your callback
const Child({
Key key,
this.counter,
this.callback,
}) : super(key: key);
#override
_ChildState createState() => _ChildState();
}
class _ChildState extends State<Child> {
String _childState = '';
#override
Widget build(BuildContext context) {
return Container(
color: Colors.green[400],
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text("Child"),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
IconButton(
icon: Icon(Icons.exposure_minus_1_rounded),
onPressed: () {
this.widget.callback(this.widget.counter - 1); //here is your callback
setState(() {
_childState = "minus one";
});
}),
IconButton(
icon: Icon(Icons.plus_one_rounded),
onPressed: () {
this.widget.callback(this.widget.counter + 1); //here is your callback
setState(() {
_childState = "plus one";
});
})
],
),
Text(_childState)
],
),
);
}
}
Parent Widget
the parent get the callback from the child.
class Parent extends StatefulWidget {
#override
_ParentState createState() => _ParentState();
}
class _ParentState extends State<Parent> {
int _counter = 0;
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Multiple State"),
),
body: Container(
child: Column(
children: [
Child(
counter: _counter,
callback: (childCounter) { //here is callback from the child
print(childCounter);
setState(() {
_counter = childCounter;
});
},
),
Text("Parent"),
Center(
child: Text(_counter.toString()),
)
],
),
),
);
}
}
to change the child state from it's parent
you can use didUpdateWidget() here you can see the docs