How to do backend validation using BLOC pattern in Flutter TextField?

4,073

You are probably not longer looking for a solution but based on the upvotes of the question I wanted to provide an answer nonetheless.

I'm not sure if I understand your code correctly and it looks like you are implementing BLoC yourself, so this is quite a disclaimer because I am providing a solution which uses the BLoC implementation by Felix Angelov (pub.dev/packages/bloc).

The outcome of the code described below

Implementation Result

Code and Approach:

First I created an empty project, and added the BLoC Library; In pubspec.yaml i added

flutter_bloc: ^3.2.0

Then i created a new bloc BackendValidationBloc with one event ValidateInput and multiple states as shown in the following code snippets.

Event Code:

Most of the time I start by defining the event which is quite simple in my example:

part of 'backend_validation_bloc.dart';

@immutable
abstract class BackendValidationEvent {}

class ValidateInput extends BackendValidationEvent {
  final String input;

  ValidateInput({@required this.input});
}

State Code:

Then you probably want one state with multiple properties or multiple states. I decided to go with one state with multiple properties because in my opinion it is easier to handle in the UI. In this example I recommend giving feedback to the user because validating the input via a backend might take some time. Therefore the BackendValidationState features two states: loading and validated.

part of 'backend_validation_bloc.dart';

@immutable
class BackendValidationState {
  final bool isInProcess;
  final bool isValidated;
  bool get isError => errorMessage.isNotEmpty;
  final String errorMessage;

  BackendValidationState(
      {this.isInProcess, this.isValidated, this.errorMessage});

  factory BackendValidationState.empty() {
    return BackendValidationState(
        isInProcess: false, isValidated: false);
  }

  BackendValidationState copyWith(
      {bool isInProcess, bool isValidated, String errorMessage}) {
    return BackendValidationState(
      isValidated: isValidated ?? this.isValidated,
      isInProcess: isInProcess ?? this.isInProcess,
      // This is intentionally not defined as
      // errorMessage: errorMessage ?? this.errorMessage
      // because if the errorMessage is null, it means the input was valid
      errorMessage: errorMessage,
    );
  }

  BackendValidationState loading() {
    return this.copyWith(isInProcess: true);
  }

  BackendValidationState validated({@required String errorMessage}) {
    return this.copyWith(errorMessage: errorMessage, isInProcess: false);
  }
}

Bloc Code:

At last, you "connect" event with states by defining the bloc which makes calls to your backend:

import 'dart:async';

import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:meta/meta.dart';

part 'backend_validation_event.dart';
part 'backend_validation_state.dart';

class BackendValidationBloc
    extends Bloc<BackendValidationEvent, BackendValidationState> {
  @override
  BackendValidationState get initialState => BackendValidationState.empty();

  @override
  Stream<BackendValidationState> mapEventToState(
    BackendValidationEvent event,
  ) async* {
    if (event is ValidateInput) {
      yield this.state.loading();
      String backendValidationMessage =
          await this.simulatedBackendFunctionality(event.input);
      yield this.state.validated(errorMessage: backendValidationMessage);
    }
  }

  Future<String> simulatedBackendFunctionality(String input) async {
    // This simulates delay of the backend call
    await Future.delayed(Duration(milliseconds: 500));
    // This simulates the return of the backend call
    String backendValidationMessage;
    if (input != 'hello') {
      backendValidationMessage = "Input does not equal to 'hello'";
    }
    return backendValidationMessage;
  }
}

UI Code:

In case you are not familiar with how the implemented BLoC is used in the UI, this is the frontend code using the state to feed different values (for the actual error message and for user feedback while waiting for the backend response) to the errorText property of the TextField.

import 'package:backend_validation_using_bloc/bloc/backend_validation_bloc.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';

void main() => runApp(App());

class App extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: BlocProvider<BackendValidationBloc>(
          create: (context) => BackendValidationBloc(), child: HomeScreen()),
    );
  }
}

class HomeScreen extends StatefulWidget {
  HomeScreen({Key key}) : super(key: key);

  @override
  _HomeScreenState createState() => _HomeScreenState();
}

class _HomeScreenState extends State<HomeScreen> {
  TextEditingController textEditingController = TextEditingController();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(),
        body: BlocBuilder<BackendValidationBloc, BackendValidationState>(
          builder: (BuildContext context, BackendValidationState state) {
            return TextField(
              controller: textEditingController,
              onChanged: (String currentValue) {
                BlocProvider.of<BackendValidationBloc>(context)
                    .add(ValidateInput(input: currentValue));
              },
              decoration: InputDecoration(errorText: state.isInProcess ? 'Valiating input...' : state.errorMessage),
            );
          },
        ));
  }
}

Connecting a real backend

So I kinda faked a backend, but if you want to use a real one it is common to implement a Repository and pass it to the BLoC in a constructor, which makes using different implementations of backend easier (if properly implemented against interfaces). If you want a more detailed tutorial check out Felix Angelov's tutorials (they are pretty good)

Hope this helps you or others.

Share:
4,073
firmansyah ramadhan
Author by

firmansyah ramadhan

Updated on December 08, 2022

Comments

  • firmansyah ramadhan
    firmansyah ramadhan over 1 year

    I want to create a TextField that check if the value exist in database.

    How to do async validation using BLOC pattern with TextField widget? Should I use StreamTransformer to add error to the Stream? I tried using DebounceStreamTransformer but it's just block the Stream from receiving a new value.

    This is my Observable

     Observable<String> get valueStream => valueController.stream.transform(PropertyNameExist.handle('Blabla', null));
    

    This is my StreamTransformer

    class PropertyNameExist implements StreamTransformerValidator {
      static StreamTransformer<String, String> handle(String fieldname, String data) {
        Http http = new Http();
        return StreamTransformer<String, String>.fromHandlers(
            handleData: (String stringData, sink) {
              http.post('/my_api',data:{
                'property_name':stringData,
              }).then((Response response){
                Map<String,dynamic> responseData = jsonDecode(response.data);
                bool isValid = responseData['valid'] == 'true';
                if(isValid){
                  sink.add(stringData);
                } else {
                  sink.addError('Opps Error');
                }
              });
        });
      }
    }
    

    This is my Widget

    StreamBuilder<String>(
            stream: valueStream,
            builder: (context, AsyncSnapshot<String> snapshot) {
              if (snapshot.hasData) {
                _textInputController.setTextAndPosition(snapshot.data);
              }
              return TextField(
                controller: _textInputController,
                onChanged: (String newVal) {
                  updateValue(newVal);
                },
                decoration: InputDecoration(
                  errorText: snapshot.error,
                ),
              );
            },
          )