BLoC 在更新模型时表现错误

BLoC behaved wrongly when updating the Model

我正在编写一个类似的任务管理器应用程序,我在使用 BLoC 时遇到了一些问题:

但是,当用户按下“保存”按钮时,名称字段和 ratePerHour 字段始终为空,即使我在每个 TextField() 中调用 onChanged: 来更新它们也是如此。

这是我的 flutter 医生:

Doctor summary (to see all details, run flutter doctor -v):
[√] Flutter (Channel stable, 2.2.1, on Microsoft Windows [Version 10.0.19042.985], locale en-US)
[!] Android toolchain - develop for Android devices (Android SDK version 30.0.3)
    X Android license status unknown.
      Run `flutter doctor --android-licenses` to accept the SDK licenses.
      See https://flutter.dev/docs/get-started/install/windows#android-setup for more details.
[√] Chrome - develop for the web
[√] Android Studio (version 4.1.0)
[√] Connected device (3 available)

! Doctor found issues in 1 category.

*** 1 个类别中有 1 个问题,但没关系,因为我 运行 在真实设备上出现抖动。

这是我的 pbspec.yaml:

name: untitled
description: A new Flutter project.

    # The following line prevents the package from being accidentally published to
    # pub.dev using `pub publish`. This is preferred for private packages.
    publish_to: 'none' # Remove this line if you wish to publish to pub.dev
    
    # The following defines the version and build number for your application.
    # A version number is three numbers separated by dots, like 1.2.43
    # followed by an optional build number separated by a +.
    # Both the version and the builder number may be overridden in flutter
    # build by specifying --build-name and --build-number, respectively.
    # In Android, build-name is used as versionName while build-number used as versionCode.
    # Read more about Android versioning at https://developer.android.com/studio/publish/versioning
    # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
    # Read more about iOS versioning at
    # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
    version: 1.0.0+1
    
    environment:
      sdk: ">=2.7.0 <3.0.0"
    
    dependencies:
      flutter:
        sdk: flutter
    
    
      # The following adds the Cupertino Icons font to your application.
      # Use with the CupertinoIcons class for iOS style icons.
      cupertino_icons: ^1.0.2
      firebase_auth: ^0.14.0+5
      google_sign_in: ^4.0.7
      flutter_facebook_login: ^2.0.1
      provider: ^5.0.0
      cloud_firestore: ^0.12.9+5
    
    dev_dependencies:
      flutter_test:
        sdk: flutter
    
    # For information on the generic Dart part of this file, see the
    # following page: https://dart.dev/tools/pub/pubspec
    
    # The following section is specific to Flutter.
    flutter:
    
      # The following line ensures that the Material Icons font is
      # included with your application, so that you can use the icons in
      # the material Icons class.
      uses-material-design: true
    
      # To add assets to your application, add an assets section, like this:
      assets:
        - images/
    
      # An image asset can refer to one or more resolution-specific "variants", see
      # https://flutter.dev/assets-and-images/#resolution-aware.
    
      # For details regarding adding assets from package dependencies, see
      # https://flutter.dev/assets-and-images/#from-packages
    
      # To add custom fonts to your application, add a fonts section here,
      # in this "flutter" section. Each entry in this list should have a
      # "family" key with the font family name, and a "fonts" key with a
      # list giving the asset and other descriptors for the font. For
      # example:
      # fonts:
      #   - family: Schyler
      #     fonts:
      #       - asset: fonts/Schyler-Regular.ttf
      #       - asset: fonts/Schyler-Italic.ttf
      #         style: italic
      #   - family: Trajan Pro
      #     fonts:
      #       - asset: fonts/TrajanPro.ttf
      #       - asset: fonts/TrajanPro_Bold.ttf
      #         weight: 700
      #
      # For details regarding fonts from package dependencies,
      # see https://flutter.dev/custom-fonts/#from-packages

这是我的代码: *** 工作页面():

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:untitled/app/home/job_form/jobs_form.dart';
import 'package:untitled/app/home/models/job.dart';
import 'package:untitled/common_widgets/platform_alert_dialog.dart';
import 'package:untitled/services/auth.dart';
import 'package:untitled/services/database.dart';

class JobsPage extends StatelessWidget {


  Future<void> _signOut(BuildContext context) async{
    try {
      final auth = Provider.of<AuthBase>(context, listen: false) ;
      await auth.signOut();
    } catch(e){
      print(e.toString());
    }
  }

  Future<void> _confirmSignOut(BuildContext context) async{
    final didRequestSignOut = await PlatformAlertDialog(
        title: 'Sign out',
        content: 'Are you sure?',
      cancelText: 'No',
    ).show(context);
    if (didRequestSignOut == true){
      _signOut(context);
    }
  }

  void _navigateToJobForm(BuildContext context){
    Navigator.of(context).push(
      MaterialPageRoute<void>(
        fullscreenDialog: true,
        builder: (_) => JobForm.create(context),
      ),
    );
  }


  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('JOB'),
        centerTitle: true,
        actions: <Widget>[
          TextButton(
            child: Text(
                'Sign out',
              style: TextStyle(
                color: Colors.white,
                fontSize: 18.0,
              ),
            ),
            onPressed: () => _confirmSignOut(context),
          ),
        ],
      ),
      floatingActionButton: FloatingActionButton(
        child: Icon(Icons.add),
        onPressed: () => _navigateToJobForm(context),
      ),
      body:  _buildContents(context),
    );
  }

  Widget _buildContents(BuildContext context) {
    final database = Provider.of<Database>(context);

    return StreamBuilder<List<Job>>(
      stream: database.jobsStream(),
        builder: (context, snapshot) {
        if(snapshot.hasData){
          final jobs = snapshot.data;
          final children = jobs.map((job) => Text(job.name)).toList();
          return ListView(
            children: children,
          );
        }
        if(snapshot.hasError){
          return Center(child: Text('Some error occurred'));
        }
        return Center(
          child: CircularProgressIndicator(),
        );
        }
    );
  }
}

*** JobForm():

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:provider/provider.dart';
import 'package:untitled/app/home/job_form/job_form_bloc.dart';
import 'package:untitled/app/home/job_form/job_form_model.dart';
import 'package:untitled/app/sign_in/validator.dart';
import 'package:untitled/common_widgets/platform_exception_alert_dialog.dart';
import 'package:untitled/services/database.dart';

class JobForm extends StatefulWidget {
  const JobForm({Key key,  @required this.bloc}) : super(key: key);
  final JobFormBloc bloc;

  static Widget create(BuildContext context){
    final Database database = Provider.of<Database>(context, listen: false);
    return Provider(
      create: (context) => JobFormBloc(database: database),
      child: Consumer<JobFormBloc>(
        builder: (context, bloc, _) => JobForm(bloc: bloc),
      ),
      dispose: (context, bloc) => bloc.dispose(),
    );
  }
  @override
  _JobFormState createState() => _JobFormState();
}

class _JobFormState extends State<JobForm> with EmailAndPasswordValidator {

  final TextEditingController _nameController =  TextEditingController();
  final TextEditingController _ratePerHourController =  TextEditingController();
  final FocusNode _nameFocusNode = FocusNode();
  final FocusNode _ratePerHourFocusNode = FocusNode();



  @override
  void dispose(){
    _nameController.dispose();
    _nameFocusNode.dispose();
    _ratePerHourFocusNode.dispose();
    _ratePerHourController.dispose();
    super.dispose();
  }
  void _onEditingNameComplete(JobFormModel model){
    print(model.name);
    final newNode =  model.emailValidator.isValid(model.name) ? _ratePerHourFocusNode : _nameFocusNode;
    FocusScope.of(context).requestFocus(newNode);
  }

  Future<void> _submit() async {
    try{
        await widget.bloc.submitToFirebase();
        Navigator.of(context).pop();
    } on PlatformException catch (e){
      PlatformExceptionAlertDialog(
        title: 'Error',
        exception: e,
      ).show(context);
    }
  }

  @override
  Widget build(BuildContext context) {
    return StreamBuilder<JobFormModel>(
      stream: widget.bloc.modelStream,
      initialData: JobFormModel(),
      builder: (context, snapshot) {
        final JobFormModel model = snapshot.data;
        return Scaffold(
          appBar: AppBar(
            title: Text('Create new job'),
            centerTitle: true,
            actions: <Widget>[
              TextButton(
                onPressed: model.isLoading ? null :  _submit,
                child: Text(
                  'SAVE',
                  style: TextStyle(
                    color: Colors.white,
                    fontSize: 16.0,
                  ),
                ),
              )
            ],
            leading: IconButton(
              icon: Icon(Icons.arrow_back),
              onPressed: () => !model.isLoading ? Navigator.of(context).pop() : null,
            ),
          ),
          body: Padding(
            padding: const EdgeInsets.all(16.0),
            child: Card(
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.stretch,
                mainAxisSize: MainAxisSize.min,
                children: _buildContent(model),
              ),
            ),
          ),
        );
      }
    );
  }





  List<Widget> _buildContent(JobFormModel model) {
    return [

      _buildNameTextField(model),

      SizedBox(height: 8.0),

      _buildRatePerHourTextField(model),

      SizedBox(height: 8.0),

    ];
  }

  TextField _buildRatePerHourTextField(JobFormModel model) {
    return TextField(
      controller: _ratePerHourController,
      keyboardType: TextInputType.number,
      decoration: InputDecoration(
        labelText: 'Rate per hour',
        hintText: '0',
        errorText: model.ratePerHourErrorText ,
        enabled: model.isLoading == false,
      ),
      autocorrect: false,
      inputFormatters: [
        FilteringTextInputFormatter.digitsOnly,
      ],
      focusNode: _ratePerHourFocusNode,
      textInputAction: TextInputAction.done,
      onEditingComplete:  _submit,
      onChanged: widget.bloc.updateRatePerHour,
    );
  }

  TextField _buildNameTextField(JobFormModel model) {
    return TextField(
      controller: _nameController,
      focusNode: _nameFocusNode,
      decoration: InputDecoration(
        labelText: 'Job name',
        hintText: 'Blogging',
        errorText: model.nameErrorText,
        enabled: model.isLoading == false,
      ),
      autocorrect: false,
      textInputAction: TextInputAction.next,
      onChanged:  widget.bloc.updateName,
      onEditingComplete:() => _onEditingNameComplete(model),
    );
  }


}

*** JobFormModel():

import 'package:untitled/app/sign_in/validator.dart';

class JobFormModel with EmailAndPasswordValidator{
  JobFormModel(
      {
      this.isLoading = false ,
      this.ratePerHour = '10',
      this.name = '',
        this.submitted = false,
      });

  final bool isLoading ;
  final String ratePerHour;
  final String name;
  final bool submitted;

  bool get canPop {
    return !isLoading;
  }

   bool get canSave{
    return emailValidator.isValid(name) && emailValidator.isValid(ratePerHour) && !isLoading;
   }

   String get nameErrorText{
    bool show = !emailValidator.isValid(name) ;
    return show ? "Can't leave the Name field  empty" : null;
   }

   String get ratePerHourErrorText{
    bool show = !emailValidator.isValid(ratePerHour);
    return show ? "Can't leave this Field empty" : null;
   }

   JobFormModel copyWith({
      bool submitted,
      bool isLoading,
      String ratePerHour,
      String name,
}){
    return JobFormModel(
      isLoading: isLoading ?? this.isLoading,
      submitted: submitted ?? this.submitted,
      ratePerHour: ratePerHour ?? this.ratePerHour,
      name: name ?? this.name,
    );
}
}

*** JobFormBloc():

import 'dart:async';

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:untitled/app/home/job_form/job_form_model.dart';
import 'package:untitled/app/home/models/job.dart';
import 'package:untitled/services/database.dart';

class JobFormBloc {
  JobFormBloc({@required this.database});

  final Database database;
  // ignore: close_sinks
  final StreamController<JobFormModel> _modelController =
      StreamController<JobFormModel>();

  Stream get modelStream => _modelController.stream;

  JobFormModel _model = JobFormModel();

  void dispose() {
    _modelController.close();
  }

  Future<void> submitToFirebase() async {
    updateWith(
      isLoading: true,
      submitted: true,
    );
    try {
      await database.createJob(Job(
        name: _model.name,
        ratePerHour: int.parse(_model.ratePerHour),
      ));
    } catch (e) {
      updateWith(
        isLoading: false,
      );
      rethrow;
    }
  }

  void updateName(String name) => updateWith(name: name);

  void updateRatePerHour(String ratePerHour) =>
      updateWith(ratePerHour: ratePerHour);

  void updateWith({
    String name,
    String ratePerHour,
    bool submitted,
    bool isLoading,
  }) {
    _model.copyWith(
      name: name,
      ratePerHour: ratePerHour,
      isLoading: isLoading,
      submitted: submitted,
    );
    _modelController.add(_model);
  }
}

*** class 数据库():

import 'dart:async';
import 'package:meta/meta.dart';
import 'package:untitled/app/home/models/job.dart';
import 'package:untitled/services/api_path.dart';
import 'package:untitled/services/firestore_services.dart';

abstract class Database {
  Future<void> createJob(Job job);
  Stream<List<Job>> jobsStream();
}

final _service = FirestoreServices.instance;

class FirestoreDatabase implements Database {
  FirestoreDatabase({@required this.uid}) : assert(uid != null);
  final String uid;

  Future<void> createJob(Job job) async => await _service.setData(
    path: APIPath.job(uid, 'job_abc'),
    data: job.toMap(),
  );

  Stream<List<Job>> jobsStream() => _service.collectionStream(
    path: APIPath.jobs(uid),
    builder: (data) => Job.fromMap(data),
  );


}

问题出在 JobFormBlocupdateWith 方法上。

目前您有以下内容:

    _model.copyWith(
      name: name,
      ratePerHour: ratePerHour,
      isLoading: isLoading,
      submitted: submitted,
    );

_model.copyWith returns 具有更新值的新模型,但 不会更新 _model随它变。新型号未使用。

因此,为了更新值,您应该将更新后的模型分配给 _model 变量,如下所示:

_model = _model.copyWith(
  name: name,
  ratePerHour: ratePerHour,
  isLoading: isLoading,
  submitted: submitted,
);