I want to change the value of segment control when button is clicked in parent widget. If button and segment control are in same widget I can easily change the value of segment control but if they are in different widgets how can I change?
This is my main.dart
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: SafeArea(
child: Column(
children: [
const TabView(),
ElevatedButton(onPressed: () {//go to next segment on click},
child: const Text('Next'))
],
)),
),
);
}
}
This is my tab_view.dart
class TabView extends StatefulWidget {
const TabView({Key? key}) : super(key: key);
@override
State<TabView> createState() => _TabViewState();
}
class _TabViewState extends State<TabView> {
FormGroup formGroup = FormGroup({
'name': FormControl<String>(validators: [Validators.required]),
'note': FormControl<String>()
});
final FormControl<String> segmentControl =
FormControl<String>(validators: [Validators.required]);
@override
Widget build(BuildContext context) {
return ReactiveForm(
formGroup: formGroup,
child: Column(
children: [
ReactiveSlidingSegmentedControl<String, String>(
formControl: segmentControl,
children: const {
'name': Text('Name'),
'note': Text('Note'),
},
),
const SizedBox(height: 16),
LayoutBuilder(builder: (context, constraints) {
return ReactiveValueListenableBuilder(
formControl: segmentControl,
builder: (context, field, child) {
return _buildView(field as FormControl<String>, formGroup);
});
}),
],
),
);
}
Widget _buildView(FormControl<String> control, FormGroup formGroup) {
if ((control.value != 'name') && formGroup.control('name').invalid) {
formGroup.control('name').markAsTouched();
control.value = 'name';
}
switch (control.value) {
case 'name':
return ReactiveTextField(
formControlName: 'name',
decoration: const InputDecoration(labelText: 'Name'),
);
case 'note':
return ReactiveTextField(
formControlName: 'note',
decoration: const InputDecoration(labelText: 'Note'),
);
default:
return Container();
}
}
}
Is there any method so that the child Widget knows button has been clicked in Parent widget and change the value of segment control.
create two final variables next
and prev
, and ask inside the constructor, set to false
initially in TabView
. Override didUpdateWidget
and put the logic of changing the segmentControl
to reflect next
/prev
value by finding the current value and next value from map given inside the children
param.
Make MyApp Stateful widget declare two state variables _next
and _prev
alter their value on prev/next button pass these value as param to TabView(prev:_prev,next:_next)
and you are good to go.
A detailed example:
class MyApp extends StatefulWidget {
const MyApp({Key? key}) : super(key: key);
@override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
///HERE
bool _next = false;
bool _prev = false;
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: SafeArea(
child: Column(
children: [
TabView(
///HERE
next: _next,
prev: _prev,
),
const SizedBox(
height: 20,
),
Row(mainAxisAlignment: MainAxisAlignment.center, children: [
ElevatedButton(
onPressed: () {
///HERE
setState(() {
_next = false;
_prev = true;
});
},
child: const Text('Prev')),
const SizedBox(
width: 20,
),
ElevatedButton(
onPressed: () {
///HERE
setState(() {
_next = true;
_prev = false;
});
},
child: const Text('Next'))
])
],
)),
),
);
}
}
class TabView extends StatefulWidget {
const TabView({Key? key, this.prev = false, this.next = false})
: super(key: key);
final bool prev;
final bool next;
@override
State<TabView> createState() => _TabViewState();
}
class _TabViewState extends State<TabView> {
FormGroup formGroup = FormGroup({
'name': FormControl<String>(validators: [Validators.required]),
'note': FormControl<String>()
});
final List<Widget> control = [const Text('Name'), const Text('Note')];
final FormControl<int> segmentControl = FormControl<int>(value: 0);
@override
void didUpdateWidget(TabView oldWidget) {
if (widget.next) {
final nextIndex =
min((segmentControl.value ?? -1) + 1, control.length - 1);
segmentControl.value = nextIndex;
}
if (widget.prev) {
final prevIndex = max((segmentControl.value ?? -1) - 1, 0);
segmentControl.value = prevIndex;
}
super.didUpdateWidget(oldWidget);
}
@override
Widget build(BuildContext context) {
return ReactiveForm(
formGroup: formGroup,
child: Column(
children: [
ReactiveSlidingSegmentedControl<int, int>(
formControl: segmentControl,
children: control.asMap(),
),
const SizedBox(height: 16),
ReactiveValueListenableBuilder(
formControl: segmentControl,
builder: (context, control, child) {
return _buildView(segmentControl.value!, formGroup);
})
],
),
);
}
Widget _buildView(int index, FormGroup formGroup) {
switch (index) {
case 0:
return ReactiveTextField(
formControlName: 'name',
decoration: const InputDecoration(labelText: 'Name'),
);
case 1:
return ReactiveTextField(
formControlName: 'note',
decoration: const InputDecoration(labelText: 'Note'));
default:
return Container();
}
}
}
A more flexible approach would be to add callback function onTraversalFallback
instead of hardcoding.
One usecase may be: in stepper form you want next to trigger tabview next tab until last tab is reached. if the last tab
is reached move to stepCount+1
.
For that a little tweak is needed. Hope one finds useful
class TabView extends StatefulWidget {
/*...*/
final VoidCallback? onTraversalFallback;
/*...*/
}
class _TabViewState extends State<TabView> {
/*...*/
@override
void didUpdateWidget(SaveElearningCourseDetailsForm oldWidget) {
if (widget.next) {
final nextIndex =
min((segmentControl.value ?? -1) + 1, control.length - 1);
if (nextIndex == segmentControl.value) {
widget.onTraversalFallback?.call();
} else {
segmentControl.value = nextIndex;
}
}
if (widget.prev) {
final prevIndex = max((segmentControl.value ?? -1) - 1, 0);
if (prevIndex == segmentControl.value) {
widget.onTraversalFallback?.call();
} else {
segmentControl.value = prevIndex;
}
}
super.didUpdateWidget(oldWidget);
}
/*...*/
}
on caller side you can do something like:
TabView(
///HERE
next: _next,
prev: _prev,
onTraversalFallback: () {
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
setState(() {
if (_prev) {
_currentStep -= 1;
_prev = false;
}
if (_next) {
_currentStep + 1;
_next = false;
}
});
});
},
),