price_tag.dart //價格標籤
product_card.dart //商品卡
products.dart //如果商品陣列大於零,就呼叫ListView.builder的那個列表
ui_elements/
title_default.dart //字體為Oswald的text
helpers/
ensure_visible.dart //確認讓input可視的組件
ensure_visible.dart檔案內容
pages/auth.dart
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,
pages/product_edit.dart
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,
//編輯狀況時,是有上一頁可以點的