Flutter - Cubit - loaded state - managing redirection to a page - 2 builds of the page are made

3.5k views Asked by At

Sorry for my english I'm French.

I develop in Flutter (dart) and I experience a strange behavior in my code using Cubit (Bloc) when I want to redirect to a page after a form submission (with "Reactive forms" package, but also with classic form) and the step of the Cubit loaded state: I see 2 calls to the page (2 builds) which gives a sort of "flapping" effect which means that the final user sees the interface charging twice.

It's my first application in Flutter.

I created an application containing a login form: when the form is submitted I print another form.

At the beginning of my application I was using "auto_route" package and I obtained a refresh of the page each time I clicked inside the text field after the login process. So I was not able to write anything inside the text field.

I was thinking that the problem came from the "Reactive forms" package so I opened an issue to the github repository of this package: issue opened

But as I didn't see where was the problem I came back to a much more basic development for my application and also a more basic method for managing the pages routing in order to explain my problem to the maintainer of the "Reactive forms" package, a really nice guy which really tried to help me.

But even the maintainer does not understand why I have this problem.

Now I reduced my more simple code in one page.

For the moment I don't have the problem when I clicked inside the text field but I see that interface is built twice and the Cubit loaded state which maybe explains why I had the initial problem.

So now I try to understand why the interface is built twice before debugging my original code.

I think my main problem is that the Cubit loaded state is waiting a synchronous widget return but when I try to redirect to a another page it needs an asynchronous action (with "auto_route" package or more simply using "Navigator.push()" action).

But I don't know how to call a Future inside a Cubit loaded state which wait a classic Widget.

I tried this:

  Widget myAuthBuildLoaded(context) {
    Timer.run(() {
      Navigator.push(context, MaterialPageRoute(builder: (context) => HomePage(1)));
    });
    return Container();
  }

So I think that the returned widget "return Container()" build the interface one time ans after the Navigator.push() build the interface another time. I tried to directly return "Navigator.push" but I have an error (because it is not a Widget).

I would really appreciate some help for this problem. Thanks.

Here is my full code (the more simple version).

My pubspec.yaml file:

name: myapi
description: MyApi mobile application
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
version: 1.0.0+1
environment:
  sdk: ">=2.12.0 <3.0.0"

dependencies:
  flutter:
    sdk: flutter
  bloc: ^7.0.0
  flutter_bloc: ^7.0.0
  reactive_forms: ^10.0.3

dependency_overrides:

dev_dependencies:

flutter:
  generate: true
  uses-material-design: true
  assets:
    - assets/images/

My code:

import 'dart:async';
import 'dart:developer';

import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:reactive_forms/reactive_forms.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  runApp(Application());
}

class AppColors {
  static const Color PRIMARY_COLOR = Colors.blue;
  static const Color ACCENT_COLOR = Colors.black;
  static const Color BG_COLOR_01 = Color(0xFFFFFFFF);
  static const Color BG_COLOR_02 = Color(0xFFDDE7DD);
  static const Color BG_COLOR_03 = Color(0xFFCCCFBD);
  static const Color TXT_COLOR_01 = Colors.black;
}

class Application extends StatefulWidget {
  @override
  ApplicationState createState() => ApplicationState();
}

class ApplicationState extends State<Application> {
  @override
  void initState() {
    super.initState();
  }

  @override
  void dispose() {
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    log("Build MyApi Application");

    return MaterialApp(
      title: 'MYAPI',
      showSemanticsDebugger: false,
      debugShowCheckedModeBanner: false,
      home: HomePage(0),
    );
  }
}

class HomePage extends StatefulWidget {
  final int indexSelected;
  HomePage(this.indexSelected) : super();

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

class _HomePageState extends State<HomePage> {
  List<Widget> _pages = [];
  int _indexSelected = 0;

  @override
  void initState() {
    super.initState();
    _pages.addAll([
      AuthPage(),
      ConnectedFirstPage(),
    ]);
  }

  @override
  Widget build(BuildContext context) {
    _indexSelected = widget.indexSelected;
    return Scaffold(
      body: Container(
        child: _pages.elementAt(_indexSelected),
      ),
    );
  }
}

class AuthPage extends StatelessWidget {
  AuthPage() : super();

  @override
  Widget build(BuildContext context) {
    log("Build AuthPage");

    final bool isPortrait = MediaQuery.of(context).orientation == Orientation.portrait;
    final FormGroup form = FormGroup({
      'client_code': FormControl(validators: [Validators.required]),
    });
    AuthCubit? authCubit;
    return BlocProvider<AuthCubit>(
      create: (context) {
        authCubit = AuthCubit(Auth(), form);
        // authCubit!.defineFormLogIn();
        // form = authCubit!.form;
        return authCubit!;
      },
      // child: Scaffold(
      child: SafeArea(
        child: Overlay(
          initialEntries: [
            OverlayEntry(
              builder: (context) => Scaffold(
                backgroundColor: Colors.white,
                body: Container(
                  child: Center(
                    child: Stack(
                      children: [
                        Column(
                          children: [
                            Expanded(
                              child: SingleChildScrollView(
                                child: Container(
                                  padding: EdgeInsets.fromLTRB(10, 150, 10, 10),
                                  margin: EdgeInsets.fromLTRB(10, 2, 10, 2),
                                  child: Column(
                                    crossAxisAlignment: CrossAxisAlignment.center,
                                    children: [
                                      SizedBox(height: 35.0),
                                      Container(
                                        child: Column(
                                          crossAxisAlignment: CrossAxisAlignment.center,
                                          children: [
                                            Row(
                                              mainAxisAlignment: MainAxisAlignment.center,
                                              children: [
                                                Wrap(
                                                  children: [
                                                    Container(
                                                      alignment: Alignment.topLeft,
                                                      child: RichText(
                                                        text: TextSpan(
                                                          text: "Login",
                                                          style: TextStyle(
                                                            fontSize: 26,
                                                            fontFamily: 'Times',
                                                            fontWeight: FontWeight.w700,
                                                            color: Theme.of(context).accentColor,
                                                          ),
                                                        ),
                                                      ),
                                                    ),
                                                  ],
                                                ),
                                              ],
                                            ),
                                          ],
                                        ),
                                      ),
                                      SizedBox(height: 10),
                                      Container(
                                        child: FractionallySizedBox(
                                          widthFactor: 0.7,
                                          // child: Form(
                                          child: ReactiveForm(
                                            formGroup: form,
                                            // formGroup: authCubit!.form!,
                                            // key: _formKey,
                                            child: Column(
                                              children: [
                                                BlocConsumer<AuthCubit, AuthState>(
                                                  listener: (context, state) {
                                                    if (state is AuthError) {
                                                      myAuthBuildError(context, state.message);
                                                    }
                                                  },
                                                  builder: (context, state) {
                                                    if (state is AuthInitial) {
                                                      return myAuthBuildInitial(context);
                                                    } else if (state is AuthLoading) {
                                                      return myAuthBuildLoading(context);
                                                    } else if (state is AuthLoaded) {
                                                      return myAuthBuildLoaded(context);
                                                    } else {
                                                      // In case of error we call the initial widget here and we handle the error with the above listener
                                                      return myAuthBuildInitial(context);
                                                    }
                                                  },
                                                )
                                              ],
                                            ),
                                          ),
                                        ),
                                      ),
                                      Container(
                                        child: SizedBox(height: 2.0),
                                      ),
                                    ],
                                  ),
                                ),
                              ),
                            ),
                          ],
                        ),
                      ],
                    ),
                  ),
                ),
              ),
            ),
          ],
        ),
      ),
      // ),
    );
  }

  void myAuthFormSubmit(context) async {
    log("Form 'client code' submitted!");
    final authCubit = BlocProvider.of<AuthCubit>(context);
    try {
      await authCubit.logIn();
    } on FormatException catch (e) {
      myAuthBuildError(context, e.message);
    }
  }

  Widget myAuthBuildInitial(context) {
    final form = BlocProvider.of<AuthCubit>(context).form;
    return ReactiveFormBuilder(
      form: () => form!,
      // form: form,
      builder: (context, form, child) {
        String _fieldName = "client_code";
        String _fieldTitle = "Enter your client code";
        String _msgRequired = "required field";
        double _padding = 10.0;
        return Stack(
          children: [
            Column(
              children: [
                Column(
                  crossAxisAlignment: CrossAxisAlignment.stretch,
                  children: [
                    // MyFormInputText(
                    //   fieldName: "client_code",
                    //   fieldTitle: "client code",
                    //   msgRequired: "required field",
                    //   isRequired: true,
                    // ),
                    Container(
                      height: 60.0,
                      child: Row(
                        children: [
                          Expanded(
                            child: ReactiveTextField(
                              // autofocus: true,
                              formControlName: _fieldName,
                              validationMessages: (control) => {ValidationMessage.required: _msgRequired},
                              style: TextStyle(
                                fontSize: 20,
                                color: Theme.of(context).accentColor,
                                fontFamily: 'Times',
                                fontWeight: FontWeight.w400,
                              ),
                              decoration: InputDecoration(
                                contentPadding: EdgeInsets.all(_padding),
                                focusColor: Theme.of(context).accentColor,
                                hoverColor: Theme.of(context).accentColor,
                                hintText: _fieldTitle,
                                border: OutlineInputBorder(
                                  borderRadius: BorderRadius.circular(30.0),
                                  borderSide: BorderSide(
                                    color: Theme.of(context).primaryColor,
                                  ),
                                ),
                                focusedBorder: OutlineInputBorder(
                                  borderRadius: BorderRadius.circular(30.0),
                                  borderSide: BorderSide(
                                    color: Theme.of(context).primaryColor,
                                  ),
                                ),
                              ),
                            ),
                          ),
                        ],
                      ),
                    ),
                  ],
                ),
                SizedBox(height: 10.0),
                ReactiveFormConsumer(
                  builder: (context, form, child) {
                    String mybuttonTitle = "Validate";
                    double mywidth = 100.0;
                    double myheight = 50.0;
                    double myradius = 20.0;
                    double myfontSize = 20;
                    String myfontFamily = "Times";
                    FontWeight myfontWeight = FontWeight.w400;
                    Color mybackgroundColor = AppColors.PRIMARY_COLOR;
                    Color mytextColor = Colors.white;
                    // return MyButtonValidate(buttonContext: context, buttonAction: () => myAuthFormSubmit(context));
                    return Container(
                      width: mywidth,
                      height: myheight,
                      child: Row(
                        mainAxisAlignment: MainAxisAlignment.center,
                        children: [
                          Expanded(
                            child: TextButton(
                              style: ButtonStyle(
                                foregroundColor: MaterialStateProperty.resolveWith((state) {
                                  return mytextColor;
                                }),
                                backgroundColor: MaterialStateProperty.resolveWith((state) {
                                  return mybackgroundColor;
                                }),
                                overlayColor: MaterialStateProperty.resolveWith((state) {
                                  return mybackgroundColor;
                                }),
                                padding: MaterialStateProperty.all(EdgeInsets.symmetric(vertical: 2.0, horizontal: 2.0)),
                                textStyle: MaterialStateProperty.all(
                                  TextStyle(
                                    fontSize: myfontSize,
                                    fontFamily: myfontFamily,
                                    fontWeight: myfontWeight,
                                  ),
                                ),
                                shape: MaterialStateProperty.resolveWith((state) {
                                  if (state.contains(MaterialState.disabled) && form != null && form.valid) {
                                    return RoundedRectangleBorder(
                                      borderRadius: BorderRadius.circular(myradius),
                                      side: BorderSide(
                                        color: AppColors.ACCENT_COLOR.withAlpha(90),
                                      ),
                                    );
                                  } else {
                                    return RoundedRectangleBorder(
                                      borderRadius: BorderRadius.circular(myradius),
                                      side: BorderSide(
                                        color: mybackgroundColor,
                                      ),
                                    );
                                  }
                                }),
                              ),
                              child: Row(
                                mainAxisAlignment: MainAxisAlignment.center,
                                children: [
                                  Text(mybuttonTitle),
                                ],
                              ),
                              onPressed: () => myAuthFormSubmit(context),
                            ),
                          ),
                        ],
                      ),
                    );
                  },
                ),
              ],
            ),
          ],
        );
      },
    );
  }

  Widget myAuthBuildLoading(context) {
    return CircularProgressIndicator(backgroundColor: Theme.of(context).primaryColor);
  }

  Widget myAuthBuildLoaded(context) {
    Timer.run(() {
      Navigator.push(context, MaterialPageRoute(builder: (context) => HomePage(1)));
    });
    return Container();
  }

  myAuthBuildError(context, message) {
    return Text("Error", style: TextStyle(fontWeight: FontWeight.bold, color: Colors.red));
  }
}

class AuthCubit extends Cubit<AuthState> {
  final Auth? _auth;
  final FormGroup? form;

  String? _clientCode = "";
  AuthCubit(this._auth, this.form) : super(AuthInitial());

  // bool _isFormValid = false;

  Auth get getAuth => _auth!;

  // defineFormLogIn() {
  //   log("Info: defineFormLogIn");
  //   form = FormGroup({
  //     'client_code': FormControl(validators: [Validators.required]),
  //   });
  // }

  Future<void> logIn() async {
    _clientCode = form!.control("client_code").value.toString();
    log("Info: Form - _clientCode=$_clientCode");
    try {
      emit(AuthLoading());
      await Future.delayed(const Duration(milliseconds: 2000), () {
        log("AuthCubit - logIn: Handle something!");
      });
      emit(AuthLoaded(_auth!));
    } on Exception {
      emit(AuthError("impossible to connect to myapi"));
    }
  }
}

@immutable
abstract class AuthState {
  const AuthState();
}

class AuthInitial extends AuthState {
  const AuthInitial();
}

class AuthLoading extends AuthState {
  const AuthLoading();
}

class AuthLoaded extends AuthState {
  final Auth auth;
  const AuthLoaded(this.auth);

  @override
  bool operator ==(Object o) {
    if (identical(this, o)) return true;

    return o is AuthLoaded && o.auth == auth;
  }

  @override
  int get hashCode => auth.hashCode;
}

class AuthError extends AuthState {
  final String message;
  const AuthError(this.message);

  @override
  bool operator ==(Object o) {
    if (identical(this, o)) return true;

    return o is AuthError && o.message == message;
  }

  @override
  int get hashCode => message.hashCode;
}

class Auth {
  String _clientCode = "";
  String state = "not connected";
  bool isConnected = false;

  Auth();
}

class ConnectedFirstPage extends StatelessWidget {
  ConnectedFirstPage() : super();

  final FormGroup form = FormGroup({
    'event_id': FormControl(),
  });

  @override
  Widget build(BuildContext context) {
    log("Build ConnectedFirstPage");
    return SafeArea(
      child: Scaffold(
        body: SingleChildScrollView(
          // child: ReactiveForm(
          //   formGroup: form,
          //   child: ReactiveTextField(
          //     formControlName: "event_id",
          //     style: TextStyle(
          //       fontSize: 15,
          //       color: Theme.of(context).accentColor,
          //       fontFamily: 'Times',
          //       fontWeight: FontWeight.w400,
          //     ),
          //     decoration: InputDecoration(
          //       hintText: "My event",
          //     ),
          //   ),
          // ),

          child: ReactiveFormBuilder(
            form: () => form,
            builder: (context, form, child) {
              return ReactiveTextField(
                formControlName: "event_id",
                style: TextStyle(
                  fontSize: 20,
                  color: Theme.of(context).accentColor,
                  fontFamily: 'Times',
                  fontWeight: FontWeight.w400,
                ),
                decoration: InputDecoration(
                  hintText: "Event ID",
                ),
              );
            },
          ),
        ),
      ),
    );
  }
}

My "flutter doctor -v" result:

[✓] Flutter (Channel stable, 2.0.6, on macOS 11.2.1 20D74 darwin-arm, locale fr-FR)
    • Flutter version 2.0.6 at /opt/homebrew/Caskroom/flutter/1.22.6/flutter
    • Framework revision 1d9032c7e1 (4 weeks ago), 2021-04-29 17:37:58 -0700
    • Engine revision 05e680e202
    • Dart version 2.12.3

[✓] Android toolchain - develop for Android devices (Android SDK version 30.0.3)
    • Android SDK at /Users/mycompany/Library/Android/sdk
    • Platform android-30, build-tools 30.0.3
    • Java binary at: /Applications/Android Studio.app/Contents/jre/jdk/Contents/Home/bin/java
    • Java version OpenJDK Runtime Environment (build 1.8.0_242-release-1644-b3-6915495)
    • All Android licenses accepted.

[✓] Xcode - develop for iOS and macOS
    • Xcode at /Applications/Xcode.app/Contents/Developer
    • Xcode 12.4, Build version 12D4e
    • CocoaPods version 1.10.1

[✓] Chrome - develop for the web
    • Chrome at /Applications/Google Chrome.app/Contents/MacOS/Google Chrome

[✓] Android Studio (version 4.1)
    • Android Studio at /Applications/Android Studio.app/Contents
    • Flutter plugin can be installed from:
       https://plugins.jetbrains.com/plugin/9212-flutter
    • Dart plugin can be installed from:
       https://plugins.jetbrains.com/plugin/6351-dart
    • Java version OpenJDK Runtime Environment (build 1.8.0_242-release-1644-b3-6915495)

[✓] Connected device (2 available)
    • sdk gphone arm64 (mobile) • emulator-5554 • android-arm64  • Android 11 (API 30) (emulator)
    • Chrome (web)              • chrome        • web-javascript • Google Chrome 90.0.4430.212

• No issues found!
1

There are 1 answers

1
AudioBubble On BEST ANSWER

I believe that I have solved your problem. This problem lies within your BlocConsumer widget.

The builder method of the BlocConsumer widget is called multiple times whenever the state of your AuthCubit changes. This results in myAuthBuildLoaded() pushing the page twice. That is what is causing the flickering effect. To avoid this, see the example below. The listener method of the BlocConsumer widget is only called once on every state change. That should revolve your problem.


BlocConsumer<AuthCubit,AuthState>(
  listener: (context, state) {
    if (state is AuthError) {
      myAuthBuildError(context, state.message);
    } //
    // Add this here.
    else if (state is AuthLoaded) {
      myAuthBuildLoaded(context);
    }
  },
  builder: (context, state) {
    if (state is AuthInitial) {
      return myAuthBuildInitial(context);
    } // 
    else if (state is AuthLoading) {
      return myAuthBuildLoading(context);
    } //
    // Remove this here.
    // else if (state is AuthLoaded) {
    //  return myAuthBuildLoaded(context);
    //} //
    else {
      // In case of error we call the initial widget here and we handle the
      // error with the above listener
      return myAuthBuildInitial(context);
    }
  },
),

I am not exactly sure if this was the problem you were trying to solve. Let me know if I can help in any other way!