Flutter - Cubit - загруженное состояние - управление перенаправлением на страницу - выполнено 2 сборки страницы

Извините за мой английский, я француз.

Я разрабатываю во Flutter (дротик), и у меня возникает странное поведение в моем коде с использованием Cubit (Bloc), когда я хочу перенаправить на страницу после отправки формы (с пакетом реактивных форм, но также и с классической формой) и на этапе Состояние загрузки Cubit: я вижу 2 вызова страницы (2 сборки), что дает своего рода эффект колебания, что означает, что конечный пользователь видит, что интерфейс заряжается дважды.

Это мое первое приложение на Flutter.

Я создал приложение, содержащее форму входа: когда форма отправлена, я распечатываю другую форму.

В начале своего приложения я использовал пакет auto_route и получал обновление страницы каждый раз, когда щелкал внутри текстового поля после входа в систему. Так что я не смог ничего написать в текстовое поле.

Я думал, что проблема возникла из пакета реактивных форм, поэтому я открыл проблему в репозитории github этого пакета: проблема открыта

Но поскольку я не видел, в чем была проблема, я вернулся к гораздо более базовой разработке моего приложения, а также к более базовому методу управления маршрутизацией страниц, чтобы объяснить свою проблему разработчику пакета реактивных форм, a действительно хороший парень, который действительно пытался мне помочь.

Но даже сопровождающий не понимает, почему у меня такая проблема.

Теперь я сократил свой более простой код на одной странице.

На данный момент у меня нет проблемы, когда я щелкнул внутри текстового поля, но я вижу, что интерфейс построен дважды и загружено состояние Cubit, что, возможно, объясняет, почему у меня возникла первоначальная проблема.

Итак, теперь я пытаюсь понять, почему интерфейс создается дважды перед отладкой моего исходного кода.

Я думаю, что моя основная проблема заключается в том, что загруженное состояние Cubit ожидает возврата синхронного виджета, но когда я пытаюсь перенаправить на другую страницу, ему требуется асинхронное действие (с пакетом auto_route или, проще говоря, с помощью действия Navigator.push ()).

Но я не знаю, как вызвать будущее в загруженном состоянии Cubit, которое ожидает классический виджет.

Я пробовал это:

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

Итак, я думаю, что возвращенный виджет return Container () создает интерфейс один раз, а после того, как Navigator.push () создает интерфейс в другой раз. Я попытался напрямую вернуть 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, 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",
                ),
              );
            },
          ),
        ),
      ),
    );
  }
}

Мой трепещущий доктор -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!

person Johann    schedule 28.05.2021    source источник
comment
Даже когда я заменяю форму текстовым виджетом на подключенной странице, у меня возникает проблема. В консоли отладки я вижу следующее: 2 [log] Build ConnectedFirstPage В классе ConnectedFirstPage я заменил child: ReactiveFormBuilder ... на: child: Text (Connected, style: TextStyle (fontWeight: FontWeight.bold, color: Colors.red) ),   -  person Johann    schedule 28.05.2021


Ответы (1)


Я считаю, что решил вашу проблему. Эта проблема кроется в вашем виджете 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);
    }
  },
),

Я не совсем уверен, была ли это проблема, которую вы пытались решить. Дайте мне знать, если я могу чем-то помочь!

person Community    schedule 28.05.2021
comment
ДА!!! Это решение! Вы спасли мою жизнь! :-) - person Johann; 31.05.2021