BLoC 在更新模型时表现错误
BLoC behaved wrongly when updating the Model
我正在编写一个类似的任务管理器应用程序,我在使用 BLoC 时遇到了一些问题:
- 首先,我创建了一个页面来显示通过按下下面的 FloatingActionButton() 添加的所有任务。
- 接下来,当用户通过单击右上角的 FlatButton 完成表单(称为 SAVE)时,它将通过 JobForm class 中的 _submit() 方法提交给 Firestore。
- 注意:我还添加了一些验证器来验证 NameForm 和 RatePerHourForm,因此当它们为 null 并且它们运行良好时会显示错误。
- 每当来自 Firestore 的数据发生变化时,我都使用 StreamBuilder() 来更新我的 JobPage()(第一个屏幕)。
- 这是我正在尝试做的事情的总结。
但是,当用户按下“保存”按钮时,名称字段和 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),
);
}
问题出在 JobFormBloc
的 updateWith
方法上。
目前您有以下内容:
_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,
);
我正在编写一个类似的任务管理器应用程序,我在使用 BLoC 时遇到了一些问题:
- 首先,我创建了一个页面来显示通过按下下面的 FloatingActionButton() 添加的所有任务。
- 接下来,当用户通过单击右上角的 FlatButton 完成表单(称为 SAVE)时,它将通过 JobForm class 中的 _submit() 方法提交给 Firestore。
- 注意:我还添加了一些验证器来验证 NameForm 和 RatePerHourForm,因此当它们为 null 并且它们运行良好时会显示错误。
- 每当来自 Firestore 的数据发生变化时,我都使用 StreamBuilder() 来更新我的 JobPage()(第一个屏幕)。
- 这是我正在尝试做的事情的总结。
但是,当用户按下“保存”按钮时,名称字段和 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),
);
}
问题出在 JobFormBloc
的 updateWith
方法上。
目前您有以下内容:
_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,
);