Flutter完整開發(fā)實戰(zhàn)詳解(二、 快速開發(fā)實戰(zhàn)篇)

作為系列文章的第二篇署惯,本篇將為你著重展示:如何搭建一個通用的Flutter App 常用功能腳手架又官,快速開發(fā)一個完整的 Flutter 應(yīng)用延刘。

友情提示:本文所有代碼均在 GSYGithubAppFlutter ,文中示例代碼均可在其中找到六敬,看完本篇相信你應(yīng)該可以輕松完成如下效果碘赖。相關(guān)基礎(chǔ)還請看篇章一

我們的目標(biāo)是外构!( ̄^ ̄)ゞ

文章匯總地址:

Flutter 完整實戰(zhàn)實戰(zhàn)系列文章專欄

Flutter 番外的世界系列文章專欄

前言

本篇內(nèi)容結(jié)構(gòu)如下圖普泡,主要分為: 基礎(chǔ)控件、數(shù)據(jù)模塊典勇、其他功能 三部分劫哼。每大塊中的小模塊,除了涉及的功能實現(xiàn)外割笙,對于實現(xiàn)過程中筆者遇到的問題权烧,會一并展開闡述,本系列的最終目的是: 讓你感受 Flutter 的愉悅伤溉! 那么就讓我們愉悅的往下開始吧般码!

我是簡陋的下圖

一、基礎(chǔ)控件

所謂的基礎(chǔ)乱顾,大概就是砍柴功了吧板祝!

1、Tabbar控件實現(xiàn)

Tabbar 頁面是常有需求走净,而在Flutter中: Scaffold + AppBar + Tabbar + TabbarView 是 Tabbar 頁面的最簡單實現(xiàn)券时,但在加上 AutomaticKeepAliveClientMixin 用于頁面 keepAlive 之后孤里,早期諸如#11895的問題便開始成為Crash的元兇,直到 flutter v0.5.7 sdk 版本修復(fù)后橘洞,問題依舊沒有完全解決捌袜,所以無奈最終修改了實現(xiàn)方案。(1.9.1 stable 中已經(jīng)修復(fù))

目前筆者是通過 Scaffold + Appbar + Tabbar + PageView 來組合實現(xiàn)效果炸枣,從而解決上述問題虏等。下面我們直接代碼走起,首先作為一個Tabbar Widget适肠,它肯定是一個 StatefulWidget 霍衫,所以我們先實現(xiàn)它的 State

 class _GSYTabBarState extends State<GSYTabBarWidget> with SingleTickerProviderStateMixin {
    ///···省略非關(guān)鍵代碼
    @override
    void initState() {
      super.initState();
      ///初始化時創(chuàng)建控制器
      ///通過 with SingleTickerProviderStateMixin 實現(xiàn)動畫效果。
      _tabController = new TabController(vsync: this, length: _tabItems.length);
    }

    @override
    void dispose() {
      ///頁面銷毀時侯养,銷毀控制器
      _tabController.dispose();
      super.dispose();
    }

    @override
    Widget build(BuildContext context) {
      ///底部TAbBar模式
      return new Scaffold(
          ///設(shè)置側(cè)邊滑出 drawer敦跌,不需要可以不設(shè)置
          drawer: _drawer,
          ///設(shè)置懸浮按鍵,不需要可以不設(shè)置
          floatingActionButton: _floatingActionButton,
          ///標(biāo)題欄
          appBar: new AppBar(
            backgroundColor: _backgroundColor,
            title: _title,
          ),
          ///頁面主體沸毁,PageView峰髓,用于承載Tab對應(yīng)的頁面
          body: new PageView(
            ///必須有的控制器,與tabBar的控制器同步
            controller: _pageController,
            ///每一個 tab 對應(yīng)的頁面主體息尺,是一個List<Widget>
            children: _tabViews,
            onPageChanged: (index) {
              ///頁面觸摸作用滑動回調(diào)携兵,用于同步tab選中狀態(tài)
              _tabController.animateTo(index);
            },
          ),
          ///底部導(dǎo)航欄,也就是tab欄
          bottomNavigationBar: new Material(
            color: _backgroundColor,
            ///tabBar控件
            child: new TabBar(
              ///必須有的控制器搂誉,與pageView的控制器同步
              controller: _tabController,
              ///每一個tab item徐紧,是一個List<Widget>
              tabs: _tabItems,
              ///tab底部選中條顏色
              indicatorColor: _indicatorColor,
            ),
          ));
    }
  }

如上代碼所示,這是一個 底部 TabBar 的頁面的效果炭懊。TabBar 和 PageView 之間通過 _pageController_tabController 實現(xiàn) Tab 和頁面的同步并级,通過 SingleTickerProviderStateMixin 實現(xiàn) Tab 的動畫切換效果 (ps 如果有需要多個嵌套動畫效果,你可能需要TickerProviderStateMixin)侮腹,從代碼中我們可以看到:

  • 手動左右滑動 PageView 時嘲碧,通過 onPageChanged 回調(diào)調(diào)用 _tabController.animateTo(index); 同步TabBar狀態(tài)。

  • _tabItems 中父阻,監(jiān)聽每個 TabBarItem 的點擊愈涩,通過 _pageController 實現(xiàn)PageView的狀態(tài)同步。

而上面代碼還缺少了 TabBarItem 的點擊加矛,因為這塊被放到了外部實現(xiàn)履婉。當(dāng)然你也可以直接在內(nèi)部封裝好控件,直接傳遞配置數(shù)據(jù)顯示斟览,這個可以根據(jù)個人需要封裝毁腿。

外部調(diào)用代碼如下:每個 Tabbar 點擊時,通過pageController.jumpTo 跳轉(zhuǎn)頁面,每個頁面需要跳轉(zhuǎn)坐標(biāo)為:當(dāng)前屏幕大小乘以索引 index 已烤。

class _TabBarBottomPageWidgetState extends State<TabBarBottomPageWidget> {

  final PageController pageController = new PageController();
  final List<String> tab = ["動態(tài)", "趨勢", "我的"];

  ///渲染底部Tab
  _renderTab() {
    List<Widget> list = new List();
    for (int i = 0; i < tab.length; i++) {
      list.add(new FlatButton(onPressed: () {
        ///每個 Tabbar 點擊時鸠窗,通過jumpTo 跳轉(zhuǎn)頁面
        ///每個頁面需要跳轉(zhuǎn)坐標(biāo)為:當(dāng)前屏幕大小 * 索引index。
        topPageControl.jumpTo(MediaQuery
            .of(context)
            .size
            .width * i);
      }, child: new Text(
        tab[I],
        maxLines: 1,
      )));
    }
    return list;
  }

  ///渲染Tab 對應(yīng)頁面
  _renderPage() {
    return [
      new TabBarPageFirst(),
      new TabBarPageSecond(),
      new TabBarPageThree(),
    ];
  }


  @override
  Widget build(BuildContext context) {
    ///帶 Scaffold 的Tabbar頁面
    return new GSYTabBarWidget(
        type: GSYTabBarWidget.BOTTOM_TAB,
        ///渲染tab
        tabItems: _renderTab(),
        ///渲染頁面
        tabViews: _renderPage(),
        topPageControl: pageController,
        backgroundColor: Colors.black45,
        indicatorColor: Colors.white,
        title: new Text("GSYGithubFlutter"));
  }
}

如果到此結(jié)束胯究,你會發(fā)現(xiàn)頁面點擊切換時塌鸯,StatefulWidget 的子頁面每次都會重新調(diào)用initState。這肯定不是我們想要的唐片,所以這時你就需要AutomaticKeepAliveClientMixin

每個 Tab 對應(yīng)的 StatefulWidget 的 State 涨颜,需要通過with AutomaticKeepAliveClientMixin 费韭,然后重寫 @override bool get wantKeepAlive => true; ,就可以實不重新構(gòu)建的效果了庭瑰,效果如下圖星持。

頁面效果

既然底部Tab頁面都實現(xiàn)了,干脆頂部tab頁面也一起完成弹灭。如下代碼督暂,和底部Tab頁的區(qū)別在于:

  • 底部tab是放在了 ScaffoldbottomNavigationBar 中。
  • 頂部tab是放在 AppBarbottom 中穷吮,也就是標(biāo)題欄之下逻翁。

同時我們在頂部 TabBar 增加 isScrollable: true 屬性,實現(xiàn)常見的頂部Tab的效果捡鱼,如下方圖片所示八回。

    return new Scaffold(
        ///設(shè)置側(cè)邊滑出 drawer,不需要可以不設(shè)置
        drawer: _drawer,
        ///設(shè)置懸浮按鍵驾诈,不需要可以不設(shè)置
        floatingActionButton: _floatingActionButton,
        ///標(biāo)題欄
        appBar: new AppBar(
          backgroundColor: _backgroundColor,
          title: _title,
          ///tabBar控件
          bottom: new TabBar(
            ///頂部時缠诅,tabBar為可以滑動的模式
            isScrollable: true,
            ///必須有的控制器,與pageView的控制器同步
            controller: _tabController,
            ///每一個tab item乍迄,是一個List<Widget>
            tabs: _tabItems,
            ///tab底部選中條顏色
            indicatorColor: _indicatorColor,
          ),
        ),
        ///頁面主體管引,PageView,用于承載Tab對應(yīng)的頁面
        body: new PageView(
          ///必須有的控制器闯两,與tabBar的控制器同步
          controller: _pageController,
          ///每一個 tab 對應(yīng)的頁面主體褥伴,是一個List<Widget>
          children: _tabViews,
          ///頁面觸摸作用滑動回調(diào),用于同步tab選中狀態(tài)
          onPageChanged: (index) {
            _tabController.animateTo(index);
          },
        ),
      );
頂部TabBar效果

在 TabBar 頁面中生蚁,一般還會出現(xiàn):父頁面需要控制 PageView 中子頁的需求噩翠,這時候就需要用到GlobalKey了,比如 GlobalKey<PageOneState> stateOne = new GlobalKey<PageOneState>(); 邦投,通過 globalKey.currentState 對象伤锚,你就可以調(diào)用到 PageOneState 中的公開方法,這里需要注意 GlobalKey 實例需要全局唯一。

2屯援、上下刷新列表

毫無爭議猛们,必備控件

Flutter 中 為我們提供了 RefreshIndicator 作為內(nèi)置下拉刷新控件狞洋;同時我們通過給 ListView 添加 ScrollController 做滑動監(jiān)聽弯淘,在最后增加一個 Item, 作為上滑加載更多的 Loading 顯示吉懊。

如下代碼所示庐橙,通過 RefreshIndicator 控件可以簡單完成下拉刷新工作,這里需要注意一點是:可以利用 GlobalKey<RefreshIndicatorState> 對外提供 RefreshIndicatorRefreshIndicatorState借嗽,這樣外部就 可以通過 GlobalKey 調(diào)用 globalKey.currentState.show();态鳖,主動顯示刷新狀態(tài)并觸發(fā) onRefresh

上拉加載更多在代碼中是通過 _getListCount() 方法恶导,在原本的數(shù)據(jù)基礎(chǔ)上浆竭,增加實際需要渲染的 item 數(shù)量給 ListView 實現(xiàn)的,最后通過 ScrollController 監(jiān)聽到底部惨寿,觸發(fā) onLoadMore邦泄。

如下代碼所示,通過 _getListCount() 方法裂垦,還可以配置空頁面顺囊,頭部等常用效果。其實就是在內(nèi)部通過改變實際item數(shù)量與渲染Item缸废,以實現(xiàn)更多配置效果包蓝。

class _GSYPullLoadWidgetState extends State<GSYPullLoadWidget> {
  ///···
  final ScrollController _scrollController = new ScrollController();

  @override
  void initState() {
    ///增加滑動監(jiān)聽
    _scrollController.addListener(() {
      ///判斷當(dāng)前滑動位置是不是到達(dá)底部,觸發(fā)加載更多回調(diào)
      if (_scrollController.position.pixels == _scrollController.position.maxScrollExtent) {
        if (this.onLoadMore != null && this.control.needLoadMore) {
          this.onLoadMore();
        }
      }
    });
    super.initState();
  }

  ///根據(jù)配置狀態(tài)返回實際列表數(shù)量
  ///實際上這里可以根據(jù)你的需要做更多的處理
  ///比如多個頭部企量,是否需要空頁面测萎,是否需要顯示加載更多。
  _getListCount() {
    ///是否需要頭部
    if (control.needHeader) {
      ///如果需要頭部届巩,用Item 0 的 Widget 作為ListView的頭部
      ///列表數(shù)量大于0時硅瞧,因為頭部和底部加載更多選項,需要對列表數(shù)據(jù)總數(shù)+2
      return (control.dataList.length > 0) ? control.dataList.length + 2 : control.dataList.length + 1;
    } else {
      ///如果不需要頭部恕汇,在沒有數(shù)據(jù)時腕唧,固定返回數(shù)量1用于空頁面呈現(xiàn)
      if (control.dataList.length == 0) {
        return 1;
      }

      ///如果有數(shù)據(jù),因為部加載更多選項,需要對列表數(shù)據(jù)總數(shù)+1
      return (control.dataList.length > 0) ? control.dataList.length + 1 : control.dataList.length;
    }
  }

  ///根據(jù)配置狀態(tài)返回實際列表渲染Item
  _getItem(int index) {
    if (!control.needHeader && index == control.dataList.length && control.dataList.length != 0) {
      ///如果不需要頭部瘾英,并且數(shù)據(jù)不為0枣接,當(dāng)index等于數(shù)據(jù)長度時,渲染加載更多Item(因為index是從0開始)
      return _buildProgressIndicator();
    } else if (control.needHeader && index == _getListCount() - 1 && control.dataList.length != 0) {
      ///如果需要頭部缺谴,并且數(shù)據(jù)不為0但惶,當(dāng)index等于實際渲染長度 - 1時,渲染加載更多Item(因為index是從0開始)
      return _buildProgressIndicator();
    } else if (!control.needHeader && control.dataList.length == 0) {
      ///如果不需要頭部,并且數(shù)據(jù)為0膀曾,渲染空頁面
      return _buildEmpty();
    } else {
      ///回調(diào)外部正常渲染Item县爬,如果這里有需要,可以直接返回相對位置的index
      return itemBuilder(context, index);
    }
  }

  @override
  Widget build(BuildContext context) {
    return new RefreshIndicator(
      ///GlobalKey添谊,用戶外部獲取RefreshIndicator的State财喳,做顯示刷新
      key: refreshKey,
      ///下拉刷新觸發(fā),返回的是一個Future
      onRefresh: onRefresh,
      child: new ListView.builder(
        ///保持ListView任何情況都能滾動斩狱,解決在RefreshIndicator的兼容問題耳高。
        physics: const AlwaysScrollableScrollPhysics(),
        ///根據(jù)狀態(tài)返回子孔健
        itemBuilder: (context, index) {
          return _getItem(index);
        },
        ///根據(jù)狀態(tài)返回數(shù)量
        itemCount: _getListCount(),
        ///滑動監(jiān)聽
        controller: _scrollController,
      ),
    );
  }
  
  ///空頁面
  Widget _buildEmpty() {
     ///···
  }

  ///上拉加載更多
  Widget _buildProgressIndicator() {
     ///···
  }
}
效果如圖

3、Loading框

在上一小節(jié)中所踊,我們實現(xiàn)上滑加載更多的效果祝高,其中就需要展示 Loading 狀態(tài)的需求。默認(rèn)系統(tǒng)提供了CircularProgressIndicator等污筷,但是有追求的我們怎么可能局限于此,這里推薦一個第三方 Loading 庫 :flutter_spinkit 乍赫,通過簡單的配置就可以使用豐富的 Loading 樣式瓣蛀。

繼續(xù)上一小節(jié)中的 _buildProgressIndicator方法實現(xiàn),通過 flutter_spinkit 可以快速實現(xiàn)更不一樣的 Loading 樣式雷厂。

 ///上拉加載更多
  Widget _buildProgressIndicator() {
    ///是否需要顯示上拉加載更多的loading
    Widget bottomWidget = (control.needLoadMore)
        ? new Row(mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[
            ///loading框
            new SpinKitRotatingCircle(color: Color(0xFF24292E)),
            new Container(
              width: 5.0,
            ),
            ///加載中文本
            new Text(
              "加載中···",
              style: TextStyle(
                color: Color(0xFF121917),
                fontSize: 14.0,
                fontWeight: FontWeight.bold,
              ),
            )
          ])
          /// 不需要加載
        : new Container();
    return new Padding(
      padding: const EdgeInsets.all(20.0),
      child: new Center(
        child: bottomWidget,
      ),
    );
  }
loading樣式

4惋增、矢量圖標(biāo)庫

矢量圖標(biāo)對筆者是必不可少的,比起一般的 png 圖片文件改鲫,矢量圖標(biāo)在開發(fā)過程中:可以輕松定義顏色诈皿,并且任意調(diào)整大小不模糊。矢量圖標(biāo)庫是引入 ttf 字體庫文件實現(xiàn)像棘,在 Flutter 中通過 Icon 控件稽亏,加載對應(yīng)的 IconData 顯示即可。

Flutter 中默認(rèn)內(nèi)置的 Icons 類就提供了豐富的圖標(biāo)缕题,直接通過 Icons 對象即可使用截歉,同時個人推薦阿里爸爸的 iconfont 。如下代碼烟零,通過在 pubspec.yaml 中添加字體庫支持瘪松,然后在代碼中創(chuàng)建 IconData 指向字體庫名稱引用即可。

  fonts:
    - family: wxcIconFont
      fonts:
        - asset: static/font/iconfont.ttf

··················
          ///使用Icons
          new Tab(
            child: new Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: <Widget>[new Icon(Icons.list, size: 16.0), new Text("趨勢")],
            ),
          ),
         ///使用iconfont
          new Tab(
            child: new Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: <Widget>[new Icon(IconData(0xe6d0, fontFamily: "wxcIconFont"), size: 16.0), new Text("我的")],
            ),
          )

5锨阿、路由跳轉(zhuǎn)

Flutter 中的頁面跳轉(zhuǎn)是通過 Navigator 實現(xiàn)的宵睦,路由跳轉(zhuǎn)又分為:帶參數(shù)跳轉(zhuǎn)和不帶參數(shù)跳轉(zhuǎn)。不帶參數(shù)跳轉(zhuǎn)比較簡單墅诡,默認(rèn)可以通過 MaterialApp 的路由表跳轉(zhuǎn)壳嚎;而帶參數(shù)的跳轉(zhuǎn),參數(shù)通過跳轉(zhuǎn)頁面的構(gòu)造方法傳遞。常用的跳轉(zhuǎn)有如下幾種使用:

新版本開始可以給 pushNamed 設(shè)置 arguments 參數(shù)诬辈,然后在新頁面通過 ModalRoute.of(context).settings.arguments 獲取酵使。

///不帶參數(shù)的路由表跳轉(zhuǎn)
Navigator.pushNamed(context, routeName);

///跳轉(zhuǎn)新頁面并且替換,比如登錄頁跳轉(zhuǎn)主頁
Navigator.pushReplacementNamed(context, routeName);

///跳轉(zhuǎn)到新的路由焙糟,并且關(guān)閉給定路由的之前的所有頁面
Navigator.pushNamedAndRemoveUntil(context, '/calendar', ModalRoute.withName('/'));

///帶參數(shù)的路由跳轉(zhuǎn)口渔,并且監(jiān)聽返回
Navigator.push(context, new MaterialPageRoute(builder: (context) => new NotifyPage())).then((res) {
      ///獲取返回處理
    });

同時我們可以看到,Navigator 的 push 返回的是一個 Future穿撮,這個Future 的作用是在頁面返回時被調(diào)用的缺脉。也就是你可以通過 Navigatorpop 時返回參數(shù),之后在 Future 中可以的監(jiān)聽中處理頁面的返回結(jié)果悦穿。

@optionalTypeArgs
static Future<T> push<T extends Object>(BuildContext context, Route<T> route) {
  return Navigator.of(context).push(route);
}
中場休息

二攻礼、數(shù)據(jù)模塊

數(shù)據(jù)為王,不過應(yīng)該不是隔壁老王吧栗柒。

1礁扮、網(wǎng)絡(luò)請求

當(dāng)前 Flutter 網(wǎng)絡(luò)請求封裝中,國內(nèi)最受歡迎的就是 Dio 了瞬沦,Dio 封裝了網(wǎng)絡(luò)請求中的數(shù)據(jù)轉(zhuǎn)換太伊、攔截器、請求返回等逛钻。如下代碼所示僚焦,通過對 Dio 的簡單封裝即可快速網(wǎng)絡(luò)請求,真的很簡單曙痘,更多的可以查 Dio 的官方文檔芳悲,這里就不展開了。

    ///創(chuàng)建網(wǎng)絡(luò)請求對象边坤,主要最好吧 dio 實例全局單里
    Dio dio = new Dio();
    Response response;
    try {
      ///發(fā)起請求
      ///url地址名扛,請求數(shù)據(jù),一般為Map或者FormData
      ///options 額外配置茧痒,可以配置超時罢洲,頭部,請求類型文黎,數(shù)據(jù)響應(yīng)類型惹苗,host等
      response = await dio.request(url, data: params, options: option);
    } on DioError catch (e) {
      ///http錯誤是通過 DioError 的catch返回的一個對象
    }

2、Json序列化

在 Flutter 中耸峭,json 序列化是有些特殊的桩蓉,不同與 JS ,比如使用上述 Dio 網(wǎng)絡(luò)請求返回劳闹,如果配置了返回數(shù)據(jù)格式為 json 院究,實際上的到會是一個Map洽瞬。而 Map 的 key-value 使用,在開發(fā)過程中并不是很方便业汰,所以你需要對Map 再進(jìn)行一次轉(zhuǎn)化雹仿,轉(zhuǎn)為實際的 Model 實體疗锐。

所以 json_serializable 插件誕生了, 中文網(wǎng)Json 對其已有一段教程,這里主要補充說明下具體的使用邏輯吭从。

dependencies:
  # Your other regular dependencies here
  json_annotation: ^0.2.2

dev_dependencies:
  # Your other dev_dependencies here
  build_runner: ^0.7.6
  json_serializable: ^0.3.2

如下發(fā)代碼所示:

  • 創(chuàng)建你的實體 Model 之后腹侣,繼承 Object 磷箕、然后通過 @JsonSerializable() 標(biāo)記類名串结。

  • 通過 with _$TemplateSerializerMixin,將 fromJson 方法委托到 Template.g.dart 的實現(xiàn)中跪妥。 其中 *.g.dart鞋喇、_$* SerializerMixin_$*FromJson 這三個的引入眉撵, 和 Model 所在的 dart 的文件名與 Model 類名有關(guān)侦香,具體可見代碼注釋和后面圖片。

  • 最后通過 flutter packages pub run build_runner build 編譯自動生成轉(zhuǎn)化對象纽疟。(個人習(xí)慣完成后手動編譯)

import 'package:json_annotation/json_annotation.dart';

///關(guān)聯(lián)文件鄙皇、允許Template訪問 Template.g.dart 中的私有方法
///Template.g.dart 是通過命令生成的文件。名稱為 xx.g.dart仰挣,其中 xx 為當(dāng)前 dart 文件名稱
///Template.g.dart中創(chuàng)建了抽象類_$TemplateSerializerMixin,實現(xiàn)了_$TemplateFromJson方法
part 'Template.g.dart';

///標(biāo)志class需要實現(xiàn)json序列化功能
@JsonSerializable()

///'xx.g.dart'文件中缠沈,默認(rèn)會根據(jù)當(dāng)前類名如 AA 生成 _$AASerializerMixin
///所以當(dāng)前類名為Template膘壶,生成的抽象類為 _$TemplateSerializerMixin
class Template extends Object with _$TemplateSerializerMixin {

  String name;

  int id;

  ///通過JsonKey重新定義參數(shù)名
  @JsonKey(name: "push_id")
  int pushId;

  Template(this.name, this.id, this.pushId);

  ///'xx.g.dart'文件中,默認(rèn)會根據(jù)當(dāng)前類名如 AA 生成 _$AAeFromJson方法
  ///所以當(dāng)前類名為Template洲愤,生成的抽象類為 _$TemplateFromJson
  factory Template.fromJson(Map<String, dynamic> json) => _$TemplateFromJson(json);
}

序列化源碼部分

上述操作生成后的 Template.g.dart 下的代碼如下颓芭,這樣我們就可以通過 Template.fromJsontoJson 方法對實體與map進(jìn)行轉(zhuǎn)化,再結(jié)合json.decodejson.encode柬赐,你就可以愉悅的在string 亡问、map、實體間相互轉(zhuǎn)化了肛宋。


part of 'Template.dart';

Template _$TemplateFromJson(Map<String, dynamic> json) => new Template(
    json['name'] as String, json['id'] as int, json['push_id'] as int);

abstract class _$TemplateSerializerMixin {
  String get name;
  int get id;
  int get pushId;
  Map<String, dynamic> toJson() =>
      <String, dynamic>{'name': name, 'id': id, 'push_id': pushId};
}

注意:新版json序列化中做了部分修改州藕,代碼更簡單了,詳見demo 酝陈。

3床玻、Redux

相信在前端領(lǐng)域、Redux 并不是一個陌生的概念沉帮,作為全局狀態(tài)管理機锈死,用于 Flutter 中再合適不過贫堰。如果你沒聽說過,Don't worry待牵,簡單來說就是:它可以跨控件管理其屏、同步State 。所以 flutter_redux 等著你征服它缨该。

大家都知道在 Flutter 中 偎行,是通過實現(xiàn) StatesetState 來渲染和改變 StatefulWidget 的,如果使用了flutter_redux 會有怎樣的效果压彭?

比如把用戶信息存儲在 reduxstore 中睦优, 好處在于: 比如某個頁面修改了當(dāng)前用戶信息,所有綁定的該 State 的控件將由 Redux 自動同步修改壮不,State 可以跨頁面共享汗盘。

更多 Redux 的詳細(xì)就不再展開,后續(xù)會有詳細(xì)介紹询一,接下來我們講講 flutter_redux 的使用隐孽,在 redux 中主要引入了 action、reducer健蕊、store 概念菱阵。

  • action 用于定義一個數(shù)據(jù)變化的請求。
  • reducer 用于根據(jù) action 產(chǎn)生新狀態(tài)
  • store 用于存儲和管理 state缩功,監(jiān)聽 action晴及,將 action 自動分配給 reducer 并根據(jù) reducer 的執(zhí)行結(jié)果更新 state。

? 所以如下代碼嫡锌,我們先創(chuàng)建一個 State 用于存儲需要保存的對象虑稼,其中關(guān)鍵代碼在于 UserReducer

///全局Redux store 的對象势木,保存State數(shù)據(jù)
class GSYState {
  ///用戶信息
  User userInfo;
  ///構(gòu)造方法
  GSYState({this.userInfo});

}

///通過 Reducer 創(chuàng)建 用于store 的 Reducer
GSYState appReducer(GSYState state, action) {
  return GSYState(
    ///通過 UserReducer 將 GSYState 內(nèi)的 userInfo 和 action 關(guān)聯(lián)在一起
    userInfo: UserReducer(state.userInfo, action),
  );
}

下面是上方使用的 UserReducer 的實現(xiàn)蛛倦,這里主要通過 TypedReducer 將 reducer 的處理邏輯與定義的 Action 綁定,最后通過 combineReducers 返回 Reducer<State> 對象應(yīng)用于上方 Store 中啦桌。

/// redux 的 combineReducers, 通過 TypedReducer 將 UpdateUserAction 與 reducers 關(guān)聯(lián)起來
final UserReducer = combineReducers<User>([
  TypedReducer<User, UpdateUserAction>(_updateLoaded),
]);

/// 如果有 UpdateUserAction 發(fā)起一個請求時
/// 就會調(diào)用到 _updateLoaded
/// _updateLoaded 這里接受一個新的userInfo溯壶,并返回
User _updateLoaded(User user, action) {
  user = action.userInfo;
  return user;
}

///定一個 UpdateUserAction ,用于發(fā)起 userInfo 的的改變
///類名隨你喜歡定義甫男,只要通過上面TypedReducer綁定就好
class UpdateUserAction {
  final User userInfo;
  UpdateUserAction(this.userInfo);
}

下面正式在 Flutter 中引入 store且改,通過 StoreProvider 將創(chuàng)建 的 store 引用到 Flutter 中。

void main() {
  runApp(new FlutterReduxApp());
}

class FlutterReduxApp extends StatelessWidget {

  /// 創(chuàng)建Store板驳,引用 GSYState 中的 appReducer 創(chuàng)建的 Reducer
  /// initialState 初始化 State
  final store = new Store<GSYState>(appReducer, initialState: new GSYState(userInfo: User.empty()));

  FlutterReduxApp({Key key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    /// 通過 StoreProvider 應(yīng)用 store
    return new StoreProvider(
      store: store,
      child: new MaterialApp(
        home: DemoUseStorePage(),
      ),
    );
  }
}

在下方 DemoUseStorePage 中钾虐,通過 StoreConnector 將State 綁定到 Widget;通過 StoreProvider.of 可以獲取 state 對象笋庄;通過 dispatch 一個 Action 可以更新State效扫。

class DemoUseStorePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    ///通過 StoreConnector 關(guān)聯(lián) GSYState 中的 User
    return new StoreConnector<GSYState, User>(
      ///通過 converter 將 GSYState 中的 userInfo返回
      converter: (store) => store.state.userInfo,
      ///在 userInfo 中返回實際渲染的控件
      builder: (context, userInfo) {
        return new Text(
          userInfo.name,
          style: Theme.of(context).textTheme.display1,
        );
      },
    );
  }
}

·····
///通過 StoreProvider.of(context) (帶有 StoreProvider 下的 context)
/// 可以任意的位置訪問到 state 中的數(shù)據(jù)
StoreProvider.of(context).state.userInfo;

·····
///通過 dispatch UpdateUserAction倔监,可以更新State
StoreProvider.of(context).dispatch(new UpdateUserAction(newUserInfo));

看到這是不是有點想靜靜了?先不管靜靜是誰菌仁,但是Redux的實用性是應(yīng)該比靜靜更吸引人浩习,作為一個有追求的程序猿,多動手?jǐn)]擼還有什么拿不下的山頭是不济丘?更詳細(xì)的實現(xiàn)請看:GSYGithubAppFlutter 谱秽。

4、數(shù)據(jù)庫

在 GSYGithubAppFlutter 中摹迷,數(shù)據(jù)庫使用的是 sqflite 的封裝疟赊,其實就是 sqlite 語法的使用而已,有興趣的可以看看完整代碼 DemoDb.dart 峡碉。 這里主要提供一種思路近哟,按照 sqflite 文檔提供的方法,重新做了一小些修改鲫寄,通過定義 Provider 操作數(shù)據(jù)庫:

  • 在 Provider 中定義表名數(shù)據(jù)庫字段常量吉执,用于創(chuàng)建表與字段操作;

  • 提供數(shù)據(jù)庫與數(shù)據(jù)實體之間的映射地来,比如數(shù)據(jù)庫對象與User對象之間的轉(zhuǎn)化戳玫;

  • 在調(diào)用 Provider 時才先判斷表是否創(chuàng)建,然后再返回數(shù)據(jù)庫對象進(jìn)行用戶查詢未斑。

如果結(jié)合網(wǎng)絡(luò)請求咕宿,通過閉包實現(xiàn),在需要數(shù)據(jù)庫時先返回數(shù)據(jù)庫蜡秽,然后通過 next 方法將網(wǎng)絡(luò)請求的方法返回府阀,最后外部可以通過調(diào)用next方法再執(zhí)行網(wǎng)絡(luò)請求。如下所示:

    UserDao.getUserInfo(userName, needDb: true).then((res) {
      ///數(shù)據(jù)庫結(jié)果
      if (res != null && res.result) {
        setState(() {
          userInfo = res.data;
        });
      }
      return res.next;
    }).then((res) {
      ///網(wǎng)絡(luò)結(jié)果
      if (res != null && res.result) {
        setState(() {
          userInfo = res.data;
        });
      }
    });   

三载城、其他功能

其他功能,只是因為想不到標(biāo)題费就。

1诉瓦、返回按鍵監(jiān)聽

Flutter 中 ,通過WillPopScope 嵌套力细,可以用于監(jiān)聽處理 Android 返回鍵的邏輯睬澡。其實 WillPopScope 并不是監(jiān)聽返回按鍵,如名字一般眠蚂,是當(dāng)前頁面將要被pop時觸發(fā)的回調(diào)煞聪。

通過onWillPop回調(diào)返回的Future,判斷是否響應(yīng) pop 逝慧。下方代碼實現(xiàn)按下返回鍵時昔脯,彈出提示框啄糙,按下確定退出App。

class HomePage extends StatelessWidget {
  /// 單擊提示退出
  Future<bool> _dialogExitApp(BuildContext context) {
    return showDialog(
        context: context,
        builder: (context) => new AlertDialog(
              content: new Text("是否退出"),
              actions: <Widget>[
                new FlatButton(onPressed: () => Navigator.of(context).pop(false), child:  new Text("取消")),
                new FlatButton(
                    onPressed: () {
                      Navigator.of(context).pop(true);
                    },
                    child: new Text("確定"))
              ],
            ));
  }

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return WillPopScope(
      onWillPop: () {
        ///如果返回 return new Future.value(false); popped 就不會被處理
        ///如果返回 return new Future.value(true); popped 就會觸發(fā)
        ///這里可以通過 showDialog 彈出確定框云稚,在返回時通過 Navigator.of(context).pop(true);決定是否退出
        return _dialogExitApp(context);
      },
      child: new Container(),
    );
  }
}

2隧饼、前后臺監(jiān)聽

WidgetsBindingObserver 包含了各種控件的生命周期通知,其中的 didChangeAppLifecycleState 就可以用于做前后臺狀態(tài)監(jiān)聽静陈。

/// WidgetsBindingObserver 包含了各種控件的生命周期通知
class _HomePageState extends State<HomePage> with WidgetsBindingObserver {

  ///重寫 WidgetsBindingObserver 中的 didChangeAppLifecycleState
  @override
  void didChangeAppLifecycleState(AppLifecycleState state) {
    ///通過state判斷App前后臺切換
    if (state == AppLifecycleState.resumed) {

    }
  }

  @override
  Widget build(BuildContext context) {
    return new Container();
  }
}

3燕雁、鍵盤焦點處理

一般觸摸收起鍵盤也是常見需求,如下代碼所示鲸拥, GestureDetector + FocusScope 可以滿足這一需求拐格。

class _LoginPageState extends State<LoginPage> {
  @override
  Widget build(BuildContext context) {
      ///定義觸摸層
      return new GestureDetector(
        ///透明也響應(yīng)處理
        behavior: HitTestBehavior.translucent,
        onTap: () {
          ///觸摸手氣鍵盤
          FocusScope.of(context).requestFocus(new FocusNode());
        },
        child: new Container(
        ),
      );
  }
}

4、啟動頁

IOS啟動頁刑赶,在ios/Runner/Assets.xcassets/LaunchImage.imageset/下捏浊, 有 Contents.json 文件和啟動圖片,將你的啟動頁放置在這個目錄下角撞,并且修改 Contents.json 即可呛伴,具體尺寸自行谷歌即可。

Android啟動頁谒所,在 android/app/src/main/res/drawable/launch_background.xml 中已經(jīng)有寫好的啟動頁热康,<item><bitmap> 部分被屏蔽,只需要打開這個屏蔽劣领,并且將你啟動圖修改為launch_image并放置到各個 mipmap 文件夾即可姐军,記得各個文件夾下提供相對于大小尺寸的文件。

自此尖淘,第二篇終于結(jié)束了奕锌!(//////)

資源推薦

完整開源項目推薦:

圖片發(fā)自簡書App
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市村生,隨后出現(xiàn)的幾起案子惊暴,更是在濱河造成了極大的恐慌,老刑警劉巖趁桃,帶你破解...
    沈念sama閱讀 216,591評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件辽话,死亡現(xiàn)場離奇詭異,居然都是意外死亡卫病,警方通過查閱死者的電腦和手機油啤,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,448評論 3 392
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來蟀苛,“玉大人益咬,你說我怎么就攤上這事≈钠剑” “怎么了幽告?”我有些...
    開封第一講書人閱讀 162,823評論 0 353
  • 文/不壞的土叔 我叫張陵梅鹦,是天一觀的道長。 經(jīng)常有香客問我评腺,道長帘瞭,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,204評論 1 292
  • 正文 為了忘掉前任蒿讥,我火速辦了婚禮蝶念,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘芋绸。我一直安慰自己媒殉,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,228評論 6 388
  • 文/花漫 我一把揭開白布摔敛。 她就那樣靜靜地躺著廷蓉,像睡著了一般。 火紅的嫁衣襯著肌膚如雪马昙。 梳的紋絲不亂的頭發(fā)上桃犬,一...
    開封第一講書人閱讀 51,190評論 1 299
  • 那天,我揣著相機與錄音行楞,去河邊找鬼攒暇。 笑死,一個胖子當(dāng)著我的面吹牛子房,可吹牛的內(nèi)容都是我干的形用。 我是一名探鬼主播,決...
    沈念sama閱讀 40,078評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼证杭,長吁一口氣:“原來是場噩夢啊……” “哼田度!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起解愤,我...
    開封第一講書人閱讀 38,923評論 0 274
  • 序言:老撾萬榮一對情侶失蹤镇饺,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后送讲,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體奸笤,經(jīng)...
    沈念sama閱讀 45,334評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,550評論 2 333
  • 正文 我和宋清朗相戀三年李茫,在試婚紗的時候發(fā)現(xiàn)自己被綠了揭保。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片肥橙。...
    茶點故事閱讀 39,727評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡魄宏,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出存筏,到底是詐尸還是另有隱情宠互,我是刑警寧澤味榛,帶...
    沈念sama閱讀 35,428評論 5 343
  • 正文 年R本政府宣布,位于F島的核電站予跌,受9級特大地震影響搏色,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜券册,卻給世界環(huán)境...
    茶點故事閱讀 41,022評論 3 326
  • 文/蒙蒙 一频轿、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧烁焙,春花似錦航邢、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,672評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至九火,卻和暖如春赚窃,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背岔激。 一陣腳步聲響...
    開封第一講書人閱讀 32,826評論 1 269
  • 我被黑心中介騙來泰國打工勒极, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人鹦倚。 一個月前我還...
    沈念sama閱讀 47,734評論 2 368
  • 正文 我出身青樓河质,卻偏偏與公主長得像,于是被迫代替她去往敵國和親震叙。 傳聞我的和親對象是個殘疾皇子掀鹅,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,619評論 2 354

推薦閱讀更多精彩內(nèi)容