Flutter type 'Null' is not a subtype of type 'String' 报错排查
问题现象
- 编辑商品场景(路由传入商品ID,
receivedProductID != null)页面可正常加载,无异常 - 新增商品场景(路由未传商品ID,
receivedProductID == null)页面打开即抛出报错 - 从该页面执行返回操作时也会触发异常
- 对应页面实现代码:
import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../models/product.dart'; import '../provider/products.dart'; class EditProductScreen extends StatefulWidget { static const routeName = '/edite-product-screen'; @override State<EditProductScreen> createState() => _EditProductScreenState(); } class _EditProductScreenState extends State<EditProductScreen> { final priceFocusNode = FocusNode(); final descriptionFocusNode = FocusNode(); final imageURLController = TextEditingController(); final imageURLFocusNode = FocusNode(); final keyForForm = GlobalKey<FormState>(); var emptyProduct = Product(id: "", title: "", description: "", price: 0, imageUrl: ""); var isInit = true; var initValues = { 'title': "", 'description': "", 'price': "", 'imageURL': "", }; @override void initState() { super.initState(); imageURLFocusNode.addListener(updateImageUrl); } @override void didChangeDependencies() { super.didChangeDependencies(); if (isInit) { final receivedProductID = ModalRoute.of(context)?.settings.arguments as String; if (receivedProductID != null) { emptyProduct = Provider.of<Products>(context, listen: false) .selectedProduct(receivedProductID); initValues = { 'title': emptyProduct.title.toString(), 'description': emptyProduct.description.toString(), 'price': emptyProduct.price.toString(), }; imageURLController.text = emptyProduct.imageUrl as String; } } isInit = false; } void updateImageUrl() { if (!imageURLFocusNode.hasFocus) { if ((!imageURLController.text.startsWith("http") && !imageURLController.text.startsWith("https")) || (!imageURLController.text.endsWith("jpg") && !imageURLController.text.endsWith("png") && !imageURLController.text.endsWith("jpeg")) || (imageURLController.text.isEmpty)) { return; } setState(() {}); } } void saveTheProductBasedOnInformationOfTheForm() { final validStatus = keyForForm.currentState?.validate(); if (!validStatus!) { return; } keyForForm.currentState?.save(); if (emptyProduct.id != null) { Provider.of<Products>(context, listen: false) .updateProduct(emptyProduct.id as String, emptyProduct); } else { Provider.of<Products>(context, listen: false).addProduct(emptyProduct); } Navigator.of(context).pop(); } @override void dispose() { imageURLFocusNode.removeListener(updateImageUrl); priceFocusNode.dispose(); descriptionFocusNode.dispose(); imageURLController.dispose(); imageURLFocusNode.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text("Products"), actions: [ IconButton( onPressed: saveTheProductBasedOnInformationOfTheForm, icon: Icon(Icons.save)) ], ), body: Padding( padding: const EdgeInsets.all(16.0), child: Form( key: keyForForm, child: ListView( children: [ TextFormField( initialValue: initValues['title'], validator: (value) { if (value!.isEmpty) { return "Please enter a valid value"; } else { return null; } }, decoration: InputDecoration(labelText: "Title"), textInputAction: TextInputAction.next, onFieldSubmitted: (_) => { FocusScope.of(context).requestFocus(priceFocusNode), }, onSaved: (value) { emptyProduct = Product( id: emptyProduct.id, title: value, description: emptyProduct.description, price: emptyProduct.price, imageUrl: emptyProduct.imageUrl, isFavorite: emptyProduct.isFavorite); }, ), TextFormField( initialValue: initValues['price'], onSaved: (value) { emptyProduct = Product( id: emptyProduct.id, title: emptyProduct.title, description: emptyProduct.description, price: double.parse(value as String), imageUrl: emptyProduct.imageUrl, isFavorite: emptyProduct.isFavorite); }, decoration: InputDecoration(labelText: "Price"), textInputAction: TextInputAction.next, keyboardType: TextInputType.number, focusNode: priceFocusNode, onFieldSubmitted: (_) => FocusScope.of(context).requestFocus(descriptionFocusNode), validator: (value) { if (value!.isEmpty) { return "Please enter a valid price"; } if (double.tryParse(value) == null) { return "Please enter a valid number"; } if (double.parse(value) <= 0) { return "Please enter a value greater than 0"; } else { return null; } }, ), TextFormField( initialValue: initValues['description'], onSaved: (value) { emptyProduct = Product( id: emptyProduct.id, title: emptyProduct.title, description: value, price: emptyProduct.price, imageUrl: emptyProduct.imageUrl, isFavorite: emptyProduct.isFavorite); }, decoration: InputDecoration(labelText: "Description"), keyboardType: TextInputType.multiline, maxLines: 3, validator: (value) { if (value!.isEmpty) { return "Please enter a valid description"; } if (value.length < 10) { return "Please enter more description"; } else { return null; } }, ), Row(crossAxisAlignment: CrossAxisAlignment.end, children: [ Container( width: 100, height: 100, margin: EdgeInsets.only(top: 10, right: 10), decoration: BoxDecoration( border: Border.all(width: 1, color: Colors.grey)), child: imageURLController.text.isEmpty ? Text("Please enter URL") : Image.network(imageURLController.text), ), Expanded( child: TextFormField( initialValue: initValues['imageURL'], validator: (value) { if (value!.isEmpty) { return "Please enter valid Image URL"; } if ((!value.startsWith("http") && !value.startsWith("https")) || (!value.endsWith("jpg") && !value.endsWith("png") && !value.endsWith("jpeg"))) { return "Please enter a valid URL!"; } }, onSaved: (value) { emptyProduct = Product( id: emptyProduct.id, title: emptyProduct.title, description: emptyProduct.description, price: emptyProduct.price, imageUrl: value, isFavorite: emptyProduct.isFavorite); }, decoration: InputDecoration(labelText: "Image URL"), keyboardType: TextInputType.url, textInputAction: TextInputAction.done, controller: imageURLController, maxLines: 3, focusNode: imageURLFocusNode, onFieldSubmitted: (_) { saveTheProductBasedOnInformationOfTheForm(); }, ), ), ]) ], )), ), ); } }
报错根因
- 路由参数强制类型转换错误:
didChangeDependencies方法中直接使用as String将路由参数强转为非空字符串类型,新增商品场景下未传路由参数时,ModalRoute.of(context)?.settings.arguments返回值为null,将null强转为非空String会直接触发类型转换异常,这是无商品ID时页面打开报错的核心原因。 - 组件属性冲突:图片URL对应的
TextFormField同时配置了initialValue属性和imageURLController控制器,Flutter的文本输入框不允许同时设置这两个属性,二者只能选其一,该冲突会导致组件初始化、销毁阶段抛出异常,对应返回页面时的报错。 - 业务判断逻辑错误:初始化
emptyProduct时给id字段赋值为空字符串"",空字符串不等于null,原代码中if (emptyProduct.id != null)的判断条件永远成立,会导致新增商品场景错误执行商品更新逻辑,而非商品新增逻辑。 - 表单校验逻辑缺失:图片URL字段的
validator方法仅在校验不通过时返回错误提示,校验通过时没有显式返回null,会导致表单校验结果出现空值,和后续的非空判断if (!validStatus!)冲突触发空异常。
修复步骤
- 修正路由参数的类型声明,使用可空类型接收路由参数,避免强转错误:
// 替换原代码中对应行 final receivedProductID = ModalRoute.of(context)?.settings.arguments as String?; - 删除图片URL输入框上的
initialValue: initValues['imageURL'],配置,该字段初始值完全通过imageURLController.text控制即可,编辑场景已有对应赋值逻辑,新增场景默认空字符串符合需求。 - 修正新增/编辑商品的判断逻辑,通过判断id是否为空字符串区分场景:
// 替换原代码中对应行 if (emptyProduct.id.isNotEmpty) { - 补全图片URL校验器的返回逻辑,校验通过时返回
null:validator: (value) { if (value!.isEmpty) { return "Please enter valid Image URL"; } if ((!value.startsWith("http") && !value.startsWith("https")) || (!value.endsWith("jpg") && !value.endsWith("png") && !value.endsWith("jpeg"))) { return "Please enter a valid URL!"; } // 新增返回逻辑 return null; }, - (可选优化)编辑场景给
initValues赋值时,补充imageURL字段的默认值,避免Map缺key导致的取值异常。
内容的提问来源于stack exchange,提问作者Ali Bajwa
相关产品推荐
相关产品推荐

