iT邦幫忙

2019 iT 邦幫忙鐵人賽

DAY 21
0
自我挑戰組

使用flutter建構Android和iOs APP系列 第 21

登入與新增商品的驗證,橫向時確認input可視

目標:
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可視的組件

ensure_visible.dart檔案內容

  1. 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,
                    ),
                    ],
                ),
                ),
            ),
            ),
        ),
        ),
    );
    }
}

  1. 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,
            );
        //編輯狀況時,是有上一頁可以點的
    }
}

    ```
    

上一篇
橫向手機時的樣式,使用者手勢監聽器
下一篇
管理員可以在商品清單的後台,編輯與刪除商品
系列文
使用flutter建構Android和iOs APP25

尚未有邦友留言

立即登入留言