flutter - Flutter:为什么表单(编辑页面)在提交并保存表单数据后重新加载,然后导航到其他地方
问题描述
我正在编写一个教程,并尝试按原样编写代码。但是,在提交表单之后,就在导航之前,它会尝试重新加载表单,而我没有这样的意图。我正在使用选项卡来创建产品并加载新页面来编辑产品。
主要.dart
import 'package:scoped_model/scoped_model.dart';
// import 'package:flutter/rendering.dart';
import './pages/auth.dart';
import './pages/products_admin.dart';
import './pages/products.dart';
import './pages/product.dart';
import './scoped-models/products.dart';
void main() {
// debugPaintSizeEnabled = true;
// debugPaintBaselinesEnabled = true;
// debugPaintPointersEnabled = true;
runApp(MyApp());
}
class MyApp extends StatefulWidget {
@override
State<StatefulWidget> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
@override
Widget build(BuildContext context) {
return ScopedModel<ProductsModel>(
model: ProductsModel(),
child: MaterialApp(
// debugShowMaterialGrid: true,
theme: ThemeData(
brightness: Brightness.light,
primarySwatch: Colors.deepOrange,
accentColor: Colors.green,
buttonTheme: ButtonThemeData(
buttonColor: Colors.deepOrange,
textTheme: ButtonTextTheme.primary,
)),
// home: AuthPage(),
routes: {
'/': (BuildContext context) => AuthPage(),
'/products': (BuildContext context) => ProductsPage(),
'/admin': (BuildContext context) => ProductsAdminPage(),
},
onGenerateRoute: (RouteSettings settings) {
final List<String> pathElements = settings.name.split('/');
if (pathElements[0] != '') {
return null;
}
if (pathElements[1] == 'product') {
final int index = int.parse(pathElements[2]);
return MaterialPageRoute<bool>(
builder: (BuildContext context) {
return ProductPage(index);
},
);
}
return null;
},
onUnknownRoute: (RouteSettings settings) {
return MaterialPageRoute(
builder: (BuildContext context) => ProductsPage(),
);
},
),
);
}
}
登陆页面 products.dart(在表单提交后加载)
import 'package:flutter/material.dart';
import '../widgets/navigation/products.dart';
import '../widgets/products/products.dart';
class ProductsPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
drawer: NavigationProductsPage(),
appBar: AppBar(
title: Text('EasyList'),
actions: [
IconButton(
icon: Icon(Icons.favorite),
onPressed: () {
},
)
],
),
body: Products(),
);
}
}
标签页 products_admin.dart
import 'package:flutter/material.dart';
import '../widgets/navigation/products_admin.dart';
import './product_list.dart';
import 'product_edit.dart';
class ProductsAdminPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return DefaultTabController(
length: 2,
child: Scaffold(
drawer: NavigationProductsAdminPage(),
appBar: AppBar(
title: Text('Manage Products'),
bottom: TabBar(
tabs: [
Tab(
icon: Icon(Icons.create),
text: 'Create Product',
),
Tab(
icon: Icon(Icons.list),
text: 'My Products',
),
],
),
),
body: TabBarView(
children: [
ProductEditPage(),
ProductListPage(),
],
),
),
);
}
}
问题页面 product_edit.dart(提交后,导航前,尝试重新加载表单并给出 Material 错误,当前列表索引为空。
import 'package:flutter/material.dart';
import 'package:scoped_model/scoped_model.dart';
import '../models/product.dart';
import '../scoped-models/products.dart';
class ProductEditPage extends StatefulWidget {
@override
_ProductEditPageState createState() => _ProductEditPageState();
}
// good habit to set your variables as private by prefixing with _, inside the state of a widget
class _ProductEditPageState extends State<ProductEditPage> {
final Map<String, dynamic> _formData = {
'title': null,
'description': null,
'price': null,
'image': 'assets/food.jpg',
};
final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
@override
Widget build(BuildContext context) {
return ScopedModelDescendant<ProductsModel>(
builder: (BuildContext context, Widget child, ProductsModel model) {
Product selectedProduct = model.selectedProduct;
final Widget pageContent = _buildPageContent(context, selectedProduct);
print('[selected index] ' + model.selectedProductIndex.toString());
return model.selectedProductIndex == null
? pageContent
: Scaffold(
appBar: AppBar(
title: Text('Edit Product'),
),
body: pageContent,
);
},
);
}
Widget _buildTitleTextField(Product selectedProduct) {
print('[just before title text field]');
return TextFormField(
initialValue: selectedProduct == null ? '' : selectedProduct.title,
decoration: InputDecoration(
labelText: 'Product Title',
),
validator: (value) {
if (value.isEmpty || value.length < 5) {
return 'Title is required and should be 5+ characters.';
}
return null;
},
onSaved: (value) {
_formData['title'] = value;
},
);
}
Widget _buildDescriptionTextField(Product selectedProduct) {
print('[just before description text field]');
return TextFormField(
initialValue: selectedProduct == null ? '' : selectedProduct.description,
decoration: InputDecoration(
labelText: 'Product Description',
),
validator: (value) {
if (value.isEmpty || value.length < 10) {
return 'Description is required and should be 10+ characters.';
}
return null;
},
maxLines: 4,
onSaved: (value) {
_formData['description'] = value;
},
);
}
Widget _buildPriceTextField(Product selectedProduct) {
print('[just before price text field]');
return TextFormField(
initialValue:
selectedProduct == null ? '' : selectedProduct.price.toString(),
decoration: InputDecoration(
labelText: 'Product Price',
),
validator: (value) {
if (value.isEmpty ||
!RegExp(r'^(?:[1-9]\d*|0)?(?:\.\d+)?$').hasMatch(value)) {
return 'Price is required and should be a number.';
}
return null;
},
keyboardType: TextInputType.number,
onSaved: (String value) {
_formData['price'] = double.parse(value);
},
);
}
_buildSubmitButton() {
return ScopedModelDescendant<ProductsModel>(
builder: (BuildContext context, Widget child, ProductsModel model) {
return RaisedButton(
child: Text('Save'),
onPressed: () => _submitForm(model.addProduct, model.updateProduct,
model.selectedProductIndex),
);
},
);
}
void _submitForm(Function addProduct, Function updateProduct,
[int selectedProductIndex]) {
if (!_formKey.currentState.validate()) {
// this will force post-validation error messages to show and not submit further
return;
}
_formKey.currentState
.save(); // this will initiate the onSaved event of formfields
if (selectedProductIndex == null) {
addProduct(
Product(
title: _formData['title'],
description: _formData['description'],
price: _formData['price'],
image: _formData['image']),
);
} else {
updateProduct(
Product(
title: _formData['title'],
description: _formData['description'],
price: _formData['price'],
image: _formData['image']),
);
}
// pushReplacementNamed prevents it from going back by pressing BACK buttons
print('[So far so good] before navigation');
Navigator.pushReplacementNamed(context, '/products');
print('[So far so good] after navigation');
}
Widget _buildPageContent(BuildContext context, Product selectedProduct) {
final deviceWidth = MediaQuery.of(context).size.width;
final targetWidth = deviceWidth > 550.0 ? 500.0 : deviceWidth * 0.95;
final targetPadding = deviceWidth - targetWidth;
print('[just before gesture detector]');
return GestureDetector(
onTap: () {
// hide keyboard if container is tapped anywhere other than form
FocusScope.of(context).requestFocus(FocusNode());
},
child: Container(
padding: EdgeInsets.all(10.0),
// Use ListView.builder only when the listCount is unknown and can grow
child: Form(
key: _formKey,
child: ListView(
// so the leftover space is distributed on left and right evenly
padding: EdgeInsets.symmetric(horizontal: targetPadding / 2),
children: [
_buildTitleTextField(selectedProduct),
_buildDescriptionTextField(selectedProduct),
_buildPriceTextField(selectedProduct),
SizedBox(
height: 20.0,
),
_buildSubmitButton(),
],
),
),
),
);
}
}
产品型号
import 'package:scoped_model/scoped_model.dart';
import '../models/product.dart';
class ProductsModel extends Model {
List<Product> _products = [];
int _selectedProductIndex;
bool _showFavorites = false;
// getter
List<Product> get products {
// always return a copy so it doesn't reference to the original list of objects
return List.from(_products);
}
List<Product> get displayedProducts {
if(_showFavorites) {
return _products.where((Product product) => product.isFavorite).toList();
}
return List.from(_products);
}
Product get selectedProduct {
if (_selectedProductIndex == null) {
return null;
}
return _products[_selectedProductIndex];
}
bool get displayFavoritesOnly {
return _showFavorites;
}
int get selectedProductIndex {
return _selectedProductIndex;
}
void addProduct(Product product) {
_products.add(product);
_selectedProductIndex = null;
notifyListeners();
}
void updateProduct(Product product) {
_products[_selectedProductIndex] = product;
_selectedProductIndex = null;
notifyListeners();
}
void deleteProduct() {
print('[index to remove] ' + _selectedProductIndex.toString());
_products.removeAt(_selectedProductIndex);
_selectedProductIndex = null;
notifyListeners();
}
void selectProduct(int index) {
_selectedProductIndex = index;
notifyListeners();
}
void toggleProductFavoriteStatus() {
final bool isCurrentlyFavorite =
selectedProduct.isFavorite;
final bool newFavoriteStatus = !isCurrentlyFavorite;
final Product updatedProduct = Product(
title: selectedProduct.title,
description: selectedProduct.description,
price: selectedProduct.price,
image: selectedProduct.image,
isFavorite: newFavoriteStatus,
);
_products[_selectedProductIndex] = updatedProduct;
_selectedProductIndex = null;
notifyListeners(); // to update all scoped model listeners to rerun their builder methods of scoped model decendents
}
void toggleDisplayMode() {
_showFavorites = !_showFavorites;
print('[Show Favorite]' + _showFavorites.toString());
notifyListeners();
}
}
错误
══╡ EXCEPTION CAUGHT BY WIDGETS LIBRARY ╞═══════════════════════════════════════════════════════════
The following assertion was thrown building TextField(controller:
TextEditingController#c4936(TextEditingValue(text: ┤├, selection: TextSelection(baseOffset: -1,
extentOffset: -1, affinity: TextAffinity.downstream, isDirectional: false), composing:
TextRange(start: -1, end: -1))), enabled: true, decoration: InputDecoration(labelText: "Product
Title", floatingLabelBehavior: FloatingLabelBehavior.auto, alignLabelWithHint: false), dirty,
dependencies: [MediaQuery], state: _TextFieldState#b214d):
No Material widget found.
TextField widgets require a Material widget ancestor.
In material design, most widgets are conceptually "printed" on a sheet of material. In Flutter's
material library, that material is represented by the Material widget. It is the Material widget
that renders ink splashes, for instance. Because of this, many material library widgets require that
there be a Material widget in the tree above them.
To introduce a Material widget, you can either directly include one, or use a widget that contains
Material itself, such as a Card, Dialog, Drawer, or Scaffold.
The specific widget that could not find a Material ancestor was:
TextField
The ancestors of this widget were:
...
TextFormField
RepaintBoundary
IndexedSemantics
NotificationListener<KeepAliveNotification>
KeepAlive
...
The relevant error-causing widget was:
TextFormField file:///D:/MobileDev/tutorial/lib/pages/product_edit.dart:42:12
When the exception was thrown, this was the stack:
#0 debugCheckHasMaterial.<anonymous closure> (package:flutter/src/material/debug.dart:30:7)
#1 debugCheckHasMaterial (package:flutter/src/material/debug.dart:52:4)
#2 _TextFieldState.build (package:flutter/src/material/text_field.dart:1015:12)
#3 StatefulElement.build (package:flutter/src/widgets/framework.dart:4663:28)
#4 ComponentElement.performRebuild (package:flutter/src/widgets/framework.dart:4546:15)
#5 StatefulElement.performRebuild (package:flutter/src/widgets/framework.dart:4719:11)
#6 Element.rebuild (package:flutter/src/widgets/framework.dart:4262:5)
#7 ComponentElement._firstBuild (package:flutter/src/widgets/framework.dart:4525:5)
#8 StatefulElement._firstBuild (package:flutter/src/widgets/framework.dart:4710:11)
#9 ComponentElement.mount (package:flutter/src/widgets/framework.dart:4520:5)
... Normal element mounting (41 frames)
#50 Element.inflateWidget (package:flutter/src/widgets/framework.dart:3490:14)
#51 Element.updateChild (package:flutter/src/widgets/framework.dart:3258:18)
#52 SliverMultiBoxAdaptorElement.updateChild (package:flutter/src/widgets/sliver.dart:1164:36)
#53 SliverMultiBoxAdaptorElement.createChild.<anonymous closure> (package:flutter/src/widgets/sliver.dart:1149:20)
#54 BuildOwner.buildScope (package:flutter/src/widgets/framework.dart:2620:19)
#55 SliverMultiBoxAdaptorElement.createChild (package:flutter/src/widgets/sliver.dart:1142:11)
#56 RenderSliverMultiBoxAdaptor._createOrObtainChild.<anonymous closure> (package:flutter/src/rendering/sliver_multi_box_adaptor.dart:356:23)
#57 RenderObject.invokeLayoutCallback.<anonymous closure> (package:flutter/src/rendering/object.dart:1868:58)
#58 PipelineOwner._enableMutationsToDirtySubtrees (package:flutter/src/rendering/object.dart:920:15)
#59 RenderObject.invokeLayoutCallback (package:flutter/src/rendering/object.dart:1868:13)
#60 RenderSliverMultiBoxAdaptor._createOrObtainChild (package:flutter/src/rendering/sliver_multi_box_adaptor.dart:345:5)
#61 RenderSliverMultiBoxAdaptor.addInitialChild (package:flutter/src/rendering/sliver_multi_box_adaptor.dart:429:5)
#62 RenderSliverList.performLayout (package:flutter/src/rendering/sliver_list.dart:81:12)
#63 RenderObject.layout (package:flutter/src/rendering/object.dart:1769:7)
#64 RenderSliverEdgeInsetsPadding.performLayout (package:flutter/src/rendering/sliver_padding.dart:137:11)
#65 RenderSliverPadding.performLayout (package:flutter/src/rendering/sliver_padding.dart:377:11)
#66 RenderObject.layout (package:flutter/src/rendering/object.dart:1769:7)
#67 RenderViewportBase.layoutChildSequence (package:flutter/src/rendering/viewport.dart:471:13)
#68 RenderViewport._attemptLayout (package:flutter/src/rendering/viewport.dart:1465:12)
#69 RenderViewport.performLayout (package:flutter/src/rendering/viewport.dart:1374:20)
#70 RenderObject.layout (package:flutter/src/rendering/object.dart:1769:7)
#71 RenderProxyBoxMixin.performLayout (package:flutter/src/rendering/proxy_box.dart:115:13)
#72 RenderObject.layout (package:flutter/src/rendering/object.dart:1769:7)
#73 RenderProxyBoxMixin.performLayout (package:flutter/src/rendering/proxy_box.dart:115:13)
#74 RenderObject.layout (package:flutter/src/rendering/object.dart:1769:7)
#75 RenderProxyBoxMixin.performLayout (package:flutter/src/rendering/proxy_box.dart:115:13)
#76 RenderObject.layout (package:flutter/src/rendering/object.dart:1769:7)
#77 RenderProxyBoxMixin.performLayout (package:flutter/src/rendering/proxy_box.dart:115:13)
#78 RenderObject.layout (package:flutter/src/rendering/object.dart:1769:7)
#79 RenderProxyBoxMixin.performLayout (package:flutter/src/rendering/proxy_box.dart:115:13)
#80 RenderObject.layout (package:flutter/src/rendering/object.dart:1769:7)
#81 RenderProxyBoxMixin.performLayout (package:flutter/src/rendering/proxy_box.dart:115:13)
#82 RenderObject.layout (package:flutter/src/rendering/object.dart:1769:7)
#83 RenderProxyBoxMixin.performLayout (package:flutter/src/rendering/proxy_box.dart:115:13)
#84 RenderObject.layout (package:flutter/src/rendering/object.dart:1769:7)
#85 RenderProxyBoxMixin.performLayout (package:flutter/src/rendering/proxy_box.dart:115:13)
#86 RenderObject.layout (package:flutter/src/rendering/object.dart:1769:7)
#87 RenderProxyBoxMixin.performLayout (package:flutter/src/rendering/proxy_box.dart:115:13)
#88 RenderObject.layout (package:flutter/src/rendering/object.dart:1769:7)
#89 RenderPadding.performLayout (package:flutter/src/rendering/shifted_box.dart:209:11)
#90 RenderObject.layout (package:flutter/src/rendering/object.dart:1769:7)
#91 RenderProxyBoxMixin.performLayout (package:flutter/src/rendering/proxy_box.dart:115:13)
#92 RenderObject.layout (package:flutter/src/rendering/object.dart:1769:7)
#93 RenderProxyBoxMixin.performLayout (package:flutter/src/rendering/proxy_box.dart:115:13)
#94 RenderObject.layout (package:flutter/src/rendering/object.dart:1769:7)
#95 RenderProxyBoxMixin.performLayout (package:flutter/src/rendering/proxy_box.dart:115:13)
#96 RenderObject._layoutWithoutResize (package:flutter/src/rendering/object.dart:1632:7)
#97 PipelineOwner.flushLayout (package:flutter/src/rendering/object.dart:889:18)
#98 RendererBinding.drawFrame (package:flutter/src/rendering/binding.dart:404:19)
#99 WidgetsBinding.drawFrame (package:flutter/src/widgets/binding.dart:867:13)
#100 RendererBinding._handlePersistentFrameCallback (package:flutter/src/rendering/binding.dart:286:5)
#101 SchedulerBinding._invokeFrameCallback (package:flutter/src/scheduler/binding.dart:1117:15)
#102 SchedulerBinding.handleDrawFrame (package:flutter/src/scheduler/binding.dart:1056:9)
#103 SchedulerBinding._handleDrawFrame (package:flutter/src/scheduler/binding.dart:972:5)
#107 _invoke (dart:ui/hooks.dart:253:10)
#108 _drawFrame (dart:ui/hooks.dart:211:3)
(elided 3 frames from dart:async)
════════════════════════════════════════════════════════════════════════════════════════════════════
════════ Exception caught by widgets library ═══════════════════════════════════════════════════════
The following assertion was thrown building TextField(controller: TextEditingController#c4936(TextEditingValue(text: ┤├, selection: TextSelection(baseOffset: -1, extentOffset: -1, affinity: TextAffinity.downstream, isDirectional: false), composing: TextRange(start: -1, end: -1))), enabled: true, decoration: InputDecoration(labelText: "Product Title", floatingLabelBehavior: FloatingLabelBehavior.auto, alignLabelWithHint: false), dirty, dependencies: [MediaQuery], state: _TextFieldState#b214d):
No Material widget found.
TextField widgets require a Material widget ancestor.
In material design, most widgets are conceptually "printed" on a sheet of material. In Flutter's material library, that material is represented by the Material widget. It is the Material widget that renders ink splashes, for instance. Because of this, many material library widgets require that there be a Material widget in the tree above them.
To introduce a Material widget, you can either directly include one, or use a widget that contains Material itself, such as a Card, Dialog, Drawer, or Scaffold.
The specific widget that could not find a Material ancestor was: TextField
controller: TextEditingController#c4936(TextEditingValue(text: ┤├, selection: TextSelection(baseOffset: -1, extentOffset: -1, affinity: TextAffinity.downstream, isDirectional: false), composing: TextRange(start: -1, end: -1)))
enabled: true
decoration: InputDecoration(labelText: "Product Title", floatingLabelBehavior: FloatingLabelBehavior.auto, alignLabelWithHint: false)
dirty
dependencies: [MediaQuery]
state: _TextFieldState#b214d
The ancestors of this widget were:
: TextFormField
dependencies: [_LocalizationsScope-[GlobalKey#f9478], _InheritedTheme, _FormScope]
state: _TextFormFieldState#b36ec
: ListView
scrollDirection: vertical
primary: using primary controller
AlwaysScrollableScrollPhysics
padding: EdgeInsets(9.8, 0.0, 9.8, 0.0)
: Form-[LabeledGlobalKey<FormState>#8c77e]
state: FormState#81587
: Container
padding: EdgeInsets.all(10.0)
: GestureDetector
startBehavior: start
: ScopedModelDescendant<ProductsModel>
dependencies: [_InheritedModel<ProductsModel>, MediaQuery]
: ProductEditPage
state: _ProductEditPageState#89794
: MaterialApp
state: _MaterialAppState#08344
: ScopedModel<ProductsModel>
: MyApp
state: _MyAppState#e9c8f
...
The relevant error-causing widget was:
TextFormField file:///D:/MobileDev/tutorial/lib/pages/product_edit.dart:42:12
When the exception was thrown, this was the stack:
#0 debugCheckHasMaterial.<anonymous closure> (package:flutter/src/material/debug.dart:30:7)
#1 debugCheckHasMaterial (package:flutter/src/material/debug.dart:52:4)
#2 _TextFieldState.build (package:flutter/src/material/text_field.dart:1015:12)
#3 StatefulElement.build (package:flutter/src/widgets/framework.dart:4663:28)
#4 ComponentElement.performRebuild (package:flutter/src/widgets/framework.dart:4546:15)
...
════════════════════════════════════════════════════════════════════════════════════════════════════
颤振医生
Doctor summary (to see all details, run flutter doctor -v):
[√] Flutter (Channel stable, 1.20.1, on Microsoft Windows [Version 10.0.18362.1082], locale en-US)
[√] Android toolchain - develop for Android devices (Android SDK version 30.0.1)
[√] Android Studio (version 4.0)
[√] VS Code (version 1.48.2)
[√] Connected device (1 available)
• No issues found!
IMP代码无论如何都不会中断。只是给出错误,然后根据需要导航到路线“/products”。此外,它不会在错误后执行/输出任何 print('.....') 行。
更新我注意到它ScopedModelDescendant<ProductsModel>( builder: (BuildContext context, Widget child, ProductsModel model)
在保存/提交表单后再次进入 ScopedModelDecendent 的 builder() ,但不是其_ProductEditPageState()
上方仅一行的 build() 方法。有人可以解释一下这里发生了什么吗?
解决方案
这就是我解决问题的方法。我包装了GestureDetector
小Material
部件。
child: GestureDetector(
onTap: () {
// hide keyboard if container is tapped anywhere other than form
FocusScope.of(context).requestFocus(FocusNode());
},
child: Container(
padding: EdgeInsets.all(10.0),
// Use ListView.builder only when the listCount is unknown and can grow
child: Form(
key: _formKey,
child: ListView(
// so the leftover space is distributed on left and right evenly
padding: EdgeInsets.symmetric(horizontal: targetPadding / 2),
children: [
_buildTitleTextField(selectedProduct),
_buildDescriptionTextField(selectedProduct),
_buildPriceTextField(selectedProduct),
SizedBox(
height: 20.0,
),
_buildSubmitButton(),
],
),
),
),
),
);
推荐阅读
- android - Android:为什么在尝试分配给变量时这个 Transformation.map 不运行?
- c# - 从 ViewModel WPF 更新 XAML 中的 ComboBox ObservableCollection 绑定
- pandas - 如何从数据框和列中的列表中弹出行
- typescript - 将所有相似的值组合到一个数组中
- bash - 将文件从源复制到目标,但删除目标中的所有文件,但不在源中
- r - 如何使用 R 中另一列的键从数据框的列中获取值?
- sql - PostgreSQL 更新日期范围
- azure - 通过 Azure Blob 存储将数据复制进出雪花
- html - 调整窗口宽度时,导航栏总是从右侧推送
- rust - 如何重用 sqlx::Executor?