DEV Community

Cover image for Star Rating with Flutter Reactive Forms
Joan Pablo
Joan Pablo

Posted on • Edited on

Star Rating with Flutter Reactive Forms

Reactive Forms is not limited just to common widgets in Forms like text inputs, dropdowns, sliders, and switches; you can easily create custom widgets that two-way binds to FormControls and create your own set of Reactive Widgets.

In this post, we will learn how to implement a Reactive Star Rating widget using Reactive Forms, and thus be able to create rich forms that can also collect information using a star rating bar.

To know more about Reactive Forms you can read my previous post Why use Reactive Forms in Flutter? or visit github and pub repo. You can also read the wiki page Custom Reactive Widgets.

This is what we are going to achieve at the end of this post:

Reactive Star Rating

We are not going to create a Star Rating Bar from zero, instead we are going to use an existing widget and convert it into a Reactive Widget.

We will use the excellent plugin flutter_rating_bar and give it the ability to two-way binds with a FormControl.

Less talking and more coding ;)

# import plugin in pubspec.yaml
dependencies:
  flutter_rating_bar: ^3.0.1+1
Enter fullscreen mode Exit fullscreen mode

flutter_rating_bar: ^3.0.1+1 was the last version at the time of the post.

Create our reactive widget, declare ReactiveStarRating and extends from ReactiveFormField:

/// A reactive star rating widget that two-way binds 
/// to a FormControl<double>.
class ReactiveStarRating extends ReactiveFormField<double> {
  //...

  @override
  ReactiveFormFieldState<double> createState() =>
      ReactiveFormFieldState<double>();
}
Enter fullscreen mode Exit fullscreen mode

Then supply to the parent class the formControlName and the builder function:

/// A reactive star rating widget that two-way binds 
/// to a FormControl<double>.
class ReactiveStarRating extends ReactiveFormField<double> {
  /// Constructs an instance of [ReactiveStarRating].
  ///
  /// The argument [formControlName] must not be null.
  ReactiveStarRating({
    @required String formControlName,
  }) : super(
          formControlName: formControlName,
          builder: (ReactiveFormFieldState<double> field) {
            // RatingBar inner widget
            return RatingBar(
              // set UI value when control changes
              initialRating: field.value,
              // set control value when UI changes
              onRatingUpdate: field.didChange,
              itemBuilder: (context, _) => Icon(Icons.star),
            );
          },
        );

  @override
  ReactiveFormFieldState<double> createState() =>
      ReactiveFormFieldState<double>();
}
Enter fullscreen mode Exit fullscreen mode

Done, say hello to our new reactive widget ReactiveStarRating.

Wow but wait a minute, what just happened here, is that all? Yes, we are done with the reactive widget, but before we start using it, let me explain the above code a little bit.

These are the steps:

1- Extend from ReactiveFormField. This is a StatefulWidget that maintains the state and is bound with the FormControl.
2- Provide a builder function in the parent's constructor (super). This function will return your inner widget, in this case, the RatingBar widget. The builder function will be called every time the FormControl changes its value.
3- Update the value of the FormControl with the method didChange method. Every time you want to update the value of the FormControl, call the didChange method of the ReactiveFormField passing the new value.

How use it?

We are almost done, now let's consume our new ReactiveStarRating, we will also add some other reactive widgets that listen for changes in the FormControl just to visualize how they stay in sync with our new ReactiveStarRating:

class StarRatingSample extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: ReactiveFormBuilder(
        // define a form with a FormControl<double> field.
        form: () => fb.group({'rating': 0.0}),
        builder: (context, form, child) {
          return Center(
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.center,
              mainAxisSize: MainAxisSize.min,
              children: [
                // listen for changes in `rating` and show value
                ReactiveValueListenableBuilder<double>(
                  formControlName: 'rating',
                  builder: (context, control, child) {
                    return Text(
                      control.value.toStringAsFixed(1),
                      style: TextStyle(fontSize: 40.0),
                    );
                  },
                ),
                SizedBox(height: 20.0),
                // Bind our custom widget with `rating` control
                // just by provinding the control name.
                ReactiveStarRating(
                  formControlName: 'rating',
                ),
                SizedBox(height: 20.0),
                // Bind also a slider
                ReactiveSlider(
                  formControlName: 'rating',
                  divisions: 5,
                  min: 0,
                  max: 5,
                ),
              ],
            ),
          );
        },
      ),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

And that's it. This was a very simple example. I urge you to make the widget more configurable so that it allows you to change the background color of the stars and evaluate with a half star.

I hope you have enjoyed the post.

Thanks.

Top comments (5)

Collapse
 
inmer profile image
Inmer

Hi Joan, I build a custom reactive field using date_time_picker package, and it works, but now I have marked it as required but the error is not showing up, what I am missing. Thanks in advance.

Here is my implementation:

class ReactiveDateTimePicker extends ReactiveFormField<String> {
final String labelText;
ReactiveDateTimePicker({
Key key,
String this.labelText,
@required String formControlName,
}) : super(
        key: key,
        formControlName: formControlName,
        validationMessages: (control) => {'required': 'this is required'},
        builder: (ReactiveFormFieldState<String> field) {
          return RDateTimePicker(
            onChanged: field.didChange,
            labelText: labelText,
          );
        });

 @override
ReactiveFormFieldState<String> createState() =>
  ReactiveFormFieldState<String>();
}
Enter fullscreen mode Exit fullscreen mode
Collapse
 
joanpablo profile image
Joan Pablo

Hi Inmer,

Well The thing is that you can see the errors in widgets like ReactiveTextField because they have a predefined place to show the errors. For example ReactiveTextFields uses an InputDecoration and this decoration show errors at the bottom of the widget.

In the case of your control you haven't define a widget for showing errors. For example if you wrap your RDateTimePicker within an InputDecorator and set the error in decoration property, you will show the error.

Collapse
 
inmer profile image
Inmer

Hi Joan, thanks for reply.

I should clarify my question my bad sorry, RDateTimePicker is just a visual customization of date_time_picker from m3uzz.com on pub.dev, here is my implementation:

class RDateTimePicker extends StatelessWidget {
  final TextEditingController controller;
  final String labelText;
  final Function(String) validator;
  final Function(String) onChanged;

  const RDateTimePicker({
    Key key,
    this.controller,
    this.labelText,
    this.validator,
    this.onChanged,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Container(
      padding: EdgeInsets.all(5),
      child: DateTimePicker(
          onChanged: onChanged,
          controller: controller,
          style:
              TextStyle(color: Colors.black, decoration: TextDecoration.none),
          decoration: InputDecoration(
            labelText: labelText,
            enabledBorder: UnderlineInputBorder(
                borderSide: BorderSide(color: Colors.black)),
            focusedBorder: UnderlineInputBorder(
                borderSide: BorderSide(color: Colors.black)),
            labelStyle: TextStyle(color: Colors.black)
          ),
          validator: validator),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

And with "normal" form, I am able to show errors using validator property, and a controller. Can't I use its implementation to show the error or anyway I have to create an InputDecorator. By the way I would like an example for those InputDecorator, because I have created a completely custom component in which I would need what you say. And thanks for such a great library.

Thread Thread
 
joanpablo profile image
Joan Pablo • Edited

There you have an InputDecoration. You should avoid using validator, and instead use the errorText of the decoration. You can search for the implementation of the ReactiveTextField to see how it is using this approach.

builder: (ReactiveFormFieldState field) {
   final state = field as _ReactiveTextFieldState;
   final InputDecoration effectiveDecoration = (decoration ?? const InputDecoration())
   .applyDefaults(Theme.of(state.context).inputDecorationTheme);

   return DateTimePicker(
      decoration: effectiveDecoration.copyWith(errorText: state.errorText),
   );         

Enter fullscreen mode Exit fullscreen mode

In the above code, I'm not using your RDateTimePicker just to simplify things, but it also applies with your wrapper widget.

Thread Thread
 
inmer profile image
Inmer

Thank you, Joan finally I was able to make it work for both cases, greatly appreciate your help.