Reusing widgets with Bloc

Issue

What do I want to achieve?

I’m using the Flutter BLoc library for my authentication component. Inside login page I have some text fields like username/password that I’m going to share with other pages as well, such as register page, forgot password, change password etc. Basically, following the DRY principle.

Each page (Register, Login, Forgot Password etc.) has it’s own BLoc component.

My problem
I couldn’t find a way to decouple the widget from the BLoc. I want to be able pass in the stateful widget any BLoc component depending on the page it is used in.

To make sense out of the written above let’s take a look at my code.

login.dart A of piece of code from the build method from the Login page.

Widget _loginForm(){
    return BlocListener<LoginBloc, LoginState>(
      listener: (context, state) {
        final status = state.formStatus;
        if (status is SubmissionFailed) {
         ...
        }
      },
      child: Form(
        key: _formKey,
        child: Column(
          children: [
            // The reusable widget: Email input field.
            BlocBuilder<LoginBloc, LoginState>(
              builder:(context, state){
                return emailInputField(context, state);
              }
            ),
            ...

Now let’s take a look in the emailInputField widget

Widget emailInputField(BuildContext context, dynamic state) {
  return TextFormField(
    validator: (value) =>
        state.isValidEmail ? null : 'Please input a valid email',
    onChanged: (value) =>
        // HERE - I want to decouple this widget from "LoginBloc".
        context.read<LoginBloc>().add(LoginUsernameChanged(username: value)),
      labelText: 'Email',
    ...
  );
}

login_bloc.dart The Login BLoc

class LoginBloc extends Bloc<BaseEvent, LoginState>{
  LoginBloc() : super(LoginState());

  @override
  Stream<LoginState> mapEventToState(BaseEvent event) async*{
    yield* event.handleEvent(state);
  }

}

And the login events class, for having a full picture login_event.dart

abstract class LoginEvent extends BaseEvent {
  AuthenticationService authService = GetIt.I.get<AuthenticationService>();
}

// Event 1
class LoginUsernameChanged extends LoginEvent {
  final String username;

  LoginUsernameChanged({this.username});

  @override
  Stream<LoginState> handleEvent(BaseState state) async* {
    // Dart style down-casting...
    LoginState loginState = state;
    yield loginState.copyWith(username: username);
  }
}

base_event.dart

abstract class BaseEvent {
  Stream<BaseState> handleEvent(BaseState state);
}

And again, is there a way to decouple that view from the BLoc?
I hope my question makes sense after reading this.

P.S

To make things even simpler, one idea I though of is to keep one single Bloc component that will handle a group of related input fields (password/username) and then just pass in different states as I did in login.dart page when calling the emailInput widget.

Solution

What I would do is decouple the widget from bloc entirely. Instead of taking the bloc state and using bloc.add, create dependencies that you can populate with any parameter.

In you example you would have:

Widget emailInputField({
 required BuildContext context, 
 required bool isValidEmail, 
 required void Function(String?) onChange,
}) {
  return TextFormField(
    validator: (value) => isValidEmail ? null : 'Please input a valid email',
    onChanged: onChange,
    labelText: 'Email',
    ...
  );
}

Then you can use emailInputField with any bloc you want. Or any state management library for that matter.

For your example this would give:

Widget _loginForm(){
    return BlocListener<LoginBloc, LoginState>(
      listener: (context, state) {
        final status = state.formStatus;
        if (status is SubmissionFailed) {
         ...
        }
      },
      child: Form(
        key: _formKey,
        child: Column(
          children: [
            // The reusable widget: Email input field.
            BlocBuilder<LoginBloc, LoginState>(
              builder:(context, state){
                return emailInputField(
                  context: context,
                  isValidEmail: state.isValidEmail, 
                  onChanged: (value) => context.read<LoginBloc>().add(LoginUsernameChanged(username: value))
                );
              }
            ),
            ...

Answered By – Lulupointu

Answer Checked By – Katrina (FlutterFixes Volunteer)

Leave a Reply

Your email address will not be published.