iT邦幫忙

2019 iT 邦幫忙鐵人賽

DAY 23
0
自我挑戰組

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

Scoped Model,產生商品類別與使用者類別

講者花了好多時間在整理檔案架構,這次就有點像物件導向的整理方式吧!
1.檔案架構

models -
    products.dart //定義了Product類別(標題,價格,圖片屬性等等)
    user.dart //定義了User類別(email,密碼,id等等)
pages - 
    auth.dart //登入頁面
    product_edit.dart //管理者新增與修改頁面
    product_list.dart //管理者商品清單頁面
    product.dart //使用者商品細節頁面
    product_admin.dart //管理者切換edit和list頁籤的那頁
    products.dart //使用者商品列表頁面
scoped-models -
    connected-products.dart //把所有對products類別產生的實例的操作寫在這
    main.dart //把User類別,Product類別以及,ConnectedProducts類別輸出到最外面的main.dart
widgets -
    helpers -
    ensure_visible.dart //讓input focus的時候看得到的工具
    products -
    address_tag.dart //標籤樣式的地址
    price_tag.dart //標籤樣式的價格
    product_card.dart //商品卡
    products.dart //商品列表
    ui-element -
    title_default.dart //商品名稱的字型公版
main.dart
  1. pubsec.yaml
    目前套件
dependencies:
    flutter:
    sdk: flutter
    cupertino_icons: ^0.1.0
    scoped_model: "^0.3.0"

另外新增了一個功能
將商品加入收藏的功能
並可以利用畫面右上角的愛心來切換全部清單/收藏清單

請見此動畫

1.main.dart

void main() { runApp(MyApp()); }

class MyApp extends StatefulWidget {...} 呼叫createState並回傳_MyAppState

class _MyAppState extends State<MyApp> {
    @override
    Widget build(BuildContext context) {
    return ScopedModel<MainModel>(
        model: MainModel(),
        child: MaterialApp(
        theme: ThemeData(...),
        routes: { ... },  //登入頁 /商品列表頁 /管理者頁 
        onGenerateRoute: (RouteSettings settings) {...}, //若網址有編號就到細節頁
        onUnknownRoute: (RouteSettings settings) {...}, //若上面回傳null就到商品列表頁
        ),
    );
    }
}
  1. pages/auth.dart

class AuthPage extends StatefulWidget {...} 呼叫createState並回傳_AuthPageState

class _AuthPageState extends State<AuthPage> {
    final Map<String, dynamic> _formData = {
    'email': null,
    'password': null,
    'acceptTerms': false
    };
    final GlobalKey<FormState> _formKey = GlobalKey<FormState>(); //表單驗證用的
    DecorationImage _buildBackgroundImage() {...} //登入頁的背景
    Widget _buildEmailTextField() {...} //email的驗證與寫到_formData['email']
    Widget _buildPasswordTextField() {...} //password的驗證與寫到_formData['password']
    Widget _buildAcceptSwitch() {...} //接受條款寫到_formData['acceptTerms']
    void _submitForm(Function login) { 
    ... //acceptTerms是否為true
    _formKey.currentState.save(); //用來呼叫驗證用的
    login(_formData['email'], _formData['password']); //從connected_products.dart傳來的login
    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(
                width: targetWidth,
                child: Form(
                key: _formKey,
                child: Column(
                    children: <Widget>[
                    _buildEmailTextField(), ...
                    _buildPasswordTextField(),
                    _buildAcceptSwitch(), ...
                    ScopedModelDescendant<MainModel>( //把自己寫的類別變資料形式了
                        builder: (BuildContext context, Widget child, MainModel model) {
                        return RaisedButton(
                            textColor: Colors.white,
                            child: Text('LOGIN'),
                            onPressed: () => _submitForm(model.login), 
                            //使用類別傳進來的model物件的login方法
...
  1. scoped-models/main.dart
class MainModel extends Model with ConnectedProductsModel, UserModel, ProductsModel {
}
// with關鍵字可以把其他model一併輸出

4.scoped-models/connected_products.dart

class ConnectedProductsModel extends Model {
    List<Product> _products = []; //自己寫的product變資料類型了
    int _selProductIndex;
    User _authenticatedUser; //自己寫的User變資料類型了

    void addProduct(String title, String description, String image, double price) {
    final Product newProduct = Product( 
        title: title,
        description: description,
        image: image,
        price: price,
        userEmail: _authenticatedUser.email,
        userId: _authenticatedUser.id
    );
    _products.add(newProduct);
    notifyListeners(); //動作作完後,去改變UI呈現
    }
}

class ProductsModel extends ConnectedProductsModel {
    bool _showFavorites = false;

    List<Product> get allProducts {
    return List.from(_products);
    //get是為要讓allProducts和_products存在不同記憶體位置
    //外面也可以call這個function
    //商品列表相關檔案都會呼叫他(等於沒有在其他檔案宣告_products了)
    }

    List<Product> get displayedProducts {
    if (_showFavorites) {
        return _products.where((Product product) => product.isFavorite).toList();
    }
    return List.from(_products);
    //若_showFavorites是true,就只顯示最喜歡的商品列表
    //商品列表頁面有傳入他當參數
    }

    int get selectedProductIndex {
    return _selProductIndex; //編輯商品的時候使用
    }

    Product get selectedProduct {
    if (selectedProductIndex == null) { //直接使用上面的function
        return null;
    }
    return _products[selectedProductIndex]; 
    //回傳整個商品物件
    //編輯商品時使用
    }

    bool get displayFavoritesOnly {
    return _showFavorites;
    //是否只顯示收藏商品的布林值
    //商品頁面header右上角的愛心有用到
    }

    void updateProduct(String title, String description, String image, double price) {
    final Product updatedProduct = Product(
        title: title,
        description: description,
        image: image,
        price: price,
        userEmail: selectedProduct.userEmail,
        userId: selectedProduct.userId
    );
    _products[selectedProductIndex] = updatedProduct;
    notifyListeners(); //編輯商品資訊
    }

    void deleteProduct() {
    _products.removeAt(selectedProductIndex);
    notifyListeners(); //刪除商品
    }

    void toggleProductFavoriteStatus() {
    final bool isCurrentlyFavorite = selectedProduct.isFavorite;
    //只接使用上面的function抓到現在商品細節的商品內容
    final bool newFavoriteStatus = !isCurrentlyFavorite;
    //把喜歡改成不喜歡,不喜歡改成喜歡
    final Product updatedProduct = Product(
        title: selectedProduct.title,
        description: selectedProduct.description,
        price: selectedProduct.price,
        image: selectedProduct.image,
        userEmail: selectedProduct.userEmail,
        userId: selectedProduct.userId,
        isFavorite: newFavoriteStatus);
    _products[selectedProductIndex] = updatedProduct;
    notifyListeners(); 
    //存進陣列
    }

    void selectProduct(int index) {
    _selProductIndex = index;
    notifyListeners();
    //把目前選到的商品編號存入變數
    }

    void toggleDisplayMode() {
    _showFavorites = !_showFavorites;
    notifyListeners();
    //切換是否只顯示喜歡的商品
    }
}

class UserModel extends ConnectedProductsModel {
    void login(String email, String password) {
    _authenticatedUser = User(id: 'fdalsdfasf', email: email, password: password);
    //把_authenticatedUser變數設成一個用User類別傳入登入時的email和密碼以及寫死的id所產生的User實例
    }
}
  1. models/products.dart
    定義用Product類別產生出來的實例有哪些屬性
class Product {
    final String title;
    final String description;
    final double price;
    final String image;
    final bool isFavorite;
    final String userEmail;
    final String userId;

    Product(
        {@required this.title,
        @required this.description,
        @required this.price,
        @required this.image,
        @required this.userEmail,
        @required this.userId,
        this.isFavorite = false});
}
  1. models/user.dart
    定義用User類別產生出來的實例有哪些屬性
import 'package:flutter/material.dart';

class User {
    final String id;
    final String email;
    final String password;

    User({@required this.id, @required this.email, @required this.password});
}
  1. pages/products.dart
    商品列表頁面
    Widget build(BuildContext context) {
    return Scaffold(
        drawer: _buildSideDrawer(context),
        appBar: AppBar(
        title: Text('EasyList'),
        actions: <Widget>[
            ScopedModelDescendant<MainModel>(
            //因為要使用model的變數,故呼叫此類別
            builder: (BuildContext context, Widget child, MainModel model) {
                return IconButton(
                icon: Icon(model.displayFavoritesOnly
                //若connected_products的getter displayFavoritesOnly 是true
                    ? Icons.favorite //就是實心愛心
                    : Icons.favorite_border), //不然是邊框愛心
                onPressed: () {
                    model.toggleDisplayMode();
                    //呼叫若connected_products的toggleDisplayMode
...

8.widgets/products/products.dart
商品列表

    Widget build(BuildContext context) {
    return ScopedModelDescendant<MainModel>(builder: (BuildContext context, Widget child, MainModel model) {
        return  _buildProductList(model.displayedProducts); 
        //傳入connected_products的displayedProducts
        //若傳入陣列長度大於零
        //就用ListView.builder跑出商品列表
    },);
    }
  1. widgets/products/product_card.dart
    商品卡
    Widget _buildActionButtons(BuildContext context) {
    //愛心的部分
    return ButtonBar(
        alignment: MainAxisAlignment.center,
        children: <Widget>[
        IconButton(...), //切到細節頁面的按鈕
        ScopedModelDescendant<MainModel>(
            builder: (BuildContext context, Widget child, MainModel model) {
            return IconButton(
                icon: Icon(model.allProducts[productIndex].isFavorite
                    ? Icons.favorite
                    : Icons.favorite_border),
                //用此商品編號的favorite來決定是否是實心愛心
                color: Colors.red,
                onPressed: () {
                model.selectProduct(productIndex);
                //改變model裡面的商品編號變數
                model.toggleProductFavoriteStatus();
                //讓這個function可以知道要去改哪個商品的被喜歡與否
  1. pages/product.dart
    商品細節頁面
    Widget build(BuildContext context) {
    return WillPopScope(
        onWillPop: () {...}, //若back按鈕被按下時,傳一個false回去 
        child: ScopedModelDescendant<MainModel>(
                //要使用model了
        builder: (BuildContext context, Widget child, MainModel model) {
            final Product product = model.allProducts[productIndex];
            //productIndex是從route name抓到的數字
            return Scaffold(
                appBar: AppBar(...),
                body: Column(...) //把product物件裡面的資訊都放進去
  1. pages/product_admin.dart
    幾乎沒改動

  2. pages/product_edit.dart


    Widget _buildSubmitButton() {
    return ScopedModelDescendant<MainModel>(
        builder: (BuildContext context, Widget child, MainModel model) {
        return RaisedButton(
            child: Text('Save'),
            textColor: Colors.white,
            onPressed: () => _submitForm(model.addProduct, model.updateProduct,
                model.selectProduct, model.selectedProductIndex),
            //_submitForm下面會有定義
        );
        },
    );
    }

    void _submitForm(Function addProduct, Function updateProduct, Function setSelectedProduct,[int selectedProductIndex]) {
    //index有可能是null,因為新增的時候沒有
    if (!_formKey.currentState.validate()) {
        return;
    }
    _formKey.currentState.save();
    if (selectedProductIndex == null) {
        addProduct(
        _formData['title'],
        _formData['description'],
        _formData['image'],
        _formData['price'],
        );
    } else {
        updateProduct(
        _formData['title'],
        _formData['description'],
        _formData['image'],
        _formData['price'],
        );
    }

    Navigator
        .pushReplacementNamed(context, '/products') //回到商品列表頁面
        .then((_) => setSelectedProduct(null)); //改變全域的編號,變成null
        //沒有傳參數,所以畫一個底線
    }

    @override
    Widget build(BuildContext context) {
    return ScopedModelDescendant<MainModel>(
        builder: (BuildContext context, Widget child, MainModel model) {
        final Widget pageContent = _buildPageContent(context, model.selectedProduct); //新增狀態
        return model.selectedProductIndex == null
            ? pageContent
            : Scaffold(
                appBar: AppBar(
                    title: Text('Edit Product'),
                ),
                body: pageContent, //編輯狀態
                );
        },
    );
    }
}

  1. pages/product_list.dart

class ProductListPage extends StatelessWidget {
    Widget _buildEditButton(BuildContext context, int index, MainModel model) {
    return IconButton(
        icon: Icon(Icons.edit),
        onPressed: () {
        model.selectProduct(index);
        //把要編輯的項次傳到全域
        Navigator.of(context).push(
        //直接產生一個最外面route沒定義的頁面
        //也就是商品編輯頁
            MaterialPageRoute(
            builder: (BuildContext context) {
                return ProductEditPage();
....

    Widget build(BuildContext context) {
    return ScopedModelDescendant<MainModel>(
        //要使用model了
        builder: (BuildContext context, Widget child, MainModel model) {
        return ListView.builder(
            itemBuilder: (BuildContext context, int index) {
            return Dismissible(
                key: Key(model.allProducts[index].title),
                //拿title當key
                onDismissed: (DismissDirection direction) {
                if (direction == DismissDirection.endToStart) {
                    model.selectProduct(index);
                    model.deleteProduct();
                    //改變全域的編號
                    //根據全域的編號刪掉商品
                } 
                ...
                },
                background: Container(color: Colors.red),
                child: Column(
                children: <Widget>[
                    ListTile(
                    ... //商品列表的一些樣式
                    trailing: _buildEditButton(context, index, model),

上一篇
管理員可以在商品清單的後台,編輯與刪除商品
下一篇
將商品清單串到firebase資料庫
系列文
使用flutter建構Android和iOs APP25

尚未有邦友留言

立即登入留言