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

问题描述

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

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

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

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

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

我认为问题来自“Reactive forms”包,所以我向这个包的github存储库打开了一个问题:issue opened

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

但即使是维护者也不明白为什么我会遇到这个问题。

现在我在一页中减少了更简单的代码

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

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

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

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

我试过了:

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

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

对于这个问题,我真的很感激一些帮助。 谢谢。

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

我的 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,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,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,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),SizedBox(height: 10.0),ReactiveFormConsumer(
                  builder: (context,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,child) {
              return ReactiveTextField(
                formControlName: "event_id",style: TextStyle(
                  fontSize: 20,decoration: Inputdecoration(
                  hintText: "Event ID",);
            },);
  }
}

我的“颤动医生 -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 小部件中。

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


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

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