Flutter - Cubit - 加载状态 - 管理重定向到页面 - 构建了 2 个页面

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

对不起我的英语我是法国人。

我在 Flutter (dart) 中开发,当我想在提交表单后重定向到一个页面时,我在使用 Cubit (Bloc) 的代码中遇到了一个奇怪的行为(使用“Reactive forms”包,但也使用经典形式)和 Cubit 加载状态的步骤:我看到对页面的 2 次调用(2 次构建)给出了一种“拍打”效果,这意味着最终用户看到界面充电两次。

这是我在 Flutter 中的第一个应用程序。

我创建了一个包含登录表单的应用程序:提交表单后,我打印另一个表单。

在我的应用程序开始时,我使用了“auto_route”包,每次在登录过程后在文本字段内单击时,我都会刷新页面。所以我无法在文本字段中写入任何内容。

我认为问题出在“Reactive forms”包中,所以我向该包的 github 存储库提出了一个问题:issue opened

但是因为我没有看到问题出在哪里,所以我回到了我的应用程序的更基本的开发以及管理页面路由的更基本的方法,以便向维护者解释我的问题“Reactive forms”包,一个非常好的人,他真的试图帮助我。

但是连维护者都不明白我为什么会出现这个问题

现在我将更简单的代码减少到一页。

目前,当我在文本字段内单击时,我没有遇到问题,但我看到界面构建了两次,Cubit 加载状态可能解释了为什么我遇到了最初的问题。

所以现在我试着理解为什么在调试我的原始代码之前界面被构建了两次。

我认为我的主要问题是 Cubit 加载状态正在等待同步小部件 return 但是当我尝试重定向到另一个页面时它需要一个异步操作(使用“auto_route”打包或更简单地使用“Navigator.push()”操作。

但我不知道如何在等待经典 Widget 的 Cubit 加载状态中调用 Future。

我试过这个:

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

所以我认为 returned 小部件“return Container()”一次构建界面,然后 Navigator.push() 再次构建界面。 我试图直接 return "Navigator.push" 但是我有一个错误(因为它不是一个 Widget)。

对于这个问题,我非常感谢您的帮助。 谢谢。

这是我的完整代码(更简单的版本)。

我的 pubspec.yaml 文件:

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/

我的代码:

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",
                ),
              );
            },
          ),
        ),
      ),
    );
  }
}

我的“flutter doctor -v”结果:

[✓] 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!

我相信我已经解决了你的问题。此问题出在您的 BlocConsumer 小部件中。

只要 AuthCubit 的状态发生变化,BlocConsumer 小部件的 builder 方法就会被多次调用。这导致 myAuthBuildLoaded() 将页面推送两次。这就是造成闪烁效果的原因。为避免这种情况,请参见下面的示例。 BlocConsumer 小部件的 listener 方法仅在每次状态更改时调用一次。那应该可以解决你的问题。


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);
    }
  },
),

我不确定这是否是您要解决的问题。如果我能以任何其他方式提供帮助,请告诉我!