So basically there is this product edit page which behaves differently uppon called.
If the product is being created for first time, then it is shown in a tab view controller.
if the product is being updated, its body is returned in scaffold.
here are some screenshots
when I submit through create product, i encounter no error.
But when I submit through update product, though the logic works, i get a short red screen with
like this
error Another exception was thrown: No Material widget found.
Here is the code for the screen
`
import 'package:flutter/material.dart';
import 'package:academy_app/models/products.dart';
import 'package:scoped_model/scoped_model.dart';
import 'package:academy_app/scoped-model/Products.dart';
class ProductEdit extends StatefulWidget {
ProductEdit();
#override
State<StatefulWidget> createState() {
// TODO: implement createState
return ProductEditState();
}
}
class ProductEditState extends State<ProductEdit> {
String title;
String description;
double price;
Product product;
final focusnode2 = FocusNode();
final focusnode3 = FocusNode();
Map<String, dynamic> formData = {
"name": null,
"desc": null,
"price": null,
"image": "asset/foood.jpg"
};
GlobalKey<FormState> formkey = GlobalKey<FormState>();
Widget buildTitle(productItem) {
return TextFormField(
initialValue: productItem != null ? productItem.title : "",
validator: (String value) {
if (value.isEmpty || value.length < 3) {
return 'title cannot be empty';
}
},
textInputAction: TextInputAction.next,
decoration: InputDecoration(labelText: "Title"),
onFieldSubmitted: (String value) {
FocusScope.of(context).requestFocus(focusnode2);
},
onSaved: (String valuee) {
setState(() {
formData["name"] = valuee;
});
},
);
}
Widget buildDesc(productItem) {
return TextFormField(
initialValue: productItem != null ? productItem.description : "",
validator: (String value) {
if (value.isEmpty || value.length < 3) {
return 'Cant have that short description';
}
},
textInputAction: TextInputAction.next,
onFieldSubmitted: (value) {
FocusScope.of(context).requestFocus(focusnode3);
},
focusNode: focusnode2,
maxLines: 3,
decoration: InputDecoration(labelText: "Description"),
onSaved: (String valuee) {
setState(() {
formData["desc"] = valuee;
});
},
);
}
Widget buildPrice(productItem) {
return TextFormField(
initialValue: productItem != null ? productItem.price.toString() : "",
textInputAction: TextInputAction.done,
focusNode: focusnode3,
decoration: InputDecoration(labelText: " How much"),
keyboardType: TextInputType.number,
onFieldSubmitted: (value) {
focusnode3.unfocus();
},
validator: (value) {
if (!RegExp(r'^(?:[1-9]\d*|0)?(?:\.\d+)?$').hasMatch(value)) {
return ' Enter numbers only';
}
},
onSaved: (String valuee) {
setState(() {
formData["price"] = double.parse(valuee);
});
},
);
}
void submitForm(Function addProduct, Function updateProduct, int index) {
if (!formkey.currentState.validate()) {
return;
}
formkey.currentState.save();
setState(() {
if (index == null) {
addProduct(Product(
price: formData["price"],
title: formData["name"],
description: formData["desc"],
image: "asset/foood.jpg"));
} else {
updateProduct(
Product(
price: formData["price"],
title: formData["name"],
description: formData["desc"],
image: "asset/foood.jpg"),
);
}
Navigator.pushReplacementNamed(context, '/');
});
}
Widget buildSubmitButton() {
return ScopedModelDescendant<ProductsModel>(
builder: (BuildContext context, Widget, ProductsModel) {
return RaisedButton(
child: Text("Save"),
onPressed: () => submitForm(ProductsModel.addProduct,
ProductsModel.updateProduct, ProductsModel.selected_index));
},
);
}
Widget _buildPageContent(BuildContext context, Product product) {
final double deviceWidth = MediaQuery.of(context).size.width;
final double targetWidth = deviceWidth > 550.0 ? 500.0 : deviceWidth * 0.95;
final double targetPadding = deviceWidth - targetWidth;
return GestureDetector(
onTap: () {
FocusScope.of(context).requestFocus(FocusNode());
},
child: Container(
margin: EdgeInsets.all(10.0),
child: Form(
key: formkey,
child: ListView(
padding: EdgeInsets.symmetric(horizontal: targetPadding / 2),
children: <Widget>[
buildTitle(product),
buildDesc(product),
buildPrice(product),
SizedBox(
height: 10.0,
),
buildSubmitButton()
],
),
),
),
);
}
#override
Widget build(BuildContext context) {
return ScopedModelDescendant<ProductsModel>(
builder: (context, Widget child, ProductsModel) {
product = ProductsModel.getproduct();
return ProductsModel.selected_index == null
? _buildPageContent(context, product)
: Scaffold(
appBar: AppBar(
title: Text("Update Item"),
),
body:_buildPageContent(context, product) ,
);
},
);
}
}
`
why am i getting that red screen error? i confused about passing the contexts. why arent the textfiled accessing the material parent through in scaffold?
As the error suggests that no material widget found, you need to wrap the Container of the _buildPateContent function into Material Widget. Here is the change that you can do:
Widget _buildPageContent(BuildContext context, Product product) {
final double deviceWidth = MediaQuery.of(context).size.width;
final double targetWidth = deviceWidth > 550.0 ? 500.0 : deviceWidth * 0.95;
final double targetPadding = deviceWidth - targetWidth;
return GestureDetector(
onTap: () {
FocusScope.of(context).requestFocus(FocusNode());
},
child: Material(
child: Container(
margin: EdgeInsets.all(10.0),
child: Form(
key: formkey,
child: ListView(
padding: EdgeInsets.symmetric(horizontal: targetPadding / 2),
children: <Widget>[
buildTitle(product),
buildDesc(product),
buildPrice(product),
SizedBox(
height: 10.0,
),
buildSubmitButton()
],
),
),
),
));
}
try wrapping you app in materialApp or wrap TextField in material Widget.
In my case I forgot to wrap my widget with Scaffold widget. A lot of widgets need to be wrapped with it to work properly. So change this
Widget build(BuildContext context) {
return YourScreenContent();
}
to this
Widget build(BuildContext context) {
return Scaffold(
body: YourScreenContent(),
);
}
I had same issue.
I got the error when I had code like this.
Widget _getLCSBar(int index) {
return Container(
decoration: BoxDecoration(
border: Border.all(color: Colors.black, width: 0.1),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
_getLikeButton(index),
_getCommentButton(index),
_getShareButton(index),
],
));
}
What I have done is wrapped it with Material
Widget _getLCSBar(int index) {
return Material(
child: Container(
decoration: BoxDecoration(
border: Border.all(color: Colors.black, width: 0.1),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
_getLikeButton(index),
_getCommentButton(index),
_getShareButton(index),
],
)));
}
Problem Solved!
Just wrap the Gesture Detector inside Material:
return Material(
child: GestureDetector(
...
),
);
Related
I am saving a list of favorites in my game app, I can add or remove games from the list, but on the removeFavorite method, when I use setState in "games.remove(index)" the listview.separeted doesn't update. If close and open favorite screen, the list is updated but while I am at the favorite screen it doesn't update.
class _FavoriteScreenState extends State<FavoriteScreen> {
List<dynamic> favoriteList = [];
List<Game> games = [];
#override
void initState() {
loadFavorites();
super.initState();
}
#override
Widget build(BuildContext context) {
Widget _buildListView(Game game, int index){
return InkWell(
child: Container(
height: 80,
child: Row(
children: <Widget>[
Expanded(
flex: 1,
child: Container(
margin: EdgeInsets.only(left: 5),
child: Image.network(game.cover),
),
),
Expanded(
flex: 2,
child: Container(
child: Row(
children: <Widget>[
SizedBox(width: 15,),
Text(
game.title,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.w300,
fontSize: 14
),
),
],
),
),
)
],
),
),
onLongPress: (){
_showDialog(index);
},
);
}
return Scaffold(
backgroundColor: Theme.of(context).backgroundColor,
appBar: AppBar(
title: Text("Favorites"),
),
body: Container(
margin: EdgeInsets.fromLTRB(16, 16, 16, screenHeight > 720 ? 90 : 62),
child: ListView.separated(
separatorBuilder: (BuildContext context, int index) => Divider(color: Colors.black,),
itemCount: games.length,
itemBuilder: (context, index){
return _buildListView(games[index], index);
},
)
),
);
}
Future<File> getFile() async{
final directory = await getApplicationDocumentsDirectory();
return File("${directory.path}/favorites.json");
}
Future<String> readFavorite() async{
try{
// Le e retorna o arquivo como String
final file = await getFile();
return file.readAsString();
} catch (e){
return null;
}
}
void loadFavorites() {
readFavorite().then((data){
// Transforma o arquivo JSON numa List
favoriteList = json.decode(data);
if(favoriteList.length > 0 && favoriteList != null){
favoriteList.forEach((map){
Game game = Game(map["cover"], map["title"], map["description"], map["url"]);
setState(() {
games.add(game);
});
});
} else {
}
print(games.length);
});
}
Future<File> saveFile() async{
String data = json.encode(favoriteList);
final file = await getFile();
return file.writeAsString(data);
}
void _showDialog(int index){
showDialog(
context: context,
builder: (BuildContext context){
return AlertDialog(
content: Text("?",
style: TextStyle(fontSize: 18) ,),
actions: <Widget>[
FlatButton(
child: Text("YES"),
onPressed: (){
Navigator.of(context).pop();
removeFavorite(index);
}
),
FlatButton(
child: Text("NO"),
onPressed: (){
Navigator.of(context).pop();
}
),
],
);
}
);
}
void removeFavorite(int index){
favoriteList.forEach((m) {
Map<String, dynamic> map = m;
if (map.containsValue(games[index].title)) {
favoriteList.remove(m);
saveFile();
setState(() {
games.remove(index);
});
}
});
}
}
My bad, I replaced the remove with removeAt and it worked.
setState(() {
games.removeAt(index);
});
I' m new in Flutter. I made an app that has a catalogue with categories, subcategories and items (as you can see in photo). My widget tree is:
Catalogue(cart):{[Category-List, Subcategory-List, Item-List:{ItemRow(ListTile)}]}.
I'm facing the following problem: I have items in my cart and their quantity is shown on the catalogue (item rows). When I delete an item from the cart, or clear all items, I can't set the controller of the text field of the item Row to zero cause that widget (current item row) has been mounted. I use Scoped model to add, delete or update items in the cart. So, my problem is just visual. When I click on another category and then go to the previous, the controller has been set to zero correctly (cause the item rows recreated again with initstate()).
Is there any solution to my problem? Thanks!
App Image:
Catalogue.dart Code:
import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter_slidable/flutter_slidable.dart';
import '../widgets/categories/categories_manager.dart';
import '../widgets/subcats/subcat_manager.dart';
import '../widgets/items/items_list.dart';
import '../scoped-models/main.dart';
import '../models/item.dart';
class CataloguePage extends StatefulWidget {
final String _langSelected;
CataloguePage(this._langSelected, this.model);
final MainModel model;
#override
State<StatefulWidget> createState() {
return _CataloguePageState();
}
}
class _CataloguePageState extends State<CataloguePage> {
Widget currentPage;
SubcatManager subcatPage;
bool _loadingProgress;
List<Item> _listCart;
final SlidableController slidableController = SlidableController();
#override
void initState() {
_listCart = widget.model.itemsInCart;
_loadingProgress = true;
widget.model
.fetchCategories(widget.model.serverAddress, widget.model.serverPort)
.then((bool success) {
if (success) {
setState(() {
widget.model
.fetchSubcats(widget.model.serverAddress, widget.model.serverPort,
widget.model.categories[0].catid)
.then((bool success2) {
if (success2) {
setState(() {
widget.model
.fetchItems(
widget.model.serverAddress,
widget.model.serverPort,
widget.model.categories[0].catid,
widget.model.subcats[0].subcatid)
.then((bool success3) {
if (success3) {
_loadingProgress = false;
}
});
});
}
});
});
} else {
showDialog(
barrierDismissible: true,
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: Text('An error has occured.'),
content: Text('Connection with Server failed!'),
actions: <Widget>[
FlatButton(
onPressed: () {
Navigator.popUntil(
context, (_) => !Navigator.canPop(context));
Navigator.pushReplacementNamed(context, '/');
},
child: Text('OK'),
)
],
);
});
}
});
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).primaryColor,
title: Text('Catalogue'),
actions: <Widget>[
Stack(
children: <Widget>[
IconButton(
icon: Icon(
Icons.shopping_cart,
size: 30.0,
),
onPressed: () {
showModalBottomSheet(
context: context,
builder: (BuildContext contex) {
return _buildCartList(_listCart);
},
);
},
),
widget.model.itemsInCart.length > 0
? CircleAvatar(
radius: 10.0,
child: Text(widget.model.itemsInCart.length.toString()),
)
: Container()
],
)
],
),
body: _buildBody(),
);
}
Widget _buildBody() {
if (_loadingProgress) {
return Container(
color: Theme.of(context).backgroundColor,
child: Center(
child: Theme.of(context).platform == TargetPlatform.iOS
? CupertinoActivityIndicator(
radius: 20.0,
)
: CircularProgressIndicator(
strokeWidth: 3.0,
),
),
);
} else {
return Container(
padding: EdgeInsets.all(20),
color: Theme.of(context).backgroundColor,
child: Row(
children: <Widget>[
Flexible(
flex: 3,
child: Column(
children: [
Flexible(
child: CategoriesManager(widget.model),
),
],
),
),
widget.model.subcats[0].subcatid == 0
? Container()
: VerticalDivider(
color: widget.model.themeBrightness == 1
? Colors.white
: Colors.black,
),
widget.model.subcats[0].subcatid == 0
? Container()
: Flexible(
flex: 3,
child: Column(
children: [
Flexible(
child: SubcatManager(widget.model),
),
],
),
),
VerticalDivider(
color: widget.model.themeBrightness == 1
? Colors.white
: Colors.black,
),
Flexible(
flex: 4,
child: Column(
children: [
Text('Items'),
SizedBox(
height: 20.0,
),
Flexible(
child: ItemList(widget.model),
),
],
),
),
],
),
);
}
}
Widget _buildCartList(List<Item> listCart) {
Widget itemCartCards;
if (listCart.length > 0) {
itemCartCards = Padding(
padding: EdgeInsets.all(20.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Text('Selected Items'),
Text('Total Quantity: ' +
widget.model.cartTotalItems.toString()),
],
),
SizedBox(
height: 10.0,
),
Expanded(
child: ListView.separated(
separatorBuilder: (contex, index) => Divider(
color: widget.model.themeBrightness == 1
? Colors.white
: Colors.black,
),
itemBuilder: (BuildContext context, index) {
return Slidable(
key: Key(listCart[index].itemid),
controller: slidableController,
delegate: SlidableDrawerDelegate(),
actionExtentRatio: 0.25,
secondaryActions: <Widget>[
IconSlideAction(
icon: Icons.delete,
caption: 'Delete',
color: Colors.red,
onTap: () {
widget.model
.deleteItemFromCart(listCart[index].itemid);
},
)
],
child: ListTile(
title: Text(
listCart[index].itemperi,
),
trailing: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.end,
children: <Widget>[
widget.model.showListItemsPrices
? Text(listCart[index].itemCount.toString() +
' x ' +
listCart[index].itemprice.toString() +
' €')
: Text(listCart[index].itemCount.toString()),
],
),
),
);
},
itemCount: listCart.length,
),
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
RaisedButton(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10)),
child: Text('Clear Order'),
color: Colors.red,
onPressed: () {
return showDialog<void>(
context: context,
barrierDismissible: false,
builder: (BuildContext context) {
return AlertDialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10.0),
),
title: Text('Warning!'),
content:
Text('Are you sure you want to empty your cart?'),
actions: <Widget>[
FlatButton(
child: Text('Yes'),
onPressed: () {
Navigator.of(context).pop();
Navigator.of(context).pop();
widget.model.deleteAllCartItems();
}),
FlatButton(
child: Text('No'),
onPressed: () {
Navigator.of(context).pop();
},
),
],
);
},
);
},
),
SizedBox(
width: 20.0,
),
RaisedButton(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10)),
child: Text('Confirm Order'),
color: Colors.green,
onPressed: () {
_buildJsonOrder();
},
),
],
),
],
),
);
} else {
itemCartCards = Container(
child: Center(
child: Text('Your cart is empty.'),
),
);
}
return itemCartCards;
}
void _buildJsonOrder() {
final List<dynamic> _listItems = [];
for (Item item in widget.model.itemsInCart) {
final Map<String, dynamic> itemData = {
'hallid': [widget.model.hallNumber],
'tableid': [widget.model.tableNumber],
'itemid': ['${item.itemid}'],
'itemperi': ['${item.itemperi}'],
'kind': [0],
'catid': [item.itemCatId],
'subcatid': [item.itemSubcatId],
'quantity': [item.itemCount],
'price': [item.itemprice]
};
_listItems.add(itemData);
}
final Map<String, dynamic> orderData = {
'hallid': [widget.model.hallNumber],
'tableid': [widget.model.tableNumber],
'typeofpos': ['4'],
'posid': [600],
'userid': [widget.model.currentUserId],
'items': _listItems
};
widget.model
.sendOrder(
widget.model.serverAddress, widget.model.serverPort, orderData)
.then((bool success) {
if (success) {
showDialog(
barrierDismissible: false,
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: Text('Success.'),
content: Text('Your order has been placed successfully!'),
actions: <Widget>[
FlatButton(
onPressed: () {
widget.model.deleteAllCartItems();
Navigator.pop(context);
Navigator.of(context).pop();
},
child: Text('OK'),
)
],
);
});
} else {
showDialog(
barrierDismissible: false,
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: Text('An error has occured.'),
content: Text('Something went wrong with your order.'),
actions: <Widget>[
FlatButton(
onPressed: () {
Navigator.pop(context);
},
child: Text('OK'),
)
],
);
});
}
});
}
}
ItemList.dart Code:
import 'package:flutter/material.dart';
import 'package:scoped_model/scoped_model.dart';
import '../../models/item.dart';
import '../../scoped-models/main.dart';
import './item_row.dart';
class ItemList extends StatefulWidget {
final MainModel model;
ItemList(this.model);
#override
State<StatefulWidget> createState() {
return _ItemListState();
}
}
class _ItemListState extends State<ItemList> {
#override
void initState() {
super.initState();
}
Widget _buildItemList(List<Item> items) {
Widget itemCards;
if (items.length > 0) {
itemCards = ListView.separated(
separatorBuilder: (contex, index) => Divider(
color: widget.model.themeBrightness == 1
? Colors.white
: Colors.black,
),
itemBuilder: (BuildContext context, index) {
return ItemRow(widget.model, items[index]);
},
itemCount: items.length,
);
} else {
itemCards = Container();
}
return itemCards;
}
#override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Expanded(
child: ScopedModelDescendant<MainModel>(
builder: (BuildContext context, Widget child, MainModel model) {
return _buildItemList(model.items);
},
),
),
],
);
}
}
ItemRow.dart Code:
import 'package:flutter/material.dart';
import 'package:scoped_model/scoped_model.dart';
import '../../models/item.dart';
import '../../scoped-models/main.dart';
class ItemRow extends StatefulWidget {
final MainModel model;
final Item item;
ItemRow(this.model, this.item);
#override
State<StatefulWidget> createState() {
return _ItemRowState();
}
}
class _ItemRowState extends State<ItemRow> {
int _itemCount;
TextEditingController _itemCountController;
#override
void initState() {
setState(() {
_itemCount = widget.item.itemCount;
_itemCountController = TextEditingController(text: _itemCount.toString());
});
super.initState();
}
#override
void didUpdateWidget(ItemRow oldWidget) {
if (oldWidget.item.itemid != widget.item.itemid) {
setState(() {
_itemCount = widget.item.itemCount;
_itemCountController =
TextEditingController(text: _itemCount.toString());
});
}
super.didUpdateWidget(oldWidget);
}
#override
Widget build(BuildContext context) {
return ScopedModelDescendant<MainModel>(
builder: (BuildContext context, Widget child, MainModel model) {
return _buildItem(widget.item);
},
);
}
Widget _buildItem(Item item) {
return ListTile(
leading: CircleAvatar(
backgroundImage: item.itemimage == ''
? AssetImage('assets/noimage.png')
: NetworkImage(item.itemimage)),
title: Text(
item.itemperi,
),
trailing: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.end,
children: <Widget>[
widget.model.showListItemsPrices
? Text(
item.itemprice.toString() + ' €',
)
: Container(),
widget.model.showListItemsCart ? VerticalDivider() : Container(),
widget.model.showListItemsCart
? _buildListItemCart(item)
: Container()
],
),
onTap: () {
if (widget.model.clickItems) {
Navigator.pushNamed(context, '/item/' + item.itemid);
}
},
);
}
Widget _buildListItemCart(Item item) {
return Container(
child: Row(
children: <Widget>[
GestureDetector(
onLongPress: () {
if (_itemCount != 0) {
setState(() {
_itemCount = 0;
_itemCountController =
TextEditingController(text: _itemCount.toString());
widget.model.deleteItemFromCart(item.itemid);
});
}
},
child: IconButton(
icon: Icon(Icons.remove),
onPressed: () {
if (_itemCount != 0) {
setState(() {
_itemCount--;
_itemCountController =
TextEditingController(text: _itemCount.toString());
if (_itemCount == 0) {
widget.model.deleteItemFromCart(item.itemid);
} else {
widget.model.updateItemCart(item.itemid, _itemCount);
}
});
}
},
),
),
Container(
width: 30.0,
child: TextField(
decoration: InputDecoration(border: InputBorder.none),
textAlign: TextAlign.center,
controller: _itemCountController,
keyboardType: TextInputType.numberWithOptions(),
onTap: () {
_itemCountController.selection = TextSelection(
baseOffset: 0,
extentOffset: _itemCountController.text.length);
},
onSubmitted: (value) {
if (value == 0.toString()) {
widget.model.deleteItemFromCart(item.itemid);
}
if (value != 0.toString() && _itemCount == 0) {
widget.model.addItemToCart(
item.itemid,
item.itemperi,
item.itemprice,
int.parse(value),
int.parse(widget.model.selectedCatid),
int.parse(widget.model.selectedSubcatId));
}
if (value != 0.toString() && _itemCount != 0) {
widget.model.updateItemCart(item.itemid, int.parse(value));
}
_itemCount = int.parse(value);
TextEditingController(text: _itemCount.toString());
},
),
),
IconButton(
icon: Icon(Icons.add),
onPressed: () {
setState(
() {
_itemCount++;
_itemCountController =
TextEditingController(text: _itemCount.toString());
if (_itemCount == 1) {
widget.model.addItemToCart(
item.itemid,
item.itemperi,
item.itemprice,
_itemCount,
int.parse(widget.model.selectedCatid),
int.parse(widget.model.selectedSubcatId));
} else {
widget.model.updateItemCart(item.itemid, _itemCount);
}
},
);
},
),
],
),
);
}
}
ScopedModelItems.dart Code:
import 'package:scoped_model/scoped_model.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';
import '../models/item.dart';
import '../scoped-models/main.dart';
mixin ItemsModel on Model {
MainModel model;
List<Item> _items = [];
List<Item> itemsEmpty = [];
int _cartTotalItems = 0;
List<Item> _itemsCartEmpty = [];
Item _itemDetails;
Item get itemDetails {
return _itemDetails;
}
List<Item> get items {
return List.of(_items);
}
List<Item> get itemsInCart {
return _itemsCartEmpty;
}
int get cartTotalItems {
if (_cartTotalItems == null) {
_cartTotalItems = 0;
}
return _cartTotalItems;
}
void initState() {
_items.forEach(_addItem);
}
void _addItem(Item item) {
itemsEmpty.add(item);
}
void addItemToCart(String itemid, String itemperi, num itemprice,
int itemQuantity, int itemCatid, int itemSubcatid) {
Item item = Item(
itemid: itemid,
itemperi: itemperi,
itemprice: itemprice,
itemCount: itemQuantity,
itemCatId: itemCatid,
itemSubcatId: itemSubcatid);
_itemsCartEmpty.add(item);
_cartTotalItems += itemQuantity;
notifyListeners();
}
void updateItemCart(String itemid, int itemQuantity) {
_itemsCartEmpty.forEach((item) {
if (item.itemid == itemid) {
_cartTotalItems -= item.itemCount;
item.itemCount = itemQuantity;
_cartTotalItems += itemQuantity;
}
});
notifyListeners();
}
void deleteItemFromCart(String itemid) {
_itemsCartEmpty.forEach((item) {
if (item.itemid == itemid) {
_cartTotalItems -= item.itemCount;
}
});
_itemsCartEmpty.removeWhere((item) => item.itemid == itemid);
notifyListeners();
}
void deleteAllCartItems() {
_itemsCartEmpty.removeRange(0, _itemsCartEmpty.length);
_cartTotalItems = 0;
notifyListeners();
}
Future<bool> fetchItems(String serverAddress, String serverPort,
dynamic catid, dynamic subcatid) {
return http.get(
'http://$serverAddress:$serverPort/cats/$catid/subcats/$subcatid/items/GR',
headers: {'Accept': 'application/json'}).then((http.Response response) {
if (response.statusCode == 200 || response.statusCode == 201) {
final List<Item> fetchedItemList = [];
final List<dynamic> itemListData = json.decode(response.body);
itemListData.forEach((dynamic itemData) {
String imageData = '', periData = '';
if (itemData['item_image_path'] != '') {
imageData = 'http://$serverAddress:$serverPort/photos/' +
itemData['item_image_path'];
}
if (itemData['item_webperi'] == '') {
periData = itemData['item_peri'];
} else {
periData = itemData['item_webperi'];
}
final Item item = Item(
itemid: itemData['item_id'],
itemperi: periData,
itemimage: imageData,
itemprice: itemData['item_price'],
itemCount: 0);
if (_itemsCartEmpty.isNotEmpty) {
for (Item itemCart in _itemsCartEmpty) {
if (itemCart.itemid == item.itemid) {
item.itemCount = itemCart.itemCount;
}
}
}
fetchedItemList.add(item);
});
_items = fetchedItemList;
notifyListeners();
return true;
} else {
return false;
}
}).catchError((error) {
print(error);
notifyListeners();
return false;
});
}
}
Since there's no code I'll just list the common errors that cause this when using scoped model:
1. Not calling notify
You have to call notifyListeners() in your scoped model to update the UI that's using your porperties.
2. Not telling model to rebuild
If you're not using a ScopedModelDescendent widget and instead you're getting your model using
appModel = ScopedModel.of<AppModel>(context);
change your code to
appModel = ScopedModel.of<AppModel>(context, rebuildOnChange: true);
If you're doing both of these then please post your code for your UI and scoped model so that it's easier to offer our help.
I'm having trouble passing the data that's been filled in a textformfields and selected in a dropdown menu.
I'm trying to use the Map function to pass down String values so that I can also pass down all types of values in the future (ex. int, bool, double etc.), however it's not working so I need someone to check it out.
main.dart
import 'package:flutter/material.dart';
import 'package:workoutapp/auth/auth.dart';
import 'package:workoutapp/auth/root_page.dart';
import 'package:workoutapp/inheritedWigets/auth_provider.dart';
void main(List<String> args) {
runApp(
WorkoutManager(),
);
}
class WorkoutManager extends StatelessWidget {
#override
Widget build(BuildContext context) {
return AuthProvider(
auth: Auth(),
child: MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Workout Manager',
home: RootPage(),
theme: ThemeData(
primaryColor: Colors.indigo,
primarySwatch: Colors.indigo,
accentColor: Colors.indigoAccent,
hintColor: Colors.indigo,
brightness: Brightness.dark,
),
),
);
}
}
HomePage
import 'package:flutter/material.dart';
import 'package:workoutapp/inheritedWigets/auth_provider.dart';
import './profile_account_page.dart';
import './routines_create_page.dart';
import '../objects/Routines/routines_manager.dart';
import '../tools/custom_drawer.dart';
class HomePage extends StatelessWidget {
final VoidCallback onSignedOut;
final List<Map<String, String>> routines;
HomePage({Key key, this.onSignedOut, this.routines}) : super(key: key);
void _signedOut(BuildContext context) async {
try {
var auth = AuthProvider.of(context).auth;
await auth.signOut();
onSignedOut();
} catch (e) {
print(e);
}
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Workout Manager', style: TextStyle(color: Colors.white)),
centerTitle: false,
actions: <Widget>[
FlatButton(
child: Text('Logout'),
onPressed: () {
return _signedOut(context);
},
),
IconButton(
icon: Icon(Icons.account_box),
tooltip: 'Profile Account',
color: Colors.white,
onPressed: () {
return Navigator.push(context,
MaterialPageRoute(builder: (BuildContext context) {
return ProfileAccountPage();
}));
},
),
],
),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.add),
onPressed: () {
Navigator.push(context,
MaterialPageRoute(builder: (BuildContext context) {
return RoutinesPageCreate();
}));
},
),
body: RoutinesManager(),
drawer: CustomDrawer(),
);
}
}
RoutineManager
import 'package:flutter/material.dart';
import 'package:workoutapp/objects/routines/routines.dart';
class RoutinesManager extends StatefulWidget {
final Map<String, String> startingRoutine;
RoutinesManager({this.startingRoutine});
#override
_RoutinesManagerState createState() => _RoutinesManagerState();
}
class _RoutinesManagerState extends State<RoutinesManager> {
List<Map<String, String>> _routines = [];
#override
void initState() {
if (widget.startingRoutine != null) {
_routines.add(widget.startingRoutine);
}
super.initState();
}
#override
Widget build(BuildContext context) {
return Column(
children: <Widget>[
Expanded(
child: Routines(_routines),
)
],
);
}
}
RoutinesCreatePage
import 'package:flutter/material.dart';
import 'package:workoutapp/pages/home_page.dart';
class RoutinesPageCreate extends StatefulWidget {
#override
_RoutinesPageCreateState createState() => _RoutinesPageCreateState();
}
class _RoutinesPageCreateState extends State<RoutinesPageCreate> {
final formKey = GlobalKey<FormState>();
List<Map<String, String>> _routines = [];
String _routineName, _routineDescription;
var _routineNameController = TextEditingController();
var _routineDescriptionController = TextEditingController();
List<DropdownMenuItem<String>> _dropdownListBodyPartMenuItem = [];
List<String> _dropdownListBodyPart = [
'Chest',
'Back',
'Leg',
'Shoulder',
'Abs',
];
String _selectedBodyPart;
List<DropdownMenuItem<String>> _dropdownListDayOfWeekMenuItem = [];
List<String> _dropdownListDayOfWeek = [
'Monday',
'Tuesday',
'Wednesday',
'Thursday',
'Friday',
'Saturday',
'Sunday',
];
String _selectedDayOfWeek;
void loadBodyPartData() {
_dropdownListBodyPartMenuItem = [];
_dropdownListBodyPartMenuItem = _dropdownListBodyPart.map((val) {
return DropdownMenuItem<String>(
child: Text(val),
value: val,
);
}).toList();
}
void loadDayOfWeekData() {
_dropdownListDayOfWeekMenuItem = [];
_dropdownListDayOfWeekMenuItem = _dropdownListDayOfWeek.map((val) {
return DropdownMenuItem<String>(
child: Text(val),
value: val,
);
}).toList();
}
final _scaffoldState = GlobalKey<ScaffoldState>();
void _showSnakBarReset() {
_scaffoldState.currentState.showSnackBar(
SnackBar(
backgroundColor: Theme.of(context).accentColor,
content: Text('Showing SnackBar TEST'),
),
);
}
void _showSnakBarCreateWorkoutRoutine() {
_scaffoldState.currentState.showSnackBar(
SnackBar(
backgroundColor: Theme.of(context).accentColor,
content: Text('Workout Routine has been created'),
),
);
}
void _addRoutine(Map<String, String> routine) {
setState(() {
_routines.add(routine);
});
}
#override
Widget build(BuildContext context) {
loadBodyPartData();
loadDayOfWeekData();
return Scaffold(
key: _scaffoldState,
appBar: AppBar(
title: Text('Create Routines'),
),
body: Container(
padding: EdgeInsets.all(15.0),
child: Form(
key: formKey,
child: ListView(children: buildInputs() + buildCreateButtons()),
),
),
);
}
List<Widget> buildInputs() {
TextStyle textStyle = Theme.of(context).textTheme.title;
return [
TextFormField(
controller: _routineNameController,
validator: (value) {
if (value.length > 20) {
return 'Not a valid Routine Name';
}
},
onSaved: (value) {
return _routineName = value;
},
decoration: InputDecoration(
labelStyle: textStyle,
labelText: 'Routine Name',
hintText: 'Enter the Routine Name for this day',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(5.0),
))),
Padding(padding: EdgeInsets.all(7.0)),
TextFormField(
controller: _routineDescriptionController,
validator: (value) {
if (value.length > 50) {
return 'Invalid: The Description must be 50 characters or less.';
}
},
onSaved: (value) {
return _routineDescription = value;
},
decoration: InputDecoration(
labelStyle: textStyle,
labelText: 'Description',
hintText: 'Enter the description of the Routine.',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(5.0),
))),
Padding(padding: const EdgeInsets.all(7.0)),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
DropdownButtonHideUnderline(
child: DropdownButton(
value: _selectedBodyPart,
items: _dropdownListBodyPartMenuItem,
hint: Text('Select Body Part', style: textStyle),
onChanged: (value) {
setState(() {
_selectedBodyPart = value;
});
})),
Padding(
padding: const EdgeInsets.all(1.0),
),
DropdownButtonHideUnderline(
child: DropdownButton(
value: _selectedDayOfWeek,
items: _dropdownListDayOfWeekMenuItem,
hint: Text('Select Day of Week', style: textStyle),
onChanged: (value) {
setState(() {
_selectedDayOfWeek = value;
});
},
),
),
Padding(
padding: const EdgeInsets.all(4.0),
)
],
),
];
}
List<Widget> buildCreateButtons() {
return [
Padding(
padding: const EdgeInsets.all(5.0),
child: Row(
children: <Widget>[
Expanded(
child: RaisedButton(
textColor: Theme.of(context).primaryColorDark,
color: Theme.of(context).accentColor,
child: Text('Create Workout Routine'),
onPressed: () {
if (formKey.currentState.validate()) {
_showSnakBarCreateWorkoutRoutine();
formKey.currentState.save();
_addRoutine({
'routineName': 'Chest Workout',
'description': 'Heavy',
'bodyPart': 'Chest',
'week': 'Monday',
});
Navigator.push(context,
MaterialPageRoute(builder: (BuildContext context) {
return HomePage();
}));
} else {
return null;
}
}),
),
Expanded(
child: RaisedButton(
textColor: Theme.of(context).primaryColorLight,
color: Theme.of(context).primaryColorDark,
child: Text('Reset'),
onPressed: () {
setState(() {
_showSnakBarReset();
formKey.currentState.reset();
_selectedBodyPart = null;
_selectedDayOfWeek = null;
});
},
),
),
],
),
),
];
}
}
Routines
import 'package:flutter/material.dart';
import 'package:workoutapp/objects/routines/routines_detail.dart';
class Routines extends StatelessWidget {
final List<Map<String, String>> routines;
Routines(this.routines);
Widget _buildRoutinesItem(BuildContext context, int index) {
TextStyle textStyle = Theme.of(context).textTheme.title;
return Expanded(
child: Card(
margin: EdgeInsets.all(5.0),
child: Column(
children: <Widget>[
Padding(
padding: const EdgeInsets.all(5.0),
child: Text(routines[index]['routineName'], style: textStyle)),
Padding(
padding: const EdgeInsets.all(5.0),
child: Text(routines[index]['description'], style: textStyle)),
Padding(
padding: const EdgeInsets.all(5.0),
child: Text(routines[index]['bodyPart'], style: textStyle)),
Padding(
padding: const EdgeInsets.all(5.0),
child: Text(routines[index]['week'], style: textStyle)),
Padding(
padding: const EdgeInsets.all(5.0),
child: ButtonBar(
alignment: MainAxisAlignment.center,
children: <Widget>[
FlatButton(
child: Text('Details'),
onPressed: () {
return Navigator.push(context,
MaterialPageRoute(builder: (BuildContext context) {
return RoutinesDetail(
routines[index]['routineName'],
routines[index]['description'],
routines[index]['bodyPart'],
routines[index]['week']);
}));
},
)
],
),
)
],
),
),
);
}
Widget _buildRoutinesList(context) {
TextStyle textStyle = Theme.of(context).textTheme.title;
Widget routinesCards = Container(
child: Container(
child: Center(
child: Text("No routines found, please add some.", style: textStyle),
),
),
);
if (routines.length > 0 || routines.length <= 7) {
ListView.builder(
itemBuilder: _buildRoutinesItem,
itemCount: routines.length,
);
}
return routinesCards;
}
#override
Widget build(BuildContext context) {
return _buildRoutinesList(context);
}
}
RoutineDetailPage
import 'package:flutter/material.dart';
class RoutinesDetail extends StatelessWidget {
final String routineName, description, bodyPart, week;
RoutinesDetail(this.routineName, this.description, this.bodyPart, this.week);
#override
Widget build(BuildContext context) {
TextStyle textStyle = Theme.of(context).textTheme.title;
return Scaffold(
appBar: AppBar(
title: Text(routineName),
centerTitle: true,
),
body: Container(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Padding(
padding: const EdgeInsets.all(5.0),
child: Text(routineName, style: textStyle)),
Padding(
padding: const EdgeInsets.all(5.0),
child: Text(description, style: textStyle)),
Padding(
padding: const EdgeInsets.all(5.0),
child: Text(bodyPart, style: textStyle)),
Padding(
padding: const EdgeInsets.all(5.0),
child: Text(week, style: textStyle)),
Container(
padding: EdgeInsets.all(5.0),
child: RaisedButton(
child: Text('Delete'),
onPressed: () {
Navigator.pop(context);
},
),
),
],
),
),
);
}
}
As you can see, I'm trying to separate the code into multiple files as much as possible, so it's more "readable" and make it easy for myself to make changes to the code whenever I have to in the future.
The problem is, it's quite spit up, I don't understand how to use the data and pass it down or up to the pages or widgets as there are multiple stateful and stateless widgets that are suppose to work together to make this app possible.
You'll notice on the HomePage file (StatelessWidget), I'm trying to show the Scaffold body argument with the RoutinesManager StatefulWidget, which is in a different file. At the same time in the HomePage file, I have a Scaffold floatingActionButton argument that will take you to the RoutinesCreatePage StatefulWidget to create a List of Cards (StatelessWidget) using the ListView.builder(). However, no Card gets created under the HomePage after the "Create Workout Routine" RaisedButton gets pressed in the RoutinesCreatePage and no data gets passed.
Can someone please help me out here as I am totally clueless. Also, I'm fairly a beginner regarding flutter/dart so a solution with a relatively easy to understand explanation would be very helpful.
Note: I do have other files that contribute to this app, however I don't think they're part of the problem so I left them out intentionally.
If more information is needed, please do let me know.
Thanks you!
it looks like you misunderstand what state in Flutter is. To explain in short, state is the internal status/data/... that belongs that that specific widget. StatefulWidget has state to determine if UI should be re-rendered on its own state change. External widgets never know about other widgets' states.
So it means, any state change happening inside RoutinesCreatePage widget, only that RoutinesCreatePage knows and reacts. Unless, you inform other widgets to know something has changed.
Alright, so talking about navigation, it works like a stack structure. HomePage trigger a push to RoutinesCreatePage, then to return, you need to pop, not another push.
Here a quick fix for your code, you can try.
HomePage
floatingActionButton: FloatingActionButton(
child: Icon(Icons.add),
onPressed: () {
handleNewRoutine(); <--- this is to handle navigation and retrieve returning data from pop
},
),
Future handleNewRoutine() async {
var newRoutine = await Navigator.push(
context,
MaterialPageRoute(builder: (context) => RoutinesPageCreate());
)
if (newRoutine == null) {
// nothing returns from RoutinesPageCreate widget
// so do nothing then
} else {
// add to routine list
// and trigger list re-rendering
setState(() {
this.routines.add(newRoutine);
});
}
}
RoutinesCreatePage: when clicking submit button, just populate all data from input fields, make object model and pop to return data to where this widget was pushed.
onPressed: () {
var newRoutine = .... // populate from UI to create new Routine model object.
Navigator.pop(context, newRoutine);
}
Also, take time to read the navigation guide from official Flutter documentation. It is very detailed on this part. https://flutter.io/docs/cookbook/navigation/returning-data
Some additional comments to your code:
in RoutinesCreatePage you don't need to know application level state, I mean _routines variable is unnecessary. You only need one object to store new routine to pop back to HomePage.
in Routines, this method Widget _buildRoutinesList(context) having unused ListView creation.
if (routines.length > 0 || routines.length <= 7) {
ListView.builder(
itemBuilder: _buildRoutinesItem,
itemCount: routines.length,
);
}
I'm trying to create a List of data from online server Firebase using StreamBuilder bu the checkbox won't get checked.
I have used StreamBuilder to get the data and used LisTile widget to build the list items but the checkboxtilelist widget won't work after defining setState() function. And buildBody is defined under build Widget class.
Widget buildBody(BuildContext context) {
return StreamBuilder<QuerySnapshot>(
stream: Firestore.instance.collection('hisab').snapshots(),
builder: (context, snapshots) {
if (!snapshots.hasData) {
return LinearProgressIndicator();
}
return _buildList(context, snapshots.data.documents);
}
);
}
Widget _buildList(BuildContext context, List<DocumentSnapshot> snapshot) {
return ListView(
padding: EdgeInsets.only(top: 20.0),
children: snapshot.map((data) => _buildListitem(context, data)).toList(),
);
}
Widget _buildListitem(BuildContext context, DocumentSnapshot data) {
final record = Record.fromSnapshot((data));
bool _values = false;
void _onChanged(bool newValue) {
setState(() {
_values = newValue;
});
}
return Padding(
padding: EdgeInsets.symmetric(horizontal: 18.0, vertical: 9.0),
child: Container(
decoration: BoxDecoration(
border: Border.all(color: Colors.white),
borderRadius: BorderRadius.circular(5.0),
),
child: new ListTile(
onTap: () {
_onChanged(!_values);
},
leading: CircleAvatar(child: Text(record.name[0])),
title: new Column(
children: <Widget>[
new CheckboxListTile(
title: Text(record.name),
value: _values,
onChanged: _onChanged,
)
],
),
),
),
);
}
It's good idea if you create new stateful widget class:
class CustomListItemWidget extends StatefulWidget {
CustomListItemWidget({Key key, #required this.record}) : super(key: key);
final record;
#override
State createState() => _CustomListItemWidgetState();
}
class _CustomListItemWidgetState extends State<CustomListItemWidget> {
bool _values = false;
void _onChanged(bool newValue) {
setState(() {
_values = newValue;
});
}
#override
Widget build(BuildContext context) {
return Padding(
padding: EdgeInsets.symmetric(horizontal: 18.0, vertical: 9.0),
child: Container(
decoration: BoxDecoration(
border: Border.all(color: Colors.white),
borderRadius: BorderRadius.circular(5.0),
),
child: new ListTile(
onTap: () {
_onChanged(!_values);
},
leading: CircleAvatar(child: Text(widget.record.name[0])),
title: new Column(
children: <Widget>[
new CheckboxListTile(
title: Text(widget.record.name[0]),
value: _values,
onChanged: _onChanged,
)
],
),
),
),
);
}
}
Next, you can pass value from your method _buildListitem:
Widget _buildListitem(BuildContext context, DocumentSnapshot data) {
return CustomListItemWidget(
record: Record.fromSnapshot((data)),
);
}
Say we created a Chip object and TextField object like below. How do you add a Chip to the inside of the TextField?
new Chip(
label: new Text('Peyton Smith'),
)
new TextField(
)
Is it possible to combine them to get something like in the Material spec where typing in something into a Material TextField adds a Chip?
What you are looking for was actually provided in the comment.
Here is a simple implementation using InputChip:
InputChip(
avatar: CircleAvatar(
backgroundColor: Colors.grey.shade800,
child: Text('AB'),
),
label: Text('Aaron Burr'),
onPressed: () {
print('I am the one thing in life.');
}
)
A material design input chip.
Input chips represent a complex piece of information, such as an
entity (person, place, or thing) or conversational text, in a compact
form.
Input chips can be made selectable by setting
onSelected,
deletable by setting
onDeleted,
and pressable like a button with
onPressed.
They have a
label,
and they can have a leading icon (see
avatar)
and a trailing icon
(deleteIcon).
Colors and padding can be customized.
Here is my simple interpretation of InputChip:
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[
Align(
alignment: Alignment.centerLeft,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: _toContainer(),
),
),
Divider(
color: Colors.blueGrey,
height: 10.0,
),
Align(
alignment: Alignment.centerLeft,
child: _subjectContainer(),
),
Divider(
color: Colors.blueGrey,
height: 10.0,
),
Align(
alignment: Alignment.centerLeft,
child: _messageContainer(),
),
],
),
), // This trailing comma makes auto-formatting nicer for build methods.
);
}
Widget _messageContainer() {
return Padding(
padding: const EdgeInsets.all(8.0),
child: Container(
child: Text(
'Message',
style: TextStyle(color: Colors.black, fontSize: 18.0),
),
),
);
}
Widget _toContainer() {
return Wrap(
spacing: 5.0,
children: <Widget>[
Padding(
padding: const EdgeInsets.only(top: 8.0, right: 8.0),
child: Container(
child: Text(
'To',
style: TextStyle(color: Colors.black, fontSize: 18.0),
),
),
),
Container(
child: _profileChips("Scott Hill",
"https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=634&q=80"),
),
],
);
}
Widget _subjectContainer() {
return Padding(
padding: const EdgeInsets.all(8.0),
child: Container(
child: Text(
'Subject',
style: TextStyle(color: Colors.black, fontSize: 18.0),
),
),
);
}
Widget _profileChips(String myName, String myImage) {
return Material(
child: InputChip(
avatar: CircleAvatar(
backgroundColor: Colors.blueGrey,
child: Container(
decoration: BoxDecoration(
shape: BoxShape.circle,
image: DecorationImage(
fit: BoxFit.fill,
image: NetworkImage(myImage),
)),
),
),
label: Text(myName),
labelStyle: TextStyle(
color: Colors.black, fontSize: 14.0, fontWeight: FontWeight.bold),
onPressed: () {},
onDeleted: () {},
),
);
}
}
Output:
And for a fully functional example, I've tested the answer in here.
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
// See: https://twitter.com/shakil807/status/1042127387515858949
// https://github.com/pchmn/MaterialChipsInput/tree/master/library/src/main/java/com/pchmn/materialchips
// https://github.com/BelooS/ChipsLayoutManager
void main() => runApp(ChipsDemoApp());
class ChipsDemoApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(
primaryColor: Colors.indigo,
accentColor: Colors.pink,
),
home: DemoScreen(),
);
}
}
class DemoScreen extends StatefulWidget {
#override
_DemoScreenState createState() => _DemoScreenState();
}
class _DemoScreenState extends State<DemoScreen> {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Material Chips Input'),
),
body: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
Padding(
padding: const EdgeInsets.all(8.0),
child: TextField(
decoration: const InputDecoration(hintText: 'normal'),
),
),
Expanded(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: ChipsInput<AppProfile>(
decoration: InputDecoration(
prefixIcon: Icon(Icons.search), hintText: 'Profile search'),
findSuggestions: _findSuggestions,
onChanged: _onChanged,
chipBuilder: (BuildContext context,
ChipsInputState<AppProfile> state, AppProfile profile) {
return InputChip(
key: ObjectKey(profile),
label: Text(profile.name),
avatar: CircleAvatar(
backgroundImage: NetworkImage(profile.imageUrl),
),
onDeleted: () => state.deleteChip(profile),
onSelected: (_) => _onChipTapped(profile),
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
);
},
suggestionBuilder: (BuildContext context,
ChipsInputState<AppProfile> state, AppProfile profile) {
return ListTile(
key: ObjectKey(profile),
leading: CircleAvatar(
backgroundImage: NetworkImage(profile.imageUrl),
),
title: Text(profile.name),
subtitle: Text(profile.email),
onTap: () => state.selectSuggestion(profile),
);
},
),
),
),
],
),
);
}
void _onChipTapped(AppProfile profile) {
print('$profile');
}
void _onChanged(List<AppProfile> data) {
print('onChanged $data');
}
Future<List<AppProfile>> _findSuggestions(String query) async {
if (query.length != 0) {
return mockResults.where((profile) {
return profile.name.contains(query) || profile.email.contains(query);
}).toList(growable: false);
} else {
return const <AppProfile>[];
}
}
}
// -------------------------------------------------
const mockResults = <AppProfile>[
AppProfile('Stock Man', 'stock#man.com',
'https://d2gg9evh47fn9z.cloudfront.net/800px_COLOURBOX4057996.jpg'),
AppProfile('Paul', 'paul#google.com',
'https://mbtskoudsalg.com/images/person-stock-image-png.png'),
AppProfile('Fred', 'fred#google.com',
'https://media.istockphoto.com/photos/feeling-great-about-my-corporate-choices-picture-id507296326'),
AppProfile('Bera', 'bera#flutter.io',
'https://upload.wikimedia.org/wikipedia/commons/7/7c/Profile_avatar_placeholder_large.png'),
AppProfile('John', 'john#flutter.io',
'https://upload.wikimedia.org/wikipedia/commons/7/7c/Profile_avatar_placeholder_large.png'),
AppProfile('Thomas', 'thomas#flutter.io',
'https://upload.wikimedia.org/wikipedia/commons/7/7c/Profile_avatar_placeholder_large.png'),
AppProfile('Norbert', 'norbert#flutter.io',
'https://upload.wikimedia.org/wikipedia/commons/7/7c/Profile_avatar_placeholder_large.png'),
AppProfile('Marina', 'marina#flutter.io',
'https://upload.wikimedia.org/wikipedia/commons/7/7c/Profile_avatar_placeholder_large.png'),
];
class AppProfile {
final String name;
final String email;
final String imageUrl;
const AppProfile(this.name, this.email, this.imageUrl);
#override
bool operator ==(Object other) =>
identical(this, other) ||
other is AppProfile &&
runtimeType == other.runtimeType &&
name == other.name;
#override
int get hashCode => name.hashCode;
#override
String toString() {
return 'Profile{$name}';
}
}
// -------------------------------------------------
typedef ChipsInputSuggestions<T> = Future<List<T>> Function(String query);
typedef ChipSelected<T> = void Function(T data, bool selected);
typedef ChipsBuilder<T> = Widget Function(
BuildContext context, ChipsInputState<T> state, T data);
class ChipsInput<T> extends StatefulWidget {
const ChipsInput({
Key key,
this.decoration = const InputDecoration(),
#required this.chipBuilder,
#required this.suggestionBuilder,
#required this.findSuggestions,
#required this.onChanged,
this.onChipTapped,
}) : super(key: key);
final InputDecoration decoration;
final ChipsInputSuggestions findSuggestions;
final ValueChanged<List<T>> onChanged;
final ValueChanged<T> onChipTapped;
final ChipsBuilder<T> chipBuilder;
final ChipsBuilder<T> suggestionBuilder;
#override
ChipsInputState<T> createState() => ChipsInputState<T>();
}
class ChipsInputState<T> extends State<ChipsInput<T>>
implements TextInputClient {
static const kObjectReplacementChar = 0xFFFC;
Set<T> _chips = Set<T>();
List<T> _suggestions;
int _searchId = 0;
FocusNode _focusNode;
TextEditingValue _value = TextEditingValue();
TextInputConnection _connection;
String get text => String.fromCharCodes(
_value.text.codeUnits.where((ch) => ch != kObjectReplacementChar),
);
bool get _hasInputConnection => _connection != null && _connection.attached;
void requestKeyboard() {
if (_focusNode.hasFocus) {
_openInputConnection();
} else {
FocusScope.of(context).requestFocus(_focusNode);
}
}
void selectSuggestion(T data) {
setState(() {
_chips.add(data);
_updateTextInputState();
_suggestions = null;
});
widget.onChanged(_chips.toList(growable: false));
}
void deleteChip(T data) {
setState(() {
_chips.remove(data);
_updateTextInputState();
});
widget.onChanged(_chips.toList(growable: false));
}
#override
void initState() {
super.initState();
_focusNode = FocusNode();
_focusNode.addListener(_onFocusChanged);
}
void _onFocusChanged() {
if (_focusNode.hasFocus) {
_openInputConnection();
} else {
_closeInputConnectionIfNeeded();
}
setState(() {
// rebuild so that _TextCursor is hidden.
});
}
#override
void dispose() {
_focusNode?.dispose();
_closeInputConnectionIfNeeded();
super.dispose();
}
void _openInputConnection() {
if (!_hasInputConnection) {
_connection = TextInput.attach(this, TextInputConfiguration());
_connection.setEditingState(_value);
}
_connection.show();
}
void _closeInputConnectionIfNeeded() {
if (_hasInputConnection) {
_connection.close();
_connection = null;
}
}
#override
Widget build(BuildContext context) {
var chipsChildren = _chips
.map<Widget>(
(data) => widget.chipBuilder(context, this, data),
)
.toList();
final theme = Theme.of(context);
chipsChildren.add(
Container(
height: 32.0,
child: Row(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
Text(
text,
style: theme.textTheme.subtitle1.copyWith(
height: 1.5,
),
),
_TextCaret(
resumed: _focusNode.hasFocus,
),
],
),
),
);
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
//mainAxisSize: MainAxisSize.min,
children: <Widget>[
GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: requestKeyboard,
child: InputDecorator(
decoration: widget.decoration,
isFocused: _focusNode.hasFocus,
isEmpty: _value.text.length == 0,
child: Wrap(
children: chipsChildren,
spacing: 4.0,
runSpacing: 4.0,
),
),
),
Expanded(
child: ListView.builder(
itemCount: _suggestions?.length ?? 0,
itemBuilder: (BuildContext context, int index) {
return widget.suggestionBuilder(
context, this, _suggestions[index]);
},
),
),
],
);
}
#override
void updateEditingValue(TextEditingValue value) {
final oldCount = _countReplacements(_value);
final newCount = _countReplacements(value);
setState(() {
if (newCount < oldCount) {
_chips = Set.from(_chips.take(newCount));
}
_value = value;
});
_onSearchChanged(text);
}
int _countReplacements(TextEditingValue value) {
return value.text.codeUnits
.where((ch) => ch == kObjectReplacementChar)
.length;
}
#override
void performAction(TextInputAction action) {
_focusNode.unfocus();
}
void _updateTextInputState() {
final text =
String.fromCharCodes(_chips.map((_) => kObjectReplacementChar));
_value = TextEditingValue(
text: text,
selection: TextSelection.collapsed(offset: text.length),
composing: TextRange(start: 0, end: text.length),
);
_connection.setEditingState(_value);
}
void _onSearchChanged(String value) async {
final localId = ++_searchId;
final results = await widget.findSuggestions(value);
if (_searchId == localId && mounted) {
setState(() => _suggestions = results
.where((profile) => !_chips.contains(profile))
.toList(growable: false));
}
}
noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
}
class _TextCaret extends StatefulWidget {
const _TextCaret({
Key key,
this.duration = const Duration(milliseconds: 500),
this.resumed = false,
}) : super(key: key);
final Duration duration;
final bool resumed;
#override
_TextCursorState createState() => _TextCursorState();
}
class _TextCursorState extends State<_TextCaret>
with SingleTickerProviderStateMixin {
bool _displayed = false;
Timer _timer;
#override
void initState() {
super.initState();
_timer = Timer.periodic(widget.duration, _onTimer);
}
void _onTimer(Timer timer) {
setState(() => _displayed = !_displayed);
}
#override
void dispose() {
_timer.cancel();
super.dispose();
}
#override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return FractionallySizedBox(
heightFactor: 0.7,
child: Opacity(
opacity: _displayed && widget.resumed ? 1.0 : 0.0,
child: Container(
width: 2.0,
color: theme.primaryColor,
),
),
);
}
}
Working output:
Aside from the samples above, you have the option to use flutter_chips_input plugin.
Flutter library for building input fields with InputChips as input
options.
Here is an example: