Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to add callback using BLoC pattern in flutter?

Tags:

flutter

I am calling login api on button click, I am able to get response from server but on clicking on button it doesn't show progress bar. I am using BLoC pattern for this. Here is the code,

import 'package:flutter/material.dart';
import '../blocs/bloc.dart';
import '../blocs/provider.dart';
import '../models/login_response.dart';

class LoginScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Provider(
  child: new Scaffold(
      body: Container(
        child: LoginForm(),
    ),
  ),
);
}
}

class LoginForm extends StatefulWidget {
// since its a stateful widget we need to create state for it.
const LoginForm({Key key}) : super(key: key);

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

class _LoginFormState extends State<LoginForm> {

@override
 Widget build(BuildContext context) {


return Form(
  child: Column(
    children: <Widget>[ 
      Padding(
        padding: const EdgeInsets.only(top: 50),
      ),
      // Start creating widget here.
      emailField(),
      passwordField(),
      Container(margin: EdgeInsets.only(top: 25.0)),
      submitButton()
    ],
  ),
 );
}

  Widget emailField() {
   return StreamBuilder(
   stream: bloc.email,
   builder: (context, snapshot) {
     return TextField(
        onChanged:  bloc.changeEmail,
        keyboardType: TextInputType.emailAddress,
        decoration: InputDecoration(
        hintText: '[email protected]',
        labelText: 'Email Address',
        errorText: snapshot.error
      ),
    );
   }
  );
}

Widget passwordField() {
  return StreamBuilder(
   stream: bloc.password,
    builder: (context, snapshot) {
      return TextField(
        onChanged: bloc.changePassword,
        obscureText: true,
        decoration: InputDecoration(
        labelText: 'Please enter your password',
        hintText: 'Password',
        errorText: snapshot.error
      ),
    );
   },
 );
}

Widget submitButton() {

return StreamBuilder(
  stream: bloc.submitValid,
  builder: (context, snapshot) {
      return RaisedButton(
        onPressed:() =>  showWidgetForNetworkCall(context),
        // onPressed: () {
        //   // Do submit button action.              
        //   showWidgetForNetworkCall(context);
        // //  callLoginApi();
        // },
        child: const Text('Login'),
        textColor: Colors.white,
        color: Colors.blueAccent,
      );
    },
  );
}

  // Loading Widget
   Widget _buildLoadingWidget() {
   return Center(
     child: Column(
       mainAxisAlignment: MainAxisAlignment.center,
       children: <Widget>[
         Text("Loading data from API...", textDirection: TextDirection.ltr), CircularProgressIndicator()
       ],
     ),
   );
 }

 // // Error Widget
  Widget _buildErrorWidget(String error) {
   return Center(
    child: Column(
    mainAxisAlignment: MainAxisAlignment.center,
    children: <Widget>[
      Text("Loading error data from API...", textDirection: TextDirection.ltr), CircularProgressIndicator()
    ],
   ),
 );
}

// show server data
 showServerData() {
   print(" Servr >>>>>> Data : ");
 }

 Widget showWidgetForNetworkCall(BuildContext context) {
  bloc.loginSubmit();
    return StreamBuilder(
     stream: bloc.loginSubject.stream,
       builder: (context, AsyncSnapshot<LoginResponse>snapshot){
     if (snapshot.hasData) {
        return showServerData();
      } else if (snapshot.hasError) {
        return _buildErrorWidget(snapshot.error);
      } else {
        return _buildLoadingWidget();
      }
    },
  );
 }
}

This is my login_screen.dart. And my bloc class for api call is:

postData() async {
LoginResponse response = await _repository.postData(_loginResource);
_subject.sink.add(response);

}

I am able to parse json api, but not able to get the response of my model i.e, 'LoginResponse' in login_screen.dart class and also the CircularProgressBar doesn't show when api is called on button click.

Code of the BLoC class is :

import 'dart:async';
import 'package:rxdart/rxdart.dart';
import 'validators.dart';
import '../models/login_response.dart';
import '../repository/login_repository.dart';
import '../resources/login_resource.dart';

class Bloc extends Object with Validators {

final LoginRepository _repository = LoginRepository();
final BehaviorSubject<LoginResponse> _subject = 
BehaviorSubject<LoginResponse>();
LoginResource _loginResource = LoginResource();

final _email = BehaviorSubject<String>(); // Declaring variable as private
final _password = BehaviorSubject<String>(); // Declaring variable as private

// Add data to stream (Its like setter)
Stream<String> get email => _email.stream.transform(validateEmail);
Stream<String> get password => 
 _password.stream.transform(validatePassword);
 Stream<bool> get submitValid => Observable.combineLatest2(email, password, (e, p) => true);

 // Change data. For retrieveing email value.
 Function(String) get changeEmail => _email.sink.add;
 Function(String) get changePassword => _password.sink.add;

 loginSubmit() {

  _loginResource.email = "bar1";
  _loginResource.password = "bar2";

  postData();
}

 postData() async {
   LoginResponse response = await _repository.postData(_loginResource);
   _subject.sink.add(response);
 }

  dispose() {
   _email.close();
   _password.close();
   _subject.close();
  }

  BehaviorSubject<LoginResponse> get loginSubject => _subject;
}

 final bloc = Bloc();

Kindly let me know what I am missing. Thanks in advance :)

like image 772
Programming Learner Avatar asked Dec 18 '22 17:12

Programming Learner


1 Answers

Well here we go. I make some changes in your UI layer and in BLoC class with order to accomplish what you're asking for. I will firstly show the pieces of code that I insert and explain what I was think when I wrote it and after all I will paste the entire source code will all changes. Maybe you can use the concept that I had used to adapt the source code to your needs. All code has comments so please read it will help you a lot.

First of all I create an enum to represent the status of the login process and a class that holds the login process status and a message about it. Both are part of your UI layer.

/// NON_LOGIN: means that login is not happening
/// LOGGIN: means that login is happening
/// LOGIN_ERROR: means that something is wrong with login
/// LOGIN_SUCCESS: the login process was a success.
enum LoginStatus { NON_LOGIN, LOGGING, LOGIN_SUCCESS, LOGIN_ERROR }

class LoginState {
  final LoginStatus status;
  final String message;

  LoginState({this.status, this.message});
}

In _LoginFormState class inside build method I inserted a StreamBuilder that will show and hide the progressbar when the login is happening or show an error widget.

@override
  Widget build(BuildContext context) {
    return Form(
      child: Column(
        children: <Widget>[
          Padding(
            padding: const EdgeInsets.only(top: 50),
          ),
          // Start creating widget here.
          emailField(),
          passwordField(),
          Container(margin: EdgeInsets.only(top: 25.0)),
          submitButton(),
          StreamBuilder<LoginState>(
            stream: bloc.loginStateStream,
            builder: (context, AsyncSnapshot<LoginState> snapshot){

              if ( !snapshot.hasData )
                return Container();

              switch(snapshot.data.status){
                case LoginStatus.LOGGING:
                  return _buildLoadingWidget();

                case LoginStatus.LOGIN_ERROR:
                  return _buildErrorWidget(snapshot.data.message);

                case LoginStatus.LOGIN_SUCCESS:
                  // Here you can go to another screen after login success.
                  return Center(child: Text("${snapshot.data.message}"),);

                case LoginStatus.NON_LOGIN:
                default:
                  return Container();
              }
            },
          ),

        ],
      ),
    );
  }

And the last change in your UI layer was in submitButton method the only change was in onPress event of your button now it calls bloc.loginSubmit method.

return RaisedButton(
          onPressed:() => bloc.loginSubmit(), // the only change
          child: const Text('Login'),
          textColor: Colors.white,
          color: Colors.blueAccent,
        );

Now all the changes are in BLoC class. Basically I created a new subject for handling the state changes of login process using LoginStatus enum and LoginState class and tell to view what widget must be showed to user.

//The subject and a get method to expose his stream
final PublishSubject<LoginState> _loginStateSubject = new PublishSubject();
Observable<LoginState> get loginStateStream => _loginStateSubject.stream;

All the login state changes handling I wrote inside postData method.

postData() async {
    // this call will change the UI and a CircularProgressBar will be showed.
    changeLoginState(state: LoginState( status: LoginStatus.LOGGING, message: "logging") );

    // waiting for login response!
    LoginResponse response = await _repository.postData(_loginResource);
    print(response); // just to text debug your response.

    //Here you can verify if the login process was successfully or if there is
    // some kind of error based in your LoginResponse model class.
    // avoiding write this logic in UI layer.

    if(response.hasError){
      changeLoginState(state: LoginState(status: LoginStatus.LOGIN_ERROR,
          message: response.errorMessage)
      );
      // and after 1.5 seconds we make the error message disappear from UI.
      // you can do this in UI layer too
      Future.delayed(Duration(milliseconds: 1500), (){
        // you can pass null to state property, will make the same effect
        changeLoginState(state: LoginState(status: LoginStatus.NON_LOGIN)); });
    }

    else {
      changeLoginState(state: LoginState(status:
      LoginStatus.LOGIN_SUCCESS, message: "Login Success"));
    }
    //_subject.sink.add(response);
  }

With this approach you avoid send to your UI layer objects from you model layer like LoginResponse class objects and this kind of concept makes your code more clean and do not broken MVC pattern and your UI layer holds only layout code.

Make some tests, I didn't, adapt to your needs and comment if you need something I will answer when I can.

The entire source code:

/// NON_LOGIN: means that login is not happening
/// LOGGIN: means that login is happening
/// LOGIN_ERROR: means that something is wrong with login
/// LOGIN_SUCCESS: the login process was a success.
///
enum LoginStatus { NON_LOGIN, LOGGING, LOGIN_SUCCESS, LOGIN_ERROR }

class LoginState {
  final LoginStatus status;
  final String message;

  LoginState({this.status, this.message});
}

class LoginScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Provider(
      child: new Scaffold(
        body: Container(
          child: LoginForm(),
        ),
      ),
    );
  }
}

class LoginForm extends StatefulWidget {
// since its a stateful widget we need to create state for it.
  const LoginForm({Key key}) : super(key: key);

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

class _LoginFormState extends State<LoginForm> {

  @override
  Widget build(BuildContext context) {
    return Form(
      child: Column(
        children: <Widget>[
          Padding(
            padding: const EdgeInsets.only(top: 50),
          ),
          // Start creating widget here.
          emailField(),
          passwordField(),
          Container(margin: EdgeInsets.only(top: 25.0)),
          submitButton(),
          StreamBuilder<LoginState>(
            stream: bloc.loginStateStream,
            builder: (context, AsyncSnapshot<LoginState> snapshot){

              if ( !snapshot.hasData )
                return Container();

              switch(snapshot.data.status){
                case LoginStatus.LOGGING:
                  return _buildLoadingWidget();

                case LoginStatus.LOGIN_ERROR:
                  return _buildErrorWidget(snapshot.data.message);

                case LoginStatus.LOGIN_SUCCESS:
                  // Here you can go to another screen after login success.
                  return Center(child: Text("${snapshot.data.message}"),);

                case LoginStatus.NON_LOGIN:
                default:
                  return Container();
              }
            },
          ),

        ],
      ),
    );
  }

  Widget emailField() {
    return StreamBuilder(
        stream: bloc.email,
        builder: (context, snapshot) {
          return TextField(
            onChanged:  bloc.changeEmail,
            keyboardType: TextInputType.emailAddress,
            decoration: InputDecoration(
                hintText: '[email protected]',
                labelText: 'Email Address',
                errorText: snapshot.error
            ),
          );
        }
    );
  }

  Widget passwordField() {
    return StreamBuilder(
      stream: bloc.password,
      builder: (context, snapshot) {
        return TextField(
          onChanged: bloc.changePassword,
          obscureText: true,
          decoration: InputDecoration(
              labelText: 'Please enter your password',
              hintText: 'Password',
              errorText: snapshot.error
          ),
        );
      },
    );
  }

  Widget submitButton() {

    return StreamBuilder(
      stream: bloc.submitValid,
      builder: (context, snapshot) {
        return RaisedButton(
          onPressed:() => bloc.loginSubmit(),
          child: const Text('Login'),
          textColor: Colors.white,
          color: Colors.blueAccent,
        );
      },
    );
  }

  // Loading Widget
  Widget _buildLoadingWidget() {
    return Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: <Widget>[
          Text("Loading data from API...", textDirection: TextDirection.ltr), CircularProgressIndicator()
        ],
      ),
    );
  }

  // // Error Widget
  Widget _buildErrorWidget(String error) {
    return Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: <Widget>[
          Text("Loading error data from API...", textDirection: TextDirection.ltr), CircularProgressIndicator()
        ],
      ),
    );
  }

  /*
  // show server data
  showServerData() {
    print(" Servr >>>>>> Data : ");
  }


  Widget showWidgetForNetworkCall() {

    return StreamBuilder(
      stream: bloc.loginSubject.stream,
      builder: (context, AsyncSnapshot<LoginResponse>snapshot){
        if (snapshot.hasData) {
          return showServerData();
        } else if (snapshot.hasError) {
          return _buildErrorWidget(snapshot.error);
        } else {
          return _buildLoadingWidget();
        }
      },
    );
  }*/
}

class Bloc extends Object with Validators {


  //final BehaviorSubject<LoginResponse> _subject = BehaviorSubject<LoginResponse>();
  //BehaviorSubject<LoginResponse> get loginSubject => _subject;

  final LoginRepository _repository = LoginRepository();
  final PublishSubject<LoginState> _loginStateSubject = new PublishSubject();
  Observable<LoginState> get loginStateStream => _loginStateSubject.stream;
  LoginResource _loginResource = LoginResource();

  final _email = BehaviorSubject<String>(); // Declaring variable as private
  final _password = BehaviorSubject<String>(); // Declaring variable as private

  // Add data to stream (Its like setter)
  Stream<String> get email => _email.stream.transform(validateEmail);
  Stream<String> get password => _password.stream.transform(validatePassword);
  Stream<bool> get submitValid => Observable.combineLatest2(email, password, (e, p) => true);

  // Change data. For retrieveing email value.
  Function(String) get changeEmail => _email.sink.add;
  Function(String) get changePassword => _password.sink.add;

  void changeLoginState({LoginState state } ) => _loginStateSubject.sink.add(state);

  loginSubmit() {

    _loginResource.email = "bar1";
    _loginResource.password = "bar2";

    postData();
  }

  postData() async {

    // this call will change the UI and a CircularProgressBar will be showed.
    changeLoginState(state: LoginState( status: LoginStatus.LOGGING, message: "logging") );

    // waiting for login response!
    LoginResponse response = await _repository.postData(_loginResource);
    print(response); // just to text debug your response.

    //Here you can verify if the login process was successfully or if there is
    // some kind of error based in your LoginResponse model class.

    if(response.hasError){
      changeLoginState(state: LoginState(status: LoginStatus.LOGIN_ERROR,
          message: response.errorMessage)
      );
      // and after 1.5 seconds we make the error message disappear from UI.
      // you can do this in UI layer too
      Future.delayed(Duration(milliseconds: 1500), (){
        // you can pass null to state property, will make the same effect
        changeLoginState(state: LoginState(status: LoginStatus.NON_LOGIN)); });
    }

    else {
      changeLoginState(state: LoginState(status:
      LoginStatus.LOGIN_SUCCESS, message: "Login Success"));
    }
    //_subject.sink.add(response);
  }

  dispose() {
    _loginStateSubject.close();
    _email.close();
    _password.close();
    //_subject.close();
  }
}
final bloc = Bloc();
like image 79
Marcos Boaventura Avatar answered Feb 03 '23 13:02

Marcos Boaventura