Introduction
Sometimes, it is necessary to get access to a StatefulWidget from another Widget to perform some operations or to link two or more StatefulWidgets to realize some business logic.
As an example, suppose that you have a 2-state button, which is either on' or 'off'. This is easy to do.
However, if you now consider a series of such buttons where your business rule says: "only one of these buttons may be 'on' at a time". How do you achieve this? More complicated, imagine the case where these buttons would be part of different widget trees...
Several ways to handle this case exist and this article is going to present a couple of solutions. Some will be straightforward, some will be more complex and some will be better than others but the objective of this article is not to be exhaustive but to make you understand the general principle...
Let's start...
Basic 2-state button
Let's first write the basic code of a 2-state button. The code could look like the following:
@immutable
class ButtonTwoStates extends StatefulWidget {
const ButtonTwoStates({
super.key,
required this.index,
required this.onChange,
this.isOn = false,
});
final bool isOn;
final int index;
final ValueChanged<bool> onChange;
@override
_ButtonTwoStatesState createState() => _ButtonTwoStatesState();
}
class _ButtonTwoStatesState extends State<ButtonTwoStates> {
//
// Internal state
//
late bool _isOn;
@override
void initState() {
super.initState();
_isOn = widget.isOn;
}
@override
Widget build(BuildContext context) {
return InkWell(
onTap: _toggleState,
child: Container(
width: 80.0,
height: 56.0,
color: _isOn ? Colors.red : Colors.green,
),
);
}
//
// The user taps on the button and we want to toggle
// its internal state
//
void _toggleState() {
_isOn = !_isOn;
if (mounted) {
setState(() {});
}
widget.onChange?.call(_isOn);
}
}
Explanation:
- line 23: internal state of this button
- line 28: we initialize the state, based on the information provided to the Widget
- line 34: when we tap on the button, we call the _toggleState method
- line 48: we simply toggle the internal state of the button,
- lines 49-51: we ask the button to rebuild, making sure that the button is still there
- line 52: we invoke the callback (if mentioned)
Note about the '?.call()'
The following code: "widget.onChange?.call(_isOn);" is equivalent to
if (widget.onChange != null){ widget.onChange(_isOn); }
Basic page
Let's now create a page that contains 2 of these buttons. The code could look like the following:
class TestPage extends StatefulWidget {
const TestPage({
super.key,
});
@override
_TestPageState createState() => _TestPageState();
}
class _TestPageState extends State<TestPage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('2-state buttons'), centerTitle: true,),
body: Center(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ButtonTwoStates(
index: 0,
isOn: false,
onChange: (bool isOn) {
debugPrint('My first button is on ? $isOn');
},
),
const SizedBox(width: 8),
ButtonTwoStates(
index: 1,
isOn: false,
onChange: (bool isOn) {
debugPrint('My second button is on ? $isOn');
},
),
],
),
),
);
}
}
We obtain the following screen:

When the user taps on one of the buttons, the corresponding one's color toggles.
How to create a relationship between the 2 buttons?
Let's now assume that we need to have the following behavior: "when one button is 'on', the other is 'off'". How could we achieve this?
At first thoughts, we could update the code of the page as follows:
class _TestPageState extends State<TestPage> {
final List<bool> _buttonIsOn = [true, false];
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('2-state buttons'),
centerTitle: true,
),
body: Center(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ButtonTwoStates(
index: 0,
isOn: _buttonIsOn[0],
onChange: (bool isOn) {
_onButtonValueChange(index: 0, isOn: isOn);
},
),
const SizedBox(width: 8),
ButtonTwoStates(
index: 1,
isOn: _buttonIsOn[1],
onChange: (bool isOn) {
_onButtonValueChange(index: 1, isOn: isOn);
},
),
],
),
),
);
}
void _onButtonValueChange({int index, bool isOn}) {
final int indexOtherButton = (index + 1) % _buttonIsOn.length;
_buttonIsOn[index] = isOn;
_buttonIsOn[indexOtherButton] = !isOn;
if (mounted){
setState(() {});
}
}
}
However, despite this modification which seems logical, it does not work... why?
The problem comes from the ButtonTwoStates button which is a StatefulWidget.
If you refer to my past article on the notion of Widget - State - Context, you will remember that:
- In a StatefulWidget, the Widget part is immutable (thus, never changes) and has to be seen as a kind of configuration
- the initState() method is only run ONCE for the whole live of the StatefulWidget
So why doesn't it work?
Simply because, by simply passing a "new value" to the ButtonState.isOn, its corresponding State will not change as the initState method will not be called a second time.
How can we solve this?
Still referring to my past article (Widget - State - Context), I did only mention one overriddable method: didUpdateWidget without explaining it...
didUpdateWidget
This method is invoked when the parent Widget rebuilds and provides different arguments to this Widget (of course, this Widget needs to keep the same Key and runtimeType).
In this case, Flutter calls the didUpdateWidget(oldWidget) method, providing the old Widget in argument.
It is therefore up to the State instance to take the appropriate actions based on the potential variations between the "old" Widget arguments and the "current" Widget arguments.
...and this is exactly what we need as:
- we are rebuilding the parent Widget (line: 44)
- we are providing the children widgets with new values (lines: 18 and 26)
Let's apply the necessary modification to our _ButtonTwoStatesState source code as follows:
class _ButtonTwoStatesState extends State<ButtonTwoStates> {
//
// Internal state
//
late bool _isOn;
@override
void initState() {
super.initState();
_isOn = widget.isOn;
}
@override
void didUpdateWidget(ButtonTwoStates oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.isOn != oldWidget.isOn){
_isOn = widget.isOn;
}
}
@override
Widget build(BuildContext context) {
return InkWell(
onTap: _onTap,
child: Container(
width: 80.0,
height: 56.0,
color: _isOn ? Colors.red : Colors.green,
),
);
}
//
// The user taps on the button
//
void _onTap() {
_isOn = !_isOn;
if (mounted) {
setState(() {});
}
widget.onChange?.call(_isOn);
}
}
Thanks to line #18, the internal value "_isOn" is now updated and the button will be rebuilt using this new value.
OK, now it works, however, the whole business logic related to the buttons is taken on board by the TestPage, which works great with 2 buttons but if you now need to consider additional buttons, it might become a nightmare moreover if the buttons are not direct children of the very same parent!
Notion of GlobalKey
The GlobalKey class generates a unique key across the entire application. But the main interest of using a GlobalKey in the context of this article is that
The GlobalKey provides an access to the State of a StatefulWidget, using the currentState getter.
So, what does this mean?
Let's first apply a change to the original source code of the ButtonTwoStates to make its State public. This is very easy: we simply remove the "_" sign from the name of the class "_ButtonTwoStatesState". This makes the class public. We then obtain:
class ButtonTwoStates extends StatefulWidget {
...
@override
ButtonTwoStatesState createState() => ButtonTwoStatesState();
}
class ButtonTwoStatesState extends State<ButtonTwoStates> {
...
}
Now, let's use the GlobalKey inside our TestPage ...
class _TestPageState extends State<TestPage> {
final List<bool> _buttonIsOn = [true, false];
final List<GlobalKey<ButtonTwoStatesState>> _buttonKeys = [
GlobalKey<ButtonTwoStatesState>(),
GlobalKey<ButtonTwoStatesState>(),
];
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('2-state buttons'),
centerTitle: true,
),
body: Center(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ButtonTwoStates(
key: _buttonKeys[0],
index: 0,
isOn: _buttonIsOn[0],
onChange: (bool isOn) {
_onButtonValueChange(index: 0, isOn: isOn);
},
),
const SizedBox(width: 8),
ButtonTwoStates(
key: _buttonKeys[1],
index: 1,
isOn: _buttonIsOn[1],
onChange: (bool isOn) {
_onButtonValueChange(index: 1, isOn: isOn);
},
),
],
),
),
);
}
void _onButtonValueChange({int index, bool isOn}) {
final int indexOtherButton = (index + 1) % _buttonIsOn.length;
_buttonIsOn[index] = isOn;
_buttonIsOn[indexOtherButton] = !isOn;
// setState(() {});
_buttonKeys[indexOtherButton].currentState.resetState();
}
}
Explanation
- lines 4-7: we generate an array of GlobalKey referring to ButtonTwoStatesState
- line 21: we tell the first button which is its key
- line 30: same for button 2
- line 49: we no longer need to fully rebuild the page.
- line 50: as we are going to see here below, we call the button which needs to be reset
So, the change to be applied to the ButtonTwoStatesState code is limited to adding a new method as follows:
class ButtonTwoStatesState extends State<ButtonTwoStates> {
...
//
// Reset the state
//
void resetState(){
_isOn = false;
if (mounted) {
setState(() {});
}
}
}
Explanation
- line 7: As you can see, the method is public (no "_" prefix), which is necessary to be accessed from the TestPage (as its source code does not reside in the same physical file)
- this code simply resets the state of this button and proceeds with a rebuild.
As we can see, this solution also works however, here again, the whole business logic related to the buttons is taken on board by the TestPage...
Another solution: a Controller
Another solution would consist in using a Controller class. In other words, this class would be used to control the logic and the buttons.
The idea of such a controller is that each button would tell the controller when it is selected. Then the controller will take the decision on the action(s) to take.
Here is a very basic implementation of such possible controller:
class ButtonTwoStatesControllers {
ButtonTwoStatesControllers({
required this.buttonsCount,
this.onChange,
int? selectedIndex,
}) : assert(buttonsCount > 0),
assert((selectedIndex == null ||
(selectedIndex >= 0 &&
selectedIndex < buttonsCount))) {
_selectedIndex = selectedIndex ?? -1;
_registeredButtons = List.generate(buttonsCount, (index) => null);
}
//
// Total number of buttons
//
final int buttonsCount;
//
// Callback to be invoked when the selection changes
//
final ValueChanged<int>? onChange;
// Index of the currently selected button
late int _selectedIndex;
// List of registered buttons
late List<ButtonTwoStatesState?> _registeredButtons;
//
// Registers a button and returns whether
// the button is selected
//
bool registerButton(ButtonTwoStatesState button) {
final int buttonIndex = button.index;
assert(buttonIndex >= 0 && buttonIndex < buttonsCount);
assert(_registeredButtons[buttonIndex] == null);
_registeredButtons[buttonIndex] = button;
return _selectedIndex == buttonIndex;
}
//
// When a button is selected, we record the information
// and unselect the others
//
void setButtonSelected(int buttonIndex) {
assert(buttonIndex >= 0 && buttonIndex < buttonsCount);
assert(_registeredButtons[buttonIndex] != null);
if (_selectedIndex != buttonIndex) {
_selectedIndex = buttonIndex;
for (int index = 0; index < buttonsCount; index++) {
_registeredButtons[index]?.isOn(index == _selectedIndex);
}
// Notify about the change
onChange?.call(buttonIndex);
}
}
}
I hope that the code is self-explanatory:
- the assert are there to ensure during the development time that the boundaries are respected
- lines #35-44: when a button registers itself, its State is recorded in the _registeredButtons array
- lines #50-63: when we change the selected button, we inform the registered buttons about their new state
Let's have a look at the modifications to be applied to both TestPage and to ButtonTwoStatesState:
TestPage:
class _TestPageState extends State<TestPage> {
final ButtonTwoStatesControllers controller = ButtonTwoStatesControllers(
buttonsCount: 2,
selectedIndex: 0,
onChange: (int selectedIndex){
// Do whatever needs to be done
},
);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('2-state buttons'),
centerTitle: true,
),
body: Center(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ButtonTwoStates(
controller: controller,
index: 0,
),
const SizedBox(width: 8),
ButtonTwoStates(
controller: controller,
index: 1,
),
],
),
),
);
}
}
Explanation
The TestPage simply initializes the ButtonTwoStatesController, mentions the number of buttons to consider, which one is selected and a callback method to be invoked when the button selection changes.
This controller is passed in arguments to each button (lines 23 & 28).
ButtonTwoStatesState:
@immutable
class ButtonTwoStates extends StatefulWidget {
const ButtonTwoStates({
super.key,
required this.controller,
this.index,
});
final ButtonTwoStatesControllers controller;
final int? index;
@override
ButtonTwoStatesState createState() => ButtonTwoStatesState();
}
class ButtonTwoStatesState extends State<ButtonTwoStates> {
late bool _isOn;
@override
void initState() {
super.initState();
_isOn = widget.controller.registerButton(this);
}
@override
Widget build(BuildContext context) {
return InkWell(
onTap: _onTap,
child: Container(
width: 80.0,
height: 56.0,
color: _isOn ? Colors.red : Colors.green,
),
);
}
//
// The user taps on the button
//
void _onTap() {
widget.controller.setButtonSelected(index);
}
//
// Sets the state
//
void isOn(bool isOn) {
if (_isOn != isOn) {
if (mounted) {
setState(() {
_isOn = isOn;
});
}
}
}
//
// Getter for the index
//
int get index => widget.index ?? -1;
}
Explanation
- line #23: At initialization time, the button registers itself ("this") against the controller which returns the status (= _isOn) of this button.
- line #42: When the user taps on the button, it informs the controller by providing its own "index"
- line #61: Convenient way for the Button to give its index number
- lines #48-56: Used by the controller to inform the button about its status.
This solution is already much better as the business logic related to the buttons has been externalized to the controller (even for the buttons themselves).
It is also much easier to add a button to the page.
Even if this solution works, this is, however, still not ideal as in a real application, buttons will most probably not all be inserted into the Widget tree by the same parent.
Provider
In order to solve this issue, let's make the controller available to any Widgets, part of the TestPage. To do this, let's use a Provider.
Let's look at the changes to apply:
class TestPage extends StatelessWidget {
const TestPage({super.key});
@override
Widget build(BuildContext context) {
return Provider<ButtonTwoStatesController>(
create: (BuildContext context) => ButtonTwoStatesController(
buttonsCount: 3,
selectedIndex: 0,
onChange: (int selectedIndex) {
// Do whatever needs to be done
print('selectedIndex: $selectedIndex');
},
),
child: Scaffold(
appBar: AppBar(
title: const Text('2-state buttons'),
centerTitle: true,
),
body: Center(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: const [
ButtonTwoStates(
index: 0,
),
SizedBox(width: 8),
ButtonTwoStates(
index: 1,
),
SizedBox(width: 8),
ButtonTwoStates(
index: 2,
),
],
),
),
),
);
}
}
As we can see, the Page may now become a StatelessWidget and we do not need to pass the controller to each button.
class ButtonTwoStates extends StatefulWidget {
const ButtonTwoStates({
super.key,
this.index,
});
final int? index;
@override
ButtonTwoStatesState createState() => ButtonTwoStatesState();
}
class ButtonTwoStatesState extends State<ButtonTwoStates> {
late bool _isOn;
late ButtonTwoStatesController controller;
@override
void initState() {
super.initState();
controller = Provider.of<ButtonTwoStatesController>(context, listen: false);
_isOn = controller.registerButton(this);
}
@override
Widget build(BuildContext context) {
return InkWell(
onTap: _onTap,
child: Container(
width: 80.0,
height: 56.0,
color: _isOn ? Colors.red : Colors.green,
),
);
}
//
// The user taps on the button
//
void _onTap() {
controller.setButtonSelected(index);
}
//
// Sets the state
//
void isOn(bool isOn) {
if (_isOn != isOn) {
if (mounted) {
setState(() {
_isOn = isOn;
});
}
}
}
//
// Getter for the index
//
int get index => widget.index ?? -1;
}
Now, it is up to the button to retrieve the controller, via a call to the Provider (line 21).
Advantages of this solution:
- the TestPage is now a StatelessWidget
- the buttons may be anywhere in the Widget tree, rooted by the TestPage, and this will work.
It is much better, however, we are exposing the ButtonTwoStatesState of the buttons to the outside world... isn't there any other solution?
Reactive
Another approach would be to let the buttons react upon any changes. This could be achieved in different ways. Let's have a look at some of them...
BLoC
Yes, I know... again the notion of BLoC and... why not? (this time I will use the Provider, to change a bit).
If you remember my articles on the topic (see here and here), we are using Streams.
The code of such BLoC could look like the following:
class ButtonTwoStatesControllerBloc {
//
// Stream to handle the index of the selected button
//
final BehaviorSubject<int> _selectedButtonStreamController = BehaviorSubject<int>();
Stream<int> get outSelectedButtonIndex => _selectedButtonStreamController.stream;
Function(int) get inSelectedButtonIndex => _selectedButtonStreamController.sink.add;
//
// Total number of buttons
//
final int buttonsCount;
//
// Callback to be invoked when the selection changes
//
final ValueChanged<int>? onChange;
//
// Constructor
//
ButtonTwoStatesControllerBloc({
required this.buttonsCount,
this.onChange,
int? selectedIndex,
}) : assert(buttonsCount > 0),
assert((selectedIndex == null || (selectedIndex >= 0 && selectedIndex < buttonsCount))) {
//
// Propagate the current selected index (if any)
//
if (selectedIndex != null) {
inSelectedButtonIndex(selectedIndex);
}
//
// Listen to changes to emit to invoke the callback
//
outSelectedButtonIndex.listen((int index) => onChange?.call(index));
}
void dispose() {
_selectedButtonStreamController.close();
}
}
As you can see, at initialization time:
- if we provide a valid selectedIndex, we send it to the stream
- we start listening to the stream, which will be used by the buttons (see later) and we invoke the "onChange" callback (line #39)
class TestPage extends StatelessWidget {
const TestPage({super.key});
@override
Widget build(BuildContext context) {
return Provider<ButtonTwoStatesControllerBloc>(
create: (BuildContext context) => ButtonTwoStatesControllerBloc(
buttonsCount: 3,
selectedIndex: 0,
onChange: (int selectedIndex) {
// Do whatever needs to be done
debugPrint('selectedIndex: $selectedIndex');
},
),
dispose: (BuildContext context, ButtonTwoStatesControllerBloc bloc) => bloc.dispose(),
child: Scaffold(
appBar: AppBar(
title: const Text('2-state buttons'),
centerTitle: true,
),
body: Center(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: const [
ButtonTwoStates(
index: 0,
),
SizedBox(width: 8),
ButtonTwoStates(
index: 1,
),
SizedBox(width: 8),
ButtonTwoStates(
index: 2,
),
],
),
),
),
);
}
}
As regards the TestPage, we simply initialize and inject the ButtonTwoStatesControllerBloc.
class ButtonTwoStates extends StatelessWidget {
const ButtonTwoStates({
super.key,
required this.index,
});
final int index;
@override
Widget build(BuildContext context) {
final ButtonTwoStatesControllerBloc controllerBloc =
Provider.of<ButtonTwoStatesControllerBloc>(context, listen: false);
return StreamBuilder<int>(
stream: controllerBloc.outSelectedButtonIndex,
initialData: -1,
builder: (BuildContext context, AsyncSnapshot<int> snapshot) {
final bool isOn = snapshot.data == index;
return InkWell(
onTap: () => controllerBloc.inSelectedButtonIndex(index),
child: Container(
width: 80.0,
height: 56.0,
color: isOn ? Colors.red : Colors.green,
),
);
},
);
}
}
And finally, for the buttons:
- lines #12-13: we retrieve the ButtonTwoStatesControllerBloc
- we use a StreamBuilder which:
- listens to the stream (line 16)
- rebuilds when a new index is emitted
- injects the index of the button when the latter is tapped (line 22)
This solution also works but all buttons are systematically rebuilt... which has impacts on the performance.
To limit the number of rebuilds, we could adapt the BLoC to only emit events related to changes (on => off, off => on) but we would then need to link the new state to the button index.
Of course, this has a price...
- we complexify the BLoC
- a new stream controller (if we really want to stick to the theory: BLoC => streams only)
- we need to maintain the state of the currently selected button
...but it is not difficult to do. This is a trade-off to do.
ValueNotifier
We could also use a ValueNotifier together with a ValueListenableBuilder.
ValueNotifier
A ValueNotifier listens to variations of an internal value. When a variation happens, it notifies everything that is listening to it. It implements a ValueListenable.
ValueListenableBuilder
A ValueListenableBuilder is a Widget which listens to notifications emitted by a ValueListenable, and rebuilds providing the emitted value to its builder method.
The solution, based on these 2 notions, consists of buttons, registering themselves to a controller, which would tell each button which ValueNotifier to listen to.
Such a solution could be written as follows:
class ButtonTwoStatesControllerListenable {
ButtonTwoStatesControllerListenable({
required this.buttonsCount,
this.onChange,
int? selectedIndex,
}) : assert(buttonsCount > 0),
assert((selectedIndex == null ||
(selectedIndex >= 0 &&
selectedIndex < buttonsCount))) {
//
// Prepare the array
//
_registeredButtons = List.generate(buttonsCount, (index) => null);
// Save the initial selected index (if any)
_selectedIndex = selectedIndex ?? -1;
}
//
// List of the buttons' ValueNotifier
//
late List<ValueNotifier<bool>?> _registeredButtons;
//
// Current selected index
//
int _selectedIndex = -1;
//
// Total number of buttons
//
final int buttonsCount;
//
// Callback to be invoked when the selection changes
//
final ValueChanged<int>? onChange;
//
// Registers a button via its index
//
ValueNotifier<bool> registerButton(int buttonIndex) {
assert(buttonIndex >= 0 && buttonIndex < buttonsCount);
assert(_registeredButtons[buttonIndex] == null);
ValueNotifier<bool> valueNotifier = _registeredButtons[buttonIndex] =
ValueNotifier<bool>(buttonIndex == _selectedIndex);
return valueNotifier;
}
//
// When a button is selected, we record the information
// and unselect the others
//
void setButtonSelectedIndex(int buttonIndex) {
assert(buttonIndex >= 0 && buttonIndex < buttonsCount);
assert(_registeredButtons[buttonIndex] != null);
if (buttonIndex != _selectedIndex) {
if (_selectedIndex != -1) {
_registeredButtons[_selectedIndex]?.value = false;
}
_selectedIndex = buttonIndex;
_registeredButtons[_selectedIndex]?.value = true;
// Invoke the callback
onChange?.call(_selectedIndex);
}
}
}
This code is very similar to our version of the first controller, earlier in the article.
The interesting parts are:
- lines #47-48: we initialize a ValueNotifier<bool> to handle the state of the button of a certain index.
- line #63: if there was a previously selected button, we mark it as not selected (= false)
- line #66: we mark the currently selected index
The simple fact of changing the inner value of a ValueNotifier (lines #63 & 68) will result in having the ValueListenableBuilder of the corresponding buttons to rebuild (see later)
As regards the TestPage, we only inject the new controller, as follows:
class TestPage extends StatelessWidget {
const TestPage({super.key});
@override
Widget build(BuildContext context) {
return Provider<ButtonTwoStatesControllerListenable>(
create: (BuildContext context) => ButtonTwoStatesControllerListenable(
buttonsCount: 3,
selectedIndex: 0,
onChange: (int selectedIndex) {
// Do whatever needs to be done
debugPrint('selectedIndex: $selectedIndex');
},
),
child: Scaffold(
appBar: AppBar(
title: const Text('2-state buttons'),
centerTitle: true,
),
body: Center(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: const [
ButtonTwoStates(
index: 0,
),
SizedBox(width: 8),
ButtonTwoStates(
index: 1,
),
SizedBox(width: 8),
ButtonTwoStates(
index: 2,
),
],
),
),
),
);
}
}
The code related to the button has to be changed as follows:
class ButtonTwoStates extends StatefulWidget {
const ButtonTwoStates({
super.key,
required this.index,
});
final int index;
@override
_ButtonTwoStatesState createState() => _ButtonTwoStatesState();
}
class _ButtonTwoStatesState extends State<ButtonTwoStates> {
late ValueNotifier<bool> valueNotifier;
late ButtonTwoStatesControllerListenable controllerListenable;
@override
void initState() {
super.initState();
controllerListenable = Provider.of<ButtonTwoStatesControllerListenable>(
context,
listen: false);
valueNotifier = controllerListenable.registerButton(widget.index);
}
@override
Widget build(BuildContext context) {
return ValueListenableBuilder<bool>(
valueListenable: valueNotifier,
builder: (BuildContext context, bool isOn, Widget? child) {
return InkWell(
onTap: () =>
controllerListenable.setButtonSelectedIndex(widget.index),
child: Container(
width: 80.0,
height: 56.0,
color: isOn ? Colors.red : Colors.green,
),
);
},
);
}
}
Explanation:
- lines 21-23: we retrieve the controller
- line #25: we register this button which results in getting a ValueNotifier, generated by the controller
- line #30: we use a ValueListenableBuilder which,
- line #31: listens to variations of the value of the ValueNotifier
- line #35: when the user taps the button, we notify the controller
That's it. This solution also works.
Conclusion
When we start developing in Flutter, the notion of StatefulWidget is not that easy to master, and when comes the moment to apply rules between multiple instances, it is very common to wonder how to make things done.
This article tries to give some hints through the basic example of 2-state buttons.
Dozens of other solutions exist and there is NO one single and best approach. This highly depends on your use case.
I hope that this article at least gives you some insight on this topic.
Stay tuned for new articles and as usual, I wish you happy coding!