I am creating a simple Shop app where I implement logic of adding our own product to list and update the existing product in the list. The update is going totally fine but whenever I tried to add new product sometime the app crashes and some time the result is reflect anything.
Here is my code. All entities are of type String except price (int).
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:shop_app/provider/product.dart';
import 'package:shop_app/provider/product_provider.dart';
class EditProductScreen extends StatefulWidget {
static const routname = '/edit-product-screen';
const EditProductScreen({Key? key}) : super(key: key);
@override
State<EditProductScreen> createState() => _EditProductScreenState();
}
class _EditProductScreenState extends State<EditProductScreen> {
final _priceFocusNode = FocusNode();
final _descriptionFocusNode = FocusNode();
final _imageUrlController = TextEditingController();
final _imageUrlFocusNode = FocusNode();
final _form = GlobalKey<FormState>();
var _editedProduct = Product(
id: '',
title: '',
price: 0,
description: '',
imageUrl: '',
);
var _initValues = {
'title': '',
'description': '',
'price': '',
'imageUrl': '',
};
var _isInit = true;
@override
void initState() {
_imageUrlFocusNode.addListener(_updateImageUrl);
super.initState();
}
@override
void didChangeDependencies() {
if (_isInit) {
final productId = ModalRoute.of(context)!.settings.arguments;
// ignore: unnecessary_null_comparison
if (productId != null) {
_editedProduct = Provider.of<ProductsProvider>(context, listen: false)
.findById(productId.toString());
_initValues = {
'title': _editedProduct.title,
'description': _editedProduct.description,
'price': _editedProduct.price.toString(),
// 'imageUrl': _editedProduct.imageUrl,
'imageUrl': '',
};
_imageUrlController.text = _editedProduct.imageUrl;
}
}
_isInit = false;
super.didChangeDependencies();
}
@override
void dispose() {
_imageUrlFocusNode.removeListener(_updateImageUrl);
_priceFocusNode.dispose();
_descriptionFocusNode.dispose();
_imageUrlController.dispose();
_imageUrlFocusNode.dispose();
super.dispose();
}
void _updateImageUrl() {
if (!_imageUrlFocusNode.hasFocus) {
if ((!_imageUrlController.text.startsWith('http') &&
!_imageUrlController.text.startsWith('https')) ||
(!_imageUrlController.text.endsWith('.png') &&
!_imageUrlController.text.endsWith('.jpg') &&
!_imageUrlController.text.endsWith('.jpeg'))) {
return;
}
setState(() {});
}
}
void _saveForm() {
final isValid = _form.currentState!.validate();
if (!isValid) {
return;
}
_form.currentState!.save();
// ignore: unnecessary_null_comparison
if (_editedProduct.id != null) {
Provider.of<ProductsProvider>(context, listen: false)
.updateProduct(_editedProduct.id, _editedProduct);
} else {
Provider.of<ProductsProvider>(context, listen: true)
.addProduct(_editedProduct);
}
Navigator.of(context).pop();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Edit Product'),
actions: <Widget>[
IconButton(
icon: const Icon(Icons.save),
onPressed: _saveForm,
),
],
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Form(
key: _form,
child: ListView(
children: <Widget>[
TextFormField(
initialValue: _initValues['title'],
decoration: const InputDecoration(labelText: 'Title'),
textInputAction: TextInputAction.next,
onFieldSubmitted: (_) {
FocusScope.of(context).requestFocus(_priceFocusNode);
},
validator: (value) {
if (value!.isEmpty) {
return 'Please provide a value.';
}
return null;
},
onSaved: (value) {
_editedProduct = Product(
title: value.toString(),
price: _editedProduct.price,
description: _editedProduct.description,
imageUrl: _editedProduct.imageUrl,
id: _editedProduct.id,
isFav: _editedProduct.isFav);
},
),
TextFormField(
initialValue: _initValues['price'],
decoration: const 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 price.';
}
if (double.tryParse(value) == null) {
return 'Please enter a valid number.';
}
if (double.parse(value) <= 0) {
return 'Please enter a number greater than zero.';
}
return null;
},
onSaved: (value) {
_editedProduct = Product(
title: _editedProduct.title,
price: int.parse(value.toString()),
description: _editedProduct.description,
imageUrl: _editedProduct.imageUrl,
id: _editedProduct.id,
isFav: _editedProduct.isFav);
},
),
TextFormField(
initialValue: _initValues['description'],
decoration: const InputDecoration(labelText: 'Description'),
maxLines: 3,
keyboardType: TextInputType.multiline,
focusNode: _descriptionFocusNode,
validator: (value) {
if (value!.isEmpty) {
return 'Please enter a description.';
}
if (value.length < 10) {
return 'Should be at least 10 characters long.';
}
return null;
},
onSaved: (value) {
_editedProduct = Product(
title: _editedProduct.title,
price: _editedProduct.price,
description: value.toString(),
imageUrl: _editedProduct.imageUrl,
id: _editedProduct.id,
isFav: _editedProduct.isFav,
);
},
),
Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: <Widget>[
Container(
width: 100,
height: 100,
margin: const EdgeInsets.only(
top: 8,
right: 10,
),
decoration: BoxDecoration(
border: Border.all(
width: 1,
color: Colors.grey,
),
),
child: _imageUrlController.text.isEmpty
? const Text('Enter a URL')
: FittedBox(
child: Image.network(
_imageUrlController.text,
fit: BoxFit.cover,
),
),
),
Expanded(
child: TextFormField(
decoration: const InputDecoration(labelText: 'Image URL'),
keyboardType: TextInputType.url,
textInputAction: TextInputAction.done,
controller: _imageUrlController,
focusNode: _imageUrlFocusNode,
onFieldSubmitted: (_) {
_saveForm();
},
validator: (value) {
if (value!.isEmpty) {
return 'Please enter an image URL.';
}
if (!value.startsWith('http') &&
!value.startsWith('https')) {
return 'Please enter a valid URL.';
}
if (!value.endsWith('.png') &&
!value.endsWith('.jpg') &&
!value.endsWith('.jpeg')) {
return 'Please enter a valid image URL.';
}
return null;
},
onSaved: (value) {
_editedProduct = Product(
title: _editedProduct.title,
price: _editedProduct.price,
description: _editedProduct.description,
imageUrl: value.toString(),
id: _editedProduct.id,
isFav: _editedProduct.isFav,
);
},
),
),
],
),
],
),
),
),
);
}
}
Here is my Product Class
class Product with ChangeNotifier {
final String id;
final String title;
final String description;
final int price;
final String imageUrl;
bool isFav;
Product(
{required this.id,
required this.title,
required this.description,
required this.price,
required this.imageUrl,
this.isFav = false});
void toggleFavStatus() {
isFav = !isFav;
notifyListeners();
}
}
Here is My Product Provider Class
class ProductsProvider with ChangeNotifier {
// ignore: prefer_final_fields
List<Product> _items = [
Product(
id: 'p1',
title: 'Red Shirt',
description: 'A red shirt - it is pretty red!',
price: 29,
imageUrl:
'https://cdn.pixabay.com/photo/2016/10/02/22/17/red-t-shirt-1710578_1280.jpg',
),
Product(
id: 'p2',
title: 'Trousers',
description: 'A nice pair of trousers.',
price: 59,
imageUrl:
'https://upload.wikimedia.org/wikipedia/commons/thumb/e/e8/Trousers,_dress_(AM_1960.022-8).jpg/512px-Trousers,_dress_(AM_1960.022-8).jpg',
),
Product(
id: 'p3',
title: 'Yellow Scarf',
description: 'Warm and cozy - exactly what you need for the winter.',
price: 19,
imageUrl:
'https://live.staticflickr.com/4043/4438260868_cc79b3369d_z.jpg',
),
Product(
id: 'p4',
title: 'A Pan',
description: 'Prepare any meal you want.',
price: 49,
imageUrl:
'https://upload.wikimedia.org/wikipedia/commons/thumb/1/14/Cast-Iron-Pan.jpg/1024px-Cast-Iron-Pan.jpg',
),
];
List<Product> get items {
// if (_showFavoritesOnly) {
// return _items.where((prodItem) => prodItem.isFavorite).toList();
// }
return [..._items];
}
List<Product> get favoriteItems {
return _items.where((prodItem) => prodItem.isFav).toList();
}
Product findById(String id) {
return _items.firstWhere((prod) => prod.id == id);
}
void showFavoritesOnly() {
notifyListeners();
}
void showAll() {
notifyListeners();
}
void addProduct(Product product) {
final newProduct = Product(
title: product.title,
description: product.description,
price: product.price,
imageUrl: product.imageUrl,
id: DateTime.now().toString(),
);
_items.add(newProduct);
// _items.insert(0, newProduct); // at the start of the list
notifyListeners();
}
void updateProduct(String id, Product newProduct) {
final prodIndex = _items.indexWhere((prod) => prod.id == id);
if (prodIndex >= 0) {
_items[prodIndex] = newProduct;
notifyListeners();
} else {}
}
void deleteProduct(String id) {
_items.removeWhere((prod) => prod.id == id);
notifyListeners();
}
}
CodePudding user response:
The current add event trigger the update product method. based on your condition, it is
var _editedProduct = Product(
id: '',
But on saved you are checking null,
if (_editedProduct.id != null) {
If you like to check null, you need to make id nullable like
String? id
.
class Product with ChangeNotifier {
final String? id;
final String title;
final String description;
final int price;
final String imageUrl;
bool isFav;
Product(
{this.id,
required this.title,
required this.description,
required this.price,
required this.imageUrl,
this.isFav = false});
And use listen: false
void _saveForm() {
final isValid = _form.currentState!.validate();
if (!isValid) {
return;
}
_form.currentState!.save();
// ignore: unnecessary_null_comparison
if (_editedProduct.id != null) {
Provider.of<ProductsProvider>(context, listen: false)
.updateProduct(_editedProduct.id!, _editedProduct);
} else {
Provider.of<ProductsProvider>(
context,
listen: false,
).addProduct(_editedProduct);
}
Navigator.of(context).pop();
}
Full test snippet
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
void main() {
runApp(
MultiProvider(
providers: [
ChangeNotifierProvider(
create: (context) => ProductsProvider(),
),
],
child: App(),
),
);
}
class App extends StatelessWidget {
const App({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
theme: Theme.of(context).copyWith(
useMaterial3: true,
),
home: HomePage(),
);
}
}
class HomePage extends StatelessWidget {
const HomePage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
floatingActionButton: FloatingActionButton(
onPressed: () {
final route = MaterialPageRoute(
builder: (context) {
return EditProductScreen();
},
);
Navigator.of(context).push(route);
},
),
body: Consumer<ProductsProvider>(
builder: (context, value, child) => ListView.builder(
itemCount: value.items.length,
itemBuilder: (context, index) {
return ListTile(
title: Text("${value.items[index].id}"),
);
},
),
),
);
}
}
class EditProductScreen extends StatefulWidget {
static const routname = '/edit-product-screen';
const EditProductScreen({Key? key}) : super(key: key);
@override
State<EditProductScreen> createState() => _EditProductScreenState();
}
class _EditProductScreenState extends State<EditProductScreen> {
final _priceFocusNode = FocusNode();
final _descriptionFocusNode = FocusNode();
final _imageUrlController = TextEditingController();
final _imageUrlFocusNode = FocusNode();
final _form = GlobalKey<FormState>();
Product _editedProduct = Product(
title: '',
price: 0,
description: '',
imageUrl: '',
);
var _initValues = {
'title': '',
'description': '',
'price': '',
'imageUrl': '',
};
var _isInit = true;
@override
void initState() {
_imageUrlFocusNode.addListener(_updateImageUrl);
super.initState();
}
@override
void didChangeDependencies() {
if (_isInit) {
final productId = ModalRoute.of(context)!.settings.arguments;
// ignore: unnecessary_null_comparison
if (productId != null) {
_editedProduct = Provider.of<ProductsProvider>(context, listen: false)
.findById(productId.toString());
_initValues = {
'title': _editedProduct.title,
'description': _editedProduct.description,
'price': _editedProduct.price.toString(),
// 'imageUrl': _editedProduct.imageUrl,
'imageUrl': '',
};
_imageUrlController.text = _editedProduct.imageUrl;
}
}
_isInit = false;
super.didChangeDependencies();
}
@override
void dispose() {
_imageUrlFocusNode.removeListener(_updateImageUrl);
_priceFocusNode.dispose();
_descriptionFocusNode.dispose();
_imageUrlController.dispose();
_imageUrlFocusNode.dispose();
super.dispose();
}
void _updateImageUrl() {
if (!_imageUrlFocusNode.hasFocus) {
if ((!_imageUrlController.text.startsWith('http') &&
!_imageUrlController.text.startsWith('https')) ||
(!_imageUrlController.text.endsWith('.png') &&
!_imageUrlController.text.endsWith('.jpg') &&
!_imageUrlController.text.endsWith('.jpeg'))) {
return;
}
setState(() {});
}
}
void _saveForm() {
final isValid = _form.currentState!.validate();
if (!isValid) {
return;
}
_form.currentState!.save();
// ignore: unnecessary_null_comparison
if (_editedProduct.id != null) {
Provider.of<ProductsProvider>(context, listen: false)
.updateProduct(_editedProduct.id!, _editedProduct);
} else {
Provider.of<ProductsProvider>(
context,
listen: false,
).addProduct(_editedProduct);
}
Navigator.of(context).pop();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Edit Product'),
actions: <Widget>[
IconButton(
icon: const Icon(Icons.save),
onPressed: _saveForm,
),
],
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Form(
key: _form,
child: ListView(
children: <Widget>[
TextFormField(
initialValue: _initValues['title'],
decoration: const InputDecoration(labelText: 'Title'),
textInputAction: TextInputAction.next,
onFieldSubmitted: (_) {
FocusScope.of(context).requestFocus(_priceFocusNode);
},
validator: (value) {
if (value!.isEmpty) {
return 'Please provide a value.';
}
return null;
},
onSaved: (value) {
_editedProduct = Product(
title: value.toString(),
price: _editedProduct.price,
description: _editedProduct.description,
imageUrl: _editedProduct.imageUrl,
isFav: _editedProduct.isFav);
},
),
TextFormField(
initialValue: _initValues['price'],
decoration: const 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 price.';
}
if (double.tryParse(value) == null) {
return 'Please enter a valid number.';
}
if (double.parse(value) <= 0) {
return 'Please enter a number greater than zero.';
}
return null;
},
onSaved: (value) {
_editedProduct = Product(
title: _editedProduct.title,
price: int.tryParse(value.toString()) ?? 0,
description: _editedProduct.description,
imageUrl: _editedProduct.imageUrl,
isFav: _editedProduct.isFav);
},
),
TextFormField(
initialValue: _initValues['description'],
decoration: const InputDecoration(labelText: 'Description'),
maxLines: 3,
keyboardType: TextInputType.multiline,
focusNode: _descriptionFocusNode,
validator: (value) {
if (value!.isEmpty) {
return 'Please enter a description.';
}
if (value.length < 10) {
return 'Should be at least 10 characters long.';
}
return null;
},
onSaved: (value) {
_editedProduct = Product(
title: _editedProduct.title,
price: _editedProduct.price,
description: value.toString(),
imageUrl: _editedProduct.imageUrl,
isFav: _editedProduct.isFav,
);
},
),
Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: <Widget>[
Container(
width: 100,
height: 100,
margin: const EdgeInsets.only(
top: 8,
right: 10,
),
decoration: BoxDecoration(
border: Border.all(
width: 1,
color: Colors.grey,
),
),
child: _imageUrlController.text.isEmpty
? const Text('Enter a URL')
: FittedBox(
child: Image.network(
_imageUrlController.text,
fit: BoxFit.cover,
),
),
),
Expanded(
child: TextFormField(
decoration: const InputDecoration(labelText: 'Image URL'),
keyboardType: TextInputType.url,
textInputAction: TextInputAction.done,
controller: _imageUrlController,
focusNode: _imageUrlFocusNode,
onFieldSubmitted: (_) {
_saveForm();
},
validator: (value) {
///* enable theses
if (value!.isEmpty) {
return 'Please enter an image URL.';
}
if (!value.startsWith('http') &&
!value.startsWith('https')) {
return 'Please enter a valid URL.';
}
if (!value.endsWith('.png') &&
!value.endsWith('.jpg') &&
!value.endsWith('.jpeg')) {
return 'Please enter a valid image URL.';
}
return null;
},
onSaved: (value) {
_editedProduct = Product(
title: _editedProduct.title,
price: _editedProduct.price,
description: _editedProduct.description,
imageUrl: value.toString(),
id: _editedProduct.id,
isFav: _editedProduct.isFav,
);
},
),
),
],
),
],
),
),
),
);
}
}
class Product with ChangeNotifier {
final String? id;
final String title;
final String description;
final int price;
final String imageUrl;
bool isFav;
Product(
{this.id,
required this.title,
required this.description,
required this.price,
required this.imageUrl,
this.isFav = false});
void toggleFavStatus() {
isFav = !isFav;
notifyListeners();
}
}
class ProductsProvider with ChangeNotifier {
// ignore: prefer_final_fields
List<Product> _items = [
Product(
id: 'p1',
title: 'Red Shirt',
description: 'A red shirt - it is pretty red!',
price: 29,
imageUrl:
'https://cdn.pixabay.com/photo/2016/10/02/22/17/red-t-shirt-1710578_1280.jpg',
),
Product(
id: 'p2',
title: 'Trousers',
description: 'A nice pair of trousers.',
price: 59,
imageUrl:
'https://upload.wikimedia.org/wikipedia/commons/thumb/e/e8/Trousers,_dress_(AM_1960.022-8).jpg/512px-Trousers,_dress_(AM_1960.022-8).jpg',
),
Product(
id: 'p3',
title: 'Yellow Scarf',
description: 'Warm and cozy - exactly what you need for the winter.',
price: 19,
imageUrl:
'https://live.staticflickr.com/4043/4438260868_cc79b3369d_z.jpg',
),
Product(
id: 'p4',
title: 'A Pan',
description: 'Prepare any meal you want.',
price: 49,
imageUrl:
'https://upload.wikimedia.org/wikipedia/commons/thumb/1/14/Cast-Iron-Pan.jpg/1024px-Cast-Iron-Pan.jpg',
),
];
List<Product> get items {
// if (_showFavoritesOnly) {
// return _items.where((prodItem) => prodItem.isFavorite).toList();
// }
return [..._items];
}
List<Product> get favoriteItems {
return _items.where((prodItem) => prodItem.isFav).toList();
}
Product findById(String id) {
return _items.firstWhere((prod) => prod.id == id);
}
void showFavoritesOnly() {
notifyListeners();
}
void showAll() {
notifyListeners();
}
void addProduct(Product product) {
final newProduct = Product(
title: product.title,
description: product.description,
price: product.price,
imageUrl: product.imageUrl,
id: DateTime.now().toString(),
);
_items.add(newProduct);
// _items.insert(0, newProduct); // at the start of the list
notifyListeners();
}
void updateProduct(String id, Product newProduct) {
final prodIndex = _items.indexWhere((prod) => prod.id == id);
if (prodIndex >= 0) {
_items[prodIndex] = newProduct;
notifyListeners();
} else {}
}
void deleteProduct(String id) {
_items.removeWhere((prod) => prod.id == id);
notifyListeners();
}
}