Flutter完整開發實戰詳解(二、快速開發實戰篇)

carguo發表於2018-08-22

 作為系列文章的第二篇,繼《Flutter完整開發實戰詳解(一、Dart語言和Flutter基礎)》之後,本篇將為你著重展示:如何搭建一個通用的Flutter App 常用功能腳手架,快速開發一個完整的 Flutter 應用

 友情提示:本文所有程式碼均在 GSYGithubAppFlutter ,文中示例程式碼均可在其中找到,看完本篇相信你應該可以輕鬆完成如下效果。相關基礎還請看篇章一

我們的目標是!( ̄^ ̄)ゞ

前言

 本篇內容結構如下圖,主要分為: 基礎控制元件、資料模組、其他功能 三部分。每大塊中的小模組,除了涉及的功能實現外,對於實現過程中筆者遇到的問題,會一併展開闡述。本系列的最終目的是: 讓你感受 Flutter 的愉悅! 那麼就讓我們愉悅的往下開始吧!(◐‿◑)

我是簡陋的下圖

一、基礎控制元件

所謂的基礎,大概就是砍柴功了吧!

1、Tabbar控制元件實現

 Tabbar 頁面是常有需求,而在Flutter中: Scaffold + AppBar + Tabbar + TabbarView 是 Tabbar 頁面的最簡單實現,但在加上 AutomaticKeepAliveClientMixin 用於頁面 keepAlive 之後,諸如#11895的問題便開始成為Crash的元凶。直到 flutter v0.5.7 sdk 版本修復後,問題依舊沒有完全解決,所以無奈最終修改了實現方案。

 目前筆者是通過 Scaffold + Appbar + Tabbar + PageView 來組合實現效果,從而解決上述問題。因為該問題較為常見,所以目前已經單獨實現了測試Demo,有興趣的可以看看 TabBarWithPageView

 下面我們直接程式碼走起,首先作為一個Tabbar Widget,它肯定是一個 StatefulWidget ,所以我們先實現它的 State

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

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

    @override
    Widget build(BuildContext context) {
      ///底部TAbBar模式
      return new Scaffold(
          ///設定側邊滑出 drawer,不需要可以不設定
          drawer: _drawer,
          ///設定懸浮按鍵,不需要可以不設定
          floatingActionButton: _floatingActionButton,
          ///標題欄
          appBar: new AppBar(
            backgroundColor: _backgroundColor,
            title: _title,
          ),
          ///頁面主體,PageView,用於承載Tab對應的頁面
          body: new PageView(
            ///必須有的控制器,與tabBar的控制器同步
            controller: _pageController,
            ///每一個 tab 對應的頁面主體,是一個List<Widget>
            children: _tabViews,
            onPageChanged: (index) {
              ///頁面觸控作用滑動回撥,用於同步tab選中狀態
              _tabController.animateTo(index);
            },
          ),
          ///底部導航欄,也就是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 實現 Tab 和頁面的同步,通過 SingleTickerProviderStateMixin 實現 Tab 的動畫切換效果 (ps 如果有需要多個巢狀動畫效果,你可能需要TickerProviderStateMixin)。 從程式碼中我們可以看到:

  • 手動左右滑動 PageView 時,通過 onPageChanged 回撥呼叫 _tabController.animateTo(index); 同步TabBar狀態。
  • _tabItems 中,監聽每個 TabBarItem 的點選,通過 _pageController 實現PageView的狀態同步。

  而上面程式碼還缺少了 TabBarItem 的點選,因為這塊被放到了外部實現。當然你也可以直接在內部封裝好控制元件,直接傳遞配置資料顯示,這個可以根據個人需要封裝。

  外部呼叫程式碼如下:每個 Tabbar 點選時,通過pageController.jumpTo 跳轉頁面,每個頁面需要跳轉座標為:當前螢幕大小乘以索引 index

class _TabBarBottomPageWidgetState extends State<TabBarBottomPageWidget> {

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

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

  ///渲染Tab 對應頁面
  _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"));
  }
}

  如果到此結束,你會發現頁面點選切換時,StatefulWidget 的子頁面每次都會重新呼叫initState。這肯定不是我們想要的,所以這時你就需要AutomaticKeepAliveClientMixin

  每個 Tab 對應的 StatefulWidget 的 State ,需要通過 with AutomaticKeepAliveClientMixin ,然後重寫 @override bool get wantKeepAlive => true; ,就可以實不重新構建的效果了,效果如下圖。

頁面效果

  既然底部Tab頁面都實現了,乾脆頂部tab頁面也一起完成。如下程式碼,和底部Tab頁的區別在於:

  • 底部tab是放在了 ScaffoldbottomNavigationBar 中。
  • 頂部tab是放在 AppBarbottom 中,也就是標題欄之下。

  同時我們在頂部 TabBar 增加 isScrollable: true 屬性,實現常見的頂部Tab的效果,如下方圖片所示。

    return new Scaffold(
        ///設定側邊滑出 drawer,不需要可以不設定
        drawer: _drawer,
        ///設定懸浮按鍵,不需要可以不設定
        floatingActionButton: _floatingActionButton,
        ///標題欄
        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對應的頁面
        body: new PageView(
          ///必須有的控制器,與tabBar的控制器同步
          controller: _pageController,
          ///每一個 tab 對應的頁面主體,是一個List<Widget>
          children: _tabViews,
          ///頁面觸控作用滑動回撥,用於同步tab選中狀態
          onPageChanged: (index) {
            _tabController.animateTo(index);
          },
        ),
      );

頂部TabBar效果

  在 TabBar 頁面中,一般還會出現:父頁面需要控制 PageView 中子頁的需求。這時候就需要用到GlobalKey了。比如 GlobalKey<PageOneState> stateOne = new GlobalKey<PageOneState>(); ,通過 globalKey.currentState 物件,你就可以呼叫到 PageOneState 中的公開方法。這裡需要注意 GlobalKey 需要全域性唯一,一般可以在build 方法中建立。

2、上下重新整理列表

毫無爭議,必備控制元件。Flutter 中 為我們提供了 RefreshIndicator 作為內建下拉重新整理控制元件;同時我們通過給 ListView 新增 ScrollController 做滑動監聽,在最後增加一個 Item, 作為上滑載入更多的 Loading 顯示。

  如下程式碼所示,通過 RefreshIndicator 控制元件可以簡單完成下拉重新整理工作。這裡需要注意一點是:可以利用 GlobalKey<RefreshIndicatorState> 對外提供 RefreshIndicatorRefreshIndicatorState,這樣外部就 可以通過 GlobalKey 呼叫 globalKey.currentState.show(); ,主動顯示重新整理狀態並觸發 onRefresh

上拉載入更多在程式碼中是通過 _getListCount() 方法,在原本的資料基礎上,增加實際需要渲染的 item 數量給 ListView 實現的,最後通過 ScrollController 監聽到底部,觸發 onLoadMore

  如下程式碼所示,通過 _getListCount() 方法,還可以配置空頁面,頭部等常用效果。其實就是在內部通過改變實際item數量與渲染Item,以實現更多配置效果

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

  @override
  void initState() {
    ///增加滑動監聽
    _scrollController.addListener(() {
      ///判斷當前滑動位置是不是到達底部,觸發載入更多回撥
      if (_scrollController.position.pixels == _scrollController.position.maxScrollExtent) {
        if (this.onLoadMore != null && this.control.needLoadMore) {
          this.onLoadMore();
        }
      }
    });
    super.initState();
  }

  ///根據配置狀態返回實際列表數量
  ///實際上這裡可以根據你的需要做更多的處理
  ///比如多個頭部,是否需要空頁面,是否需要顯示載入更多。
  _getListCount() {
    ///是否需要頭部
    if (control.needHeader) {
      ///如果需要頭部,用Item 0 的 Widget 作為ListView的頭部
      ///列表數量大於0時,因為頭部和底部載入更多選項,需要對列表資料總數+2
      return (control.dataList.length > 0) ? control.dataList.length + 2 : control.dataList.length + 1;
    } else {
      ///如果不需要頭部,在沒有資料時,固定返回數量1用於空頁面呈現
      if (control.dataList.length == 0) {
        return 1;
      }

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

  ///根據配置狀態返回實際列表渲染Item
  _getItem(int index) {
    if (!control.needHeader && index == control.dataList.length && control.dataList.length != 0) {
      ///如果不需要頭部,並且資料不為0,當index等於資料長度時,渲染載入更多Item(因為index是從0開始)
      return _buildProgressIndicator();
    } else if (control.needHeader && index == _getListCount() - 1 && control.dataList.length != 0) {
      ///如果需要頭部,並且資料不為0,當index等於實際渲染長度 - 1時,渲染載入更多Item(因為index是從0開始)
      return _buildProgressIndicator();
    } else if (!control.needHeader && control.dataList.length == 0) {
      ///如果不需要頭部,並且資料為0,渲染空頁面
      return _buildEmpty();
    } else {
      ///回撥外部正常渲染Item,如果這裡有需要,可以直接返回相對位置的index
      return itemBuilder(context, index);
    }
  }

  @override
  Widget build(BuildContext context) {
    return new RefreshIndicator(
      ///GlobalKey,使用者外部獲取RefreshIndicator的State,做顯示重新整理
      key: refreshKey,
      ///下拉重新整理觸發,返回的是一個Future
      onRefresh: onRefresh,
      child: new ListView.builder(
        ///保持ListView任何情況都能滾動,解決在RefreshIndicator的相容問題。
        physics: const AlwaysScrollableScrollPhysics(),
        ///根據狀態返回子孔健
        itemBuilder: (context, index) {
          return _getItem(index);
        },
        ///根據狀態返回數量
        itemCount: _getListCount(),
        ///滑動監聽
        controller: _scrollController,
      ),
    );
  }
  
  ///空頁面
  Widget _buildEmpty() {
     ///···
  }

  ///上拉載入更多
  Widget _buildProgressIndicator() {
     ///···
  }
}

效果如圖

3、Loading框

  在上一小節中,我們實現上滑載入更多的效果,其中就需要展示 Loading 狀態的需求。預設系統提供了CircularProgressIndicator等,但是有追求的我們怎麼可能侷限於此,這裡推薦一個第三方 Loading 庫 :flutter_spinkit ,通過簡單的配置就可以使用豐富的 Loading 樣式。

  繼續上一小節中的 _buildProgressIndicator 方法實現,通過 flutter_spinkit 可以快速實現更不一樣的 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、向量圖示庫

向量圖示對筆者是必不可少的。比起一般的 png 圖片檔案,向量圖示在開發過程中:可以輕鬆定義顏色,並且任意調整大小不模糊。向量圖示庫是引入 ttf 字型庫檔案實現,在 Flutter 中通過 Icon 控制元件,載入對應的 IconData 顯示即可。

  Flutter 中預設內建的 Icons 類就提供了豐富的圖示,直接通過 Icons 物件即可使用,同時個人推薦阿里爸爸的 iconfont 。如下程式碼,通過在 pubspec.yaml 中新增字型庫支援,然後在程式碼中建立 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、路由跳轉

  Flutter 中的頁面跳轉是通過 Navigator 實現的,路由跳轉又分為:帶引數跳轉和不帶引數跳轉。不帶引數跳轉比較簡單,預設可以通過 MaterialApp 的路由表跳轉;而帶引數的跳轉,引數通過跳轉頁面的構造方法傳遞。常用的跳轉有如下幾種使用:

///不帶引數的路由表跳轉
Navigator.pushNamed(context, routeName);

///跳轉新頁面並且替換,比如登入頁跳轉主頁
Navigator.pushReplacementNamed(context, routeName);

///跳轉到新的路由,並且關閉給定路由的之前的所有頁面
Navigator.pushNamedAndRemoveUntil(context, `/calendar`, ModalRoute.withName(`/`));

///帶引數的路由跳轉,並且監聽返回
Navigator.push(context, new MaterialPageRoute(builder: (context) => new NotifyPage())).then((res) {
      ///獲取返回處理
    });

  同時我們可以看到,Navigator 的 push 返回的是一個 Future,這個Future 的作用是在頁面返回時被呼叫的。也就是你可以通過 Navigatorpop 時返回引數,之後在 Future 中可以的監聽中處理頁面的返回結果。

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

中場休息

二、資料模組

資料為王,不過應該不是隔壁老王吧。

1、網路請求

  當前 Flutter 網路請求封裝中,國內最受歡迎的就是 Dio 了,Dio 封裝了網路請求中的資料轉換、攔截器、請求返回等。如下程式碼所示,通過對 Dio 的簡單封裝即可快速網路請求,真的很簡單,更多的可以查 Dio 的官方文件,這裡就不展開了。(真的不是懶(˶‾᷄ ⁻̫ ‾᷅˵))

    ///建立網路請求物件
    Dio dio = new Dio();
    Response response;
    try {
      ///發起請求
      ///url地址,請求資料,一般為Map或者FormData
      ///options 額外配置,可以配置超時,頭部,請求型別,資料響應型別,host等
      response = await dio.request(url, data: params, options: option);
    } on DioError catch (e) {
      ///http錯誤是通過 DioError 的catch返回的一個物件
    }

2、Json序列化

  在 Flutter 中,json 序列化是有些特殊的。不同與 JS ,比如使用上述 Dio 網路請求返回,如果配置了返回資料格式為 json ,實際上的到會是一個Map。而 Map 的 key-value 使用,在開發過程中並不是很方便,所以你需要對Map 再進行一次轉化,轉為實際的 Model 實體。

  所以 json_serializable 外掛誕生了, 中文網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

  如下發程式碼所示:

  • 建立你的實體 Model 之後,繼承 Object 、然後通過 @JsonSerializable() 標記類名。
  • 通過 with _$TemplateSerializerMixin,將 fromJson 方法委託到 Template.g.dart 的實現中。 其中 *.g.dart_$* SerializerMixin_$*FromJson 這三個的引入, 和 Model 所在的 dart 的檔名與 Model 類名有關,具體可見程式碼註釋和後面圖片。
  • 最後通過 flutter packages pub run build_runner build 編譯自動生成轉化物件。(個人習慣完成後手動編譯)
import `package:json_annotation/json_annotation.dart`;

///關聯檔案、允許Template訪問 Template.g.dart 中的私有方法
///Template.g.dart 是通過命令生成的檔案。名稱為 xx.g.dart,其中 xx 為當前 dart 檔名稱
///Template.g.dart中建立了抽象類_$TemplateSerializerMixin,實現了_$TemplateFromJson方法
part `Template.g.dart`;

///標誌class需要實現json序列化功能
@JsonSerializable()

///`xx.g.dart`檔案中,預設會根據當前類名如 AA 生成 _$AASerializerMixin
///所以當前類名為Template,生成的抽象類為 _$TemplateSerializerMixin
class Template extends Object with _$TemplateSerializerMixin {

  String name;

  int id;

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

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

  ///`xx.g.dart`檔案中,預設會根據當前類名如 AA 生成 _$AAeFromJson方法
  ///所以當前類名為Template,生成的抽象類為 _$TemplateFromJson
  factory Template.fromJson(Map<String, dynamic> json) => _$TemplateFromJson(json);
}

序列化原始碼部分

  上述操作生成後的 Template.g.dart 下的程式碼如下,這樣我們就可以通過 Template.fromJson toJson 方法對實體與map進行轉化,再結合json.decodejson.encode,你就可以愉悅的在string 、map、實體間相互轉化了


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};
}

3、Redux State

  相信在前端領域、Redux 並不是一個陌生的概念。作為全域性狀態管理機,用於 Flutter 中再合適不過。如果你沒聽說過,Don`t worry,簡單來說就是:它可以跨控制元件管理、同步State 。所以 flutter_redux 等著你征服它。

  大家都知道在 Flutter 中 ,是通過實現 StatesetState 來渲染和改變 StatefulWidget 的。如果使用了flutter_redux 會有怎樣的效果?

 比如把使用者資訊儲存在 reduxstore 中, 好處在於: 比如某個頁面修改了當前使用者資訊,所有繫結的該 State 的控制元件將由 Redux 自動同步修改。State 可以跨頁面共享。

  更多 Redux 的詳細就不再展開,接下來我們講講 flutter_redux 的使用。在 redux 中主要引入了 action、reducer、store 概念。

  • action 用於定義一個資料變化的請求。
  • reducer 用於根據 action 產生新狀態
  • store 用於儲存和管理 state,監聽 action,將 action 自動分配給 reducer 並根據 reducer 的執行結果更新 state。

  所以如下程式碼,我們先建立一個 State 用於儲存需要儲存的物件,其中關鍵程式碼在於 UserReducer

///全域性Redux store 的物件,儲存State資料
class GSYState {
  ///使用者資訊
  User userInfo;
  ///構造方法
  GSYState({this.userInfo});

}

///通過 Reducer 建立 用於store 的 Reducer
GSYState appReducer(GSYState state, action) {
  return GSYState(
    ///通過 UserReducer 將 GSYState 內的 userInfo 和 action 關聯在一起
    userInfo: UserReducer(state.userInfo, action),
  );
}

  下面是上方使用的 UserReducer 的實現。這裡主要通過 TypedReducer 將 reducer 的處理邏輯與定義的 Action 繫結,最後通過 combineReducers 返回 Reducer<State> 物件應用於上方 Store 中。

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

/// 如果有 UpdateUserAction 發起一個請求時
/// 就會呼叫到 _updateLoaded
/// _updateLoaded 這裡接受一個新的userInfo,並返回
User _updateLoaded(User user, action) {
  user = action.userInfo;
  return user;
}

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

  下面正式在 Flutter 中引入 store,通過 StoreProvider 將建立 的 store 引用到 Flutter 中。

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

class FlutterReduxApp extends StatelessWidget {

  /// 建立Store,引用 GSYState 中的 appReducer 建立的 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 應用 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 關聯 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 中的資料
StoreProvider.of(context).state.userInfo;

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

  看到這是不是有點想靜靜了?先不管靜靜是誰,但是Redux的實用性是應該比靜靜更吸引人,作為一個有追求的程式猿,多動手擼擼還有什麼拿不下的山頭是不?更詳細的實現請看:GSYGithubAppFlutter

4、資料庫

  在 GSYGithubAppFlutter 中,資料庫使用的是 sqflite 的封裝,其實就是 sqlite 語法的使用而已,有興趣的可以看看完整程式碼 DemoDb.dart 。 這裡主要提供一種思路,按照 sqflite 文件提供的方法,重新做了一小些修改,通過定義 Provider 運算元據庫:

  • 在 Provider 中定義表名資料庫欄位常量,用於建立表與欄位操作;
  • 提供資料庫與資料實體之間的對映,比如資料庫物件與User物件之間的轉化;
  • 在呼叫 Provider 時才先判斷表是否建立,然後再返回資料庫物件進行使用者查詢。

  如果結合網路請求,通過閉包實現,在需要資料庫時先返回資料庫,然後通過 next 方法將網路請求的方法返回,最後外部可以通過呼叫next方法再執行網路請求。如下所示:

    UserDao.getUserInfo(userName, needDb: true).then((res) {
      ///資料庫結果
      if (res != null && res.result) {
        setState(() {
          userInfo = res.data;
        });
      }
      return res.next;
    }).then((res) {
      ///網路結果
      if (res != null && res.result) {
        setState(() {
          userInfo = res.data;
        });
      }
    });   

三、其他功能

其他功能,只是因為想不到標題。

1、返回按鍵監聽

  Flutter 中 ,通過WillPopScope 巢狀,可以用於監聽處理 Android 返回鍵的邏輯。其實 WillPopScope 並不是監聽返回按鍵,如名字一般,是當前頁面將要被pop時觸發的回撥。

  通過onWillPop 回撥返回的Future,判斷是否響應 pop 。下方程式碼實現按下返回鍵時,彈出提示框,按下確定退出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 就會觸發
        ///這裡可以通過 showDialog 彈出確定框,在返回時通過 Navigator.of(context).pop(true);決定是否退出
        return _dialogExitApp(context);
      },
      child: new Container(),
    );
  }
}

2、前後臺監聽

WidgetsBindingObserver 包含了各種控制元件的生命週期通知,其中的 didChangeAppLifecycleState 就可以用於做前後臺狀態監聽。

/// 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(
        ///透明也響應處理
        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 中已經有寫好的啟動頁,<item><bitmap> 部分被遮蔽,只需要開啟這個遮蔽,並且將你啟動圖修改為launch_image並放置到各個 mipmap 資料夾即可,記得各個資料夾下提供相對於大小尺寸的檔案。

自此,第二篇終於結束了!(///▽///)

資源推薦

完整開源專案推薦:

文章

《Flutter完整開發實戰詳解(一、Dart語言和Flutter基礎)》

《跨平臺專案開源專案推薦》

《移動端跨平臺開發的深度解析》

我們還會再見的


相關文章