首页 > 解决方案 > 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() 方法。有人可以解释一下这里发生了什么吗?

标签: flutternavigationwidget

解决方案


这就是我解决问题的方法。我包装了GestureDetectorMaterial部件。

      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(),
              ],
            ),
          ),
        ),
      ),
    );

推荐阅读