目標:
1.登入頁面,email使用正規表示法驗證
2.登入頁面,密碼不可為空且長度要大於6
3.登入頁面,若沒有確認接受條款,則不會登入
4.商品新增頁面,商品標題不可為空且長度大於5
5.商品新增頁面,商品描述不可為空且長度大於10
6.商品新增頁面,商品價格不可為空且用正規表示法驗證為數字
7.手機橫向的時候,input是不會被小鍵盤蓋住的
檔案結構:
pages/
auth.dart //登入頁
product_edit.dart //管理者的新增與編輯商品頁
product_list.dart //管理者的商品列表
product.dart //商品細節
product_admin.dart //管理者頁面(有tab那頁)
products.dart //用戶的商品列表
widgets/
products/
address_tag.dart //地址標籤
price_tag.dart //價格標籤
product_card.dart //商品卡
products.dart //如果商品陣列大於零,就呼叫ListView.builder的那個列表
ui_elements/
title_default.dart //字體為Oswald的text
helpers/
ensure_visible.dart //確認讓input可視的組件
...
class _AuthPageState extends State<AuthPage> {
final Map<String, dynamic> _formData = {
//表單內容
'email': null,
'password': null,
'acceptTerms': false
};
final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
//空的GlobalKey,做表單驗證用
DecorationImage _buildBackgroundImage() {
return DecorationImage(...); //產生背景圖
}
Widget _buildEmailTextField() {
return TextFormField(
//此組件可以使用validator
decoration: InputDecoration(
labelText: 'E-Mail', filled: true, fillColor: Colors.white
),
keyboardType: TextInputType.emailAddress,
validator: (String value) {
if (value.isEmpty ||
!RegExp(r"[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?")
.hasMatch(value)) {
return 'Please enter a valid email';
}
//若不是email格式或為空,就回傳這個訊息
},
onSaved: (String value) {
_formData['email'] = value;
//_formKey.currentState.save被觸發時,存入資料
},
);
}
Widget _buildPasswordTextField() {
return TextFormField(
decoration: InputDecoration(
labelText: 'Password', filled: true, fillColor: Colors.white),
obscureText: true,
validator: (String value) {
if (value.isEmpty || value.length < 6) {
return 'Password invalid';
//若長度小於六或為空,就回傳這個訊息
}
},
onSaved: (String value) {
_formData['password'] = value;
},
);
}
Widget _buildAcceptSwitch() {
return SwitchListTile(
value: _formData['acceptTerms'],
onChanged: (bool value) {
setState(() {
_formData['acceptTerms'] = value;
//改變toggle時,就存進去
});
},
title: Text('Accept Terms'),
);
}
void _submitForm() {
if (!_formKey.currentState.validate() || !_formData['acceptTerms']) {
return;
//驗證沒過或acceptTerms是false時就跳出function
}
_formKey.currentState.save();
Navigator.pushReplacementNamed(context, '/products');
//存完後,就跳到商品列表頁面
}
@override
Widget build(BuildContext context) {
...
return Scaffold(
appBar: AppBar(
title: Text('Login'),
),
body: Container(
decoration: BoxDecoration(
image: _buildBackgroundImage(),
),
padding: EdgeInsets.all(10.0),
child: Center(
child: SingleChildScrollView(
child: Container(
...
child: Form(
key: _formKey,
//他的child都會根據這個key來決定驗證過不過
child: Column(
children: <Widget>[
_buildEmailTextField(),
...
_buildPasswordTextField(),
_buildAcceptSwitch(),
...
RaisedButton(
textColor: Colors.white,
child: Text('LOGIN'),
onPressed: _submitForm,
),
],
),
),
),
),
),
),
);
}
}
...
import '../widgets/helpers/ensure_visible.dart';
class ProductEditPage extends StatefulWidget {
final Function addProduct; //新增商品
final Function updateProduct; //編輯商品
final Map<String, dynamic> product; //商品資訊
final int productIndex; //商品編號
ProductEditPage({this.addProduct, this.updateProduct, this.product, this.productIndex});
@override
State<StatefulWidget> createState() {
return _ProductEditPageState();
}
}
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>();
final _titleFocusNode = FocusNode();
final _descriptionFocusNode = FocusNode();
final _priceFocusNode = FocusNode();
//新增三個節點,在小鍵盤跳出時,可以抓到位置
Widget _buildTitleTextField() {
return EnsureVisibleWhenFocused(
focusNode: _titleFocusNode, //節點指定
child: TextFormField(
focusNode: _titleFocusNode, //節點參考
decoration: InputDecoration(labelText: 'Product Title'),
initialValue: widget.product == null ? '' : widget.product['title'],
//初始值是在有傳入product時才有
validator: (String value) {
if (value.isEmpty || value.length < 5) {
return 'Title is required and should be 5+ characters long.';
//若長度小於5或為空,就回傳這個訊息
}
},
onSaved: (String value) {
_formData['title'] = value;
},
),
);
}
Widget _buildDescriptionTextField() {
return EnsureVisibleWhenFocused(
focusNode: _descriptionFocusNode,
child: TextFormField(
focusNode: _descriptionFocusNode,
maxLines: 4,
decoration: InputDecoration(labelText: 'Product Description'),
initialValue: widget.product == null ? '' : widget.product['description'],
//初始值是在有傳入product時才有
validator: (String value) {
// if (value.trim().length <= 0) {
if (value.isEmpty || value.length < 10) {
return 'Description is required and should be 10+ characters long.';
}
//若長度小於10或為空,就回傳這個訊息
},
onSaved: (String value) {
_formData['description'] = value;
},
),
);
}
Widget _buildPriceTextField() {
return EnsureVisibleWhenFocused(
focusNode: _priceFocusNode,
child: TextFormField(
focusNode: _priceFocusNode,
keyboardType: TextInputType.number,
decoration: InputDecoration(labelText: 'Product Price'),
initialValue: widget.product == null ? '' : widget.product['price'].toString(),
//初始值是在有傳入product時才有
validator: (String value) {
if (value.isEmpty ||
!RegExp(r'^(?:[1-9]d*|0)?(?:.d+)?$').hasMatch(value)) {
return 'Price is required and should be a number.';
}
//若不為數字或為空,就回傳這個訊息
},
onSaved: (String value) {
_formData['price'] = double.parse(value);
//在input裡面要轉成字串,放到變數裡面就轉回數字
},
),
);
}
void _submitForm() {
if (!_formKey.currentState.validate()) {
return;
}
//驗證沒過就跳出
_formKey.currentState.save();
//表單內容存入變數
if (widget.product == null) {
widget.addProduct(_formData);
//新增商品
} else {
widget.updateProduct(widget.productIndex, _formData);
//編輯商品
}
Navigator.pushReplacementNamed(context, '/products');
//跳到商品列表
}
@override
Widget build(BuildContext context) {
...
final Widget pageContent = GestureDetector(
//一個可以監聽使用者手勢的組件
onTap: () {
FocusScope.of(context).requestFocus(FocusNode());
//每次tap時都去找focus節點
},
child: Container(
...
child: Form(
key: _formKey,
child: ListView(
...
children: <Widget>[
_buildTitleTextField(),
_buildDescriptionTextField(),
_buildPriceTextField(),
...
RaisedButton(
child: Text('Save'),
textColor: Colors.white,
onPressed: _submitForm,
)
],
),
),
),
);
return widget.product == null
? pageContent
: Scaffold(
appBar: AppBar(
title: Text('Edit Product'),
),
body: pageContent,
);
//編輯狀況時,是有上一頁可以點的
}
}
```