Flutter從靜態介面到抽取封裝

張風捷特烈發表於2019-07-22

今天將用Flutter的元件來實際佈局演練一下,在此之前你需要熟悉Flex佈局


1、微信條目的靜態佈局

這個平時非常常見,而且相對簡單,所以是個練手的不錯人選

Flutter從靜態介面到抽取封裝

Flutter從靜態介面到抽取封裝

簡單分析一下:一共三塊,用Row佈局,左右分別處於頭尾,中間自延伸
頭像使用Image,小紅點用ClipOval對Container裁剪,堆疊在一起,用Stack佈局  
中間的文字是兩行的Column,右邊的也是兩行的Column,比較簡單,剩下的就是邊距了。
複製程式碼

1.1:左側頭像

用一個ClipRRect來進行圖片的圓角操作,Container來限制大小,
通過Stack佈局將小紅點放到圖片左上角,小紅點通過ClipOval對Container裁剪

Flutter從靜態介面到抽取封裝

var left = Container(
  child: ClipRRect(
    borderRadius: BorderRadius.circular(5),
    child: Image.asset(
      //頭像
      "images/娜美.jpg",
      fit: BoxFit.cover,
    ),
  ),
  width: 50,
  height: 50,
);
var leftWrap = Stack(
  alignment: Alignment(1.2, -1.2),
  children: <Widget>[
    left,
    ClipOval(child: Container(width: 10, height: 10, color: Colors.red,),)
  ],);
複製程式碼

2.文字和邊距的處理

想讓兩頭的固定,中間填滿,在Flex佈局中可以用Expanded將其包裹

Flutter從靜態介面到抽取封裝

var center = Column(
  mainAxisSize: MainAxisSize.min,
  crossAxisAlignment: CrossAxisAlignment.start,
  children: <Widget>[
    Text("心如止水", style: nameTextStyle),
    Text(
      "在嗎?小哥哥",
      style: infoTextStyle,
      maxLines: 1,
      overflow: TextOverflow.ellipsis,
    )
  ],
);
var right = Column(
  mainAxisAlignment: MainAxisAlignment.center,
  mainAxisSize: MainAxisSize.min,
  children: <Widget>[
    Text("06:45", style: infoTextStyle),
    Icon(
      Icons.visibility_off,
      size: 18,
      color: Color(0xff999999),
    )
  ],
);
var body = Row(
  children: <Widget>[leftWrap, Expanded(child: center,), right],//使用Expanded
);
複製程式碼

邊距根據需求自己加一下,可以用Padding,也可以用Container

Flutter從靜態介面到抽取封裝


2、微信條目的封裝

封裝一個元件,首先要看它是否有狀態,判斷的標準很簡單:
看它的介面是否有需要因響應而改變的部分,有則將該欄位當做狀態值。
這個條目元件有個小紅點,是會隨著狀態的不同而顯隱的,所以寫成有狀態的元件
封裝成元件的好處在於複用起來非常方便,如下就不用再重新寫一遍了。

Flutter從靜態介面到抽取封裝


2.1:建立資訊描述類和組建類
import 'package:flutter/material.dart';

class ItemChart extends StatefulWidget {
  ItemChart({Key key,this.chartBean,}) : super(key: key);
  final ChartBean chartBean;
  @override
  _ItemChartState createState() => _ItemChartState();
}

class _ItemChartState extends State<ItemChart> {
  bool _checked;//是否已檢視
  ChartBean _chartBean;//條目描述類
  
  @override
  Widget build(BuildContext context) {
    return Container();
  }
}

class ChartBean{
  Image image;//頭像
  String name;//名字
  String sentence;//句子
  String time;//時間
  bool shield;//是否遮蔽

  ChartBean({this.image, this.name, this.sentence, this.time,
      this.shield=false});
}
複製程式碼

2.2:使用資訊描述類替換寫死欄位
class _ItemChartState extends State<ItemChart> {
  var nameTextStyle = TextStyle(color: Colors.black, fontSize: 16);
  var infoTextStyle = TextStyle(color: Color(0xff999999), fontSize: 12);
  bool _checked=false;//是否已檢視

  @override
  Widget build(BuildContext context) {

    var left = Container(
      child: ClipRRect(
        borderRadius: BorderRadius.circular(5),
        child: widget.chartBean.image,
      ),
      width: 50,
      height: 50,
    );

    var leftWrap = Stack(
      alignment: Alignment(1.2, -1.2),
      children: <Widget>[
        left,
        ClipOval(child: Container(width: 10, height: 10, color: Colors.red,),)
      ],);

    var center = Column(
      mainAxisSize: MainAxisSize.min,
      crossAxisAlignment: CrossAxisAlignment.start,
      children: <Widget>[
        Text(widget.chartBean.name, style: nameTextStyle),
        Padding(padding: EdgeInsets.only(top:4),child: Text(
          widget.chartBean.sentence,
          style: infoTextStyle,
          maxLines: 1,
          overflow: TextOverflow.ellipsis,
        ),)
      ],
    );

    var right = Column(
      mainAxisAlignment: MainAxisAlignment.center,
      mainAxisSize: MainAxisSize.min,
      children: <Widget>[
        Text(widget.chartBean.time, style: infoTextStyle),
        widget.chartBean.shield?Icon(
          Icons.visibility_off,
          size: 18,
          color: Color(0xff999999),
        ):Container(height: 18,)
      ],
    );
    var body = Row(
      mainAxisAlignment: MainAxisAlignment.spaceAround,
      children: <Widget>[Padding(padding: EdgeInsets.only(right: 10),child: _checked?left:leftWrap,), Expanded(child: center,), right],
    );
    return Container(child: body, padding: EdgeInsets.all(10),);
  }
}
複製程式碼

2.3:新增點選事件與狀態改變

定義一個回撥函式型別,在最外層用一個InkWell漣漪事件響應元件來觸發監聽
在其中將_checked欄位進行改變,再用setState重新渲染。

Flutter從靜態介面到抽取封裝

class ItemChart extends StatefulWidget {
  ItemChart({Key key,this.chartBean,this.onTap}) : super(key: key);
  final ChartBean chartBean;
  final TapCallback onTap;
  @override
  _ItemChartState createState() => _ItemChartState();
}

typedef TapCallback = void Function(ChartBean bean);

---->[build方法中新增監聽以及回撥及狀態改變]----
return InkWell(child:Container(child: body, padding: EdgeInsets.all(10),) ,onTap:
  _checked=true;
  if(widget.onTap!=null){
    widget.onTap(widget.chartBean);
  }
  setState(() {
  });
},);

---->[使用]----
show = ItemChart(
  onTap: (bean){
    print(bean.name);
  },
  chartBean: ChartBean(
    name: "張風捷特烈",
      sentence: "我是要成為程式設計之王的男人。你要不要成為程式設計之王的女人?",
      time: "08:30",
      shield: false,
      image: Image.asset(
    "images/icon_head.png",
  )),
);
複製程式碼

這樣,當拿到json資料,解析,填充到ListView中就非常方便了。


3、掘金簡介的靜態介面

個人覺得掘金的簡介還是挺好看的,就來看看這個如何佈局:

Flutter從靜態介面到抽取封裝


3.1:佈局分析

Flutter從靜態介面到抽取封裝

最外層做個Card,其中主要三部分,可以用Row來包,
左邊頭像,可以用Image ,加圓形裁剪。
中間是一個三行的Column ,水平方向靠左,並且自延伸
右邊是兩行的Column,上下左右居中
複製程式碼

3.2: 左側

使用ClipOval將一個Image裁成圓形

var left = ClipOval(
    child: Image.asset(
  //頭像
  "images/icon_head.png",
  width: 50,
  height: 50,
));
複製程式碼

3.3:中間

這裡是關於文字的操作,有一點要注意的Flex中的textBaseline屬性對文字中的作用
使用Expanded可以讓Row儘可能延展,文字到頭也會自動換行,當橫屏是也會適應。

Flutter從靜態介面到抽取封裝

var titleTextStyle=TextStyle(color: Colors.black, fontSize: 20);
var infoTextStyle=TextStyle(color: Color(0xff72777B), fontSize: 14);

var user = Row(
  children: <Widget>[
    Text(
      "張風捷特烈",
      style: titleTextStyle,
    ),
    ClipRRect(
      borderRadius: BorderRadius.circular(5),
      child: Container(
        padding: EdgeInsets.symmetric(horizontal: 4),
        color:Color(0xff34D19B) ,
        child: Text("Lv4",
          style: TextStyle(
            fontWeight: FontWeight.w900,
            color: Colors.white,fontSize: 10,
          ),
        )),)
  ],
);
var info = Row(
  children: <Widget>[
    Container(
      margin: EdgeInsets.only(right: 5),
      child: Icon(Icons.next_week, size: 15),
    ),
    Text(
      "創世神 | 無",
      style: infoTextStyle,
    )
  ],
);
var say = Row(
  crossAxisAlignment: CrossAxisAlignment.baseline,
  textBaseline: TextBaseline.ideographic,
  children: <Widget>[
    Container(
      margin: EdgeInsets.only(right: 5),
      child: Icon(Icons.keyboard, size: 15),
    ),
    Expanded(
        child: Text("海的彼岸有我未曾見證的風采",
            style:infoTextStyle))
  ],
);

複製程式碼

3.4:右邊

也比較簡單,通過一個Column將兩塊拼出來。

var right = Column(
  crossAxisAlignment: CrossAxisAlignment.center,
  mainAxisSize: MainAxisSize.min,
  children: <Widget>[
    Row(
      children: <Widget>[
        Icon(
          Icons.language,
          size: 18,
        ),
        Padding(
            padding: EdgeInsets.symmetric(horizontal: 10),
            child: Icon(Icons.local_pharmacy, size: 18)),
        Icon(Icons.person_pin_circle, size: 18)
      ],
    ),
    OutlineButton(
      onPressed: () {},
      child: Text(
        "編輯資料",
        style: TextStyle(fontSize: 11),
      ),
      textTheme: ButtonTextTheme.primary,
      borderSide: BorderSide(color: Color(0xff6F80F7), width: 1),
    ),
  ],
);
複製程式碼

3.5:將幾部分拼合

Flutter從靜態介面到抽取封裝

var result = Card(
    child: Row(
  children: <Widget>[
    Container(
      margin: EdgeInsets.only(left: 10, right: 10),
      child: left,
    ),
    Expanded(child: center),
    Container(
      margin: EdgeInsets.only(left: 10, right: 10),
      child: right,
    ),
  ],
  mainAxisAlignment: MainAxisAlignment.spaceAround,
));
複製程式碼

4.對靜態元件的封裝

上面寫的只是一個靜態的佈局,也只是個玩偶而已,複用和可維護性是很低的。
如何讓它成為一個能隨意更改內容的有靈魂的元件呢?如下,可以很容易複用
將可以抽離的寫死欄位抽離出來,自定義一個描述類作為入參,這是基本的思路

Flutter從靜態介面到抽取封裝


4.1:建立描述類

將頁面上的欄位進行抽取,形成一個類

class User {
  String name;//名字
  int lever;//等級
  Image image;//頭像
  String position;//職務
  String company;//公司
  String proverbs;//箴言
  User({this.name, this.lever, this.image, this.position, this.company,
    this.proverbs});
}
複製程式碼

4.2:建立一個UserPanel元件

繼承自StatelessWidget的無狀態元件,傳入一個User物件

class UserPanel extends StatelessWidget {
  final User user;
  UserPanel({this.user});
  
  @override
  Widget build(BuildContext context) {
    return null;
  }
}
複製程式碼

4.3:結合

將上面的程式碼中寫死的部分用User物件的屬性替換即可,然後再build方法裡返回出去
比如在名字的等級,其他的就不貼了,換一下就行了。在使用時傳入User物件即可

var user = Row(
  children: <Widget>[
    Text(
      this.user.name,
      style: titleTextStyle,
    ),
    ClipRRect(
      borderRadius: BorderRadius.circular(5),
      child: Container(
          padding: EdgeInsets.symmetric(horizontal: 4),
          color: Color(0xff34D19B),
          child: Text(
            "Lv${this.user.lever}",
            style: TextStyle(
              fontWeight: FontWeight.w900,
              color: Colors.white,
              fontSize: 10,
            ),
          )),
    )
  ],
);

---->[使用方法]----
 UserPanel(
   user: User(
       name: "Toly",
       lever: 4,
       company: "捷特王國",
       position: "程式設計之王",
       proverbs: "心之既在,無問東西。",
       image: Image.asset(
         //頭像
         "images/head_me.jpg",
       )),
 ),
複製程式碼

4.4:新增點選事件監聽

既然封裝了,就完善一些,將影象和編輯資料的點選回撥傳遞出去
如果有其他的需要點選,也可以類似的回撥出去

//定義兩個回撥函式型別
typedef HeadTapCallback = void Function();
typedef EditTapCallback = void Function(User user);

//宣告成員變數
final HeadTapCallback onHeadTap;
final EditTapCallback onEditTap;
UserPanel({
    this.user,this.onHeadTap,this.onEditTap});

//頭像點選回撥
var left = InkWell(
    child: ClipOval(
        child: Container(
          width: 50,
          height: 50,
          child: this.user.image,
        )),
    onTap:onHeadTap,
);

//編輯按鈕點選回撥
OutlineButton(
  onPressed: (){
    if (onEditTap!=null) {
      onEditTap(this.user);
    }
  },
  child: Text(
    "編輯資料",
    style: TextStyle(fontSize: 11),
  ),
  textTheme: ButtonTextTheme.primary,
  borderSide: BorderSide(color: Color(0xff6F80F7), width: 1),
),

---->[使用]----
UserPanel(
  onHeadTap: (){
    print("----------------");
  },
  onEditTap: (user){
    print("----------------user:${user.name}");
  },
  user: User(
      name: "Toly",
      lever: 4,
      company: "捷特王國",
      position: "程式設計之王",
      proverbs: "心之既在,無問東西。",
      image: Image.asset(
        //頭像
        "images/head_me.jpg",
      )),
),
複製程式碼

這樣該元件就可以獨立出來,從一個寫死的靜態介面變成了可複用的元件
它會根據你傳入的User物件進行不同的表現,也就是它是"活的",
User便是他的靈魂,回撥監聽便是他的行為。而不像靜態介面,只是人偶而已。
今天從有狀態和無狀態兩種元件看了一下如何對元件進行簡單的封裝,希望你有所收穫。


5.仿淘寶商品item

就不寫靜態介面了,直接上。佈局和上面大同小異,只要能夠劃分好結構,都好辦
這裡要提一點的是下面的價格通過TextSpan處理了一下,你可以好好看看。

Flutter從靜態介面到抽取封裝


5.1、元件使用
var show = Goods(
        onTap:(goods){
            print(goods.title);
        },
        width: 200,
        goods: GoodBean(
          price: 21.89,
          saleCount: 99,
          title: "得力筆記本文具商務復古25K/A5記事本PU軟皮面日記本子定製可印logo簡約工作筆記本會議記錄本小清新大學生用",
          caverUrl: "https://img.alicdn.com/imgextra/i3/108452043/O1CN01IMPSxR1QxjhmdZLXA_!!0-saturn_solar.jpg_220x220.jpg_.webp"),));
  }
複製程式碼

5.2:元件封裝
import 'package:flutter/material.dart';

class Goods extends StatelessWidget {
  Goods({Key key, this.goods, this.width,this.onTap}) : super(key: key);

  final GoodBean goods;
  final double width;
  final TapCallback onTap;
  @override
  Widget build(BuildContext context) {
    var top = Image.network(goods.caverUrl);

    var center = Container(
      child: Text(
        goods.title,
        maxLines: 2,
      ),
    );
    var bottom = Row(
      mainAxisAlignment: MainAxisAlignment.spaceBetween,
      children: <Widget>[
        Padding(
          padding: EdgeInsets.only(right: 5),
          child: _getPriceWidget(goods.price),
        ),
        Expanded(
          child: Text("${goods.saleCount}人付款",style: TextStyle(fontSize:13,color: Colors.black38),),
        ),
        Icon(Icons.more_horiz,color: Colors.black38)
      ],
    );

    var result = Column(
        mainAxisAlignment: MainAxisAlignment.center,
        mainAxisSize: MainAxisSize.min,
        children: <Widget>[
          top,
          Padding(
            padding: EdgeInsets.all(10),
            child: center,
          ),
          Padding(
            padding: EdgeInsets.fromLTRB(10,0,10,10),
            child: bottom,
          )
        ]);
    return Card(
      elevation: 5,
      child: InkWell(child: Container(
        width: width,
        child: result,
      ),onTap: (){
        if(onTap!=null){
          onTap(goods);
        }
      },),
    );
  }

  Widget _getPriceWidget(double price) {
    var prices = price.toString().split(".");
    var span = TextSpan(
      text: '¥',
      style: TextStyle(fontSize: 12, color: Colors.deepOrangeAccent),
      children: <TextSpan>[
        TextSpan(
            text: "${prices[0]}.",
            style: TextStyle(fontSize: 16, color: Colors.deepOrangeAccent)),
        TextSpan(
            text: "${prices[1]}",
            style: TextStyle(fontSize: 12, color: Colors.deepOrangeAccent)),
      ],
    );
    return RichText(text: span);
  }
}

typedef TapCallback = void Function(GoodBean bean);

class GoodBean {
  String caverUrl; //封面圖連結
  String title; //連結
  double price; //價格
  int saleCount;
  GoodBean({this.caverUrl, this.title, this.price, this.saleCount}); //銷售數
}
複製程式碼
結語

本文到此接近尾聲了,如果想快速嚐鮮Flutter,《Flutter七日》會是你的必備佳品;如果想細細探究它,那就跟隨我的腳步,完成一次Flutter之旅。
另外本人有一個Flutter微信交流群,歡迎小夥伴加入,共同探討Flutter的問題,本人微訊號:zdl1994328,期待與你的交流與切磋。

相關文章