Search code examples
flutteruisegmentedcontrol

I'm using reactive_sliding_segmented package. How can I go to next segment on clicking a button on Parent Widget in Flutter?


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.


Solution

  • 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;
                        }
                      });
                    });
                  },
                ),