• 前言
  • 一、基础控件
    • 1、Tabbar控件实现
    • 2、上下刷新列表
    • 3、Loading框
    • 4、矢量图标库
    • 5、路由跳转
  • 二、数据模块
    • 1、网络请求
    • 2、Json序列化
  • 注意:新版json序列化中做了部分修改,代码更简单了,详见demo
    • 3、Redux State
    • 4、数据库
  • 三、其他功能
    • 1、返回按键监听
    • 2、前后台监听
    • 3、键盘焦点处理
    • 4、启动页
  • 资源推荐
    • 完整开源项目推荐:
    • 文章

    作为系列文章的第二篇,继《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

    1. class _GSYTabBarState extends State<GSYTabBarWidget> with SingleTickerProviderStateMixin {
    2. ///···省略非关键代码
    3. @override
    4. void initState() {
    5. super.initState();
    6. ///初始化时创建控制器
    7. ///通过 with SingleTickerProviderStateMixin 实现动画效果。
    8. _tabController = new TabController(vsync: this, length: _tabItems.length);
    9. }
    10. @override
    11. void dispose() {
    12. ///页面销毁时,销毁控制器
    13. _tabController.dispose();
    14. super.dispose();
    15. }
    16. @override
    17. Widget build(BuildContext context) {
    18. ///底部TAbBar模式
    19. return new Scaffold(
    20. ///设置侧边滑出 drawer,不需要可以不设置
    21. drawer: _drawer,
    22. ///设置悬浮按键,不需要可以不设置
    23. floatingActionButton: _floatingActionButton,
    24. ///标题栏
    25. appBar: new AppBar(
    26. backgroundColor: _backgroundColor,
    27. title: _title,
    28. ),
    29. ///页面主体,PageView,用于承载Tab对应的页面
    30. body: new PageView(
    31. ///必须有的控制器,与tabBar的控制器同步
    32. controller: _pageController,
    33. ///每一个 tab 对应的页面主体,是一个List<Widget>
    34. children: _tabViews,
    35. onPageChanged: (index) {
    36. ///页面触摸作用滑动回调,用于同步tab选中状态
    37. _tabController.animateTo(index);
    38. },
    39. ),
    40. ///底部导航栏,也就是tab栏
    41. bottomNavigationBar: new Material(
    42. color: _backgroundColor,
    43. ///tabBar控件
    44. child: new TabBar(
    45. ///必须有的控制器,与pageView的控制器同步
    46. controller: _tabController,
    47. ///每一个tab item,是一个List<Widget>
    48. tabs: _tabItems,
    49. ///tab底部选中条颜色
    50. indicatorColor: _indicatorColor,
    51. ),
    52. ));
    53. }
    54. }

    如上代码所示,这是一个 底部 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

    1. class _TabBarBottomPageWidgetState extends State<TabBarBottomPageWidget> {
    2. final PageController pageController = new PageController();
    3. final List<String> tab = ["动态", "趋势", "我的"];
    4. ///渲染底部Tab
    5. _renderTab() {
    6. List<Widget> list = new List();
    7. for (int i = 0; i < tab.length; i++) {
    8. list.add(new FlatButton(onPressed: () {
    9. ///每个 Tabbar 点击时,通过jumpTo 跳转页面
    10. ///每个页面需要跳转坐标为:当前屏幕大小 * 索引index。
    11. topPageControl.jumpTo(MediaQuery
    12. .of(context)
    13. .size
    14. .width * i);
    15. }, child: new Text(
    16. tab[i],
    17. maxLines: 1,
    18. )));
    19. }
    20. return list;
    21. }
    22. ///渲染Tab 对应页面
    23. _renderPage() {
    24. return [
    25. new TabBarPageFirst(),
    26. new TabBarPageSecond(),
    27. new TabBarPageThree(),
    28. ];
    29. }
    30. @override
    31. Widget build(BuildContext context) {
    32. ///带 Scaffold 的Tabbar页面
    33. return new GSYTabBarWidget(
    34. type: GSYTabBarWidget.BOTTOM_TAB,
    35. ///渲染tab
    36. tabItems: _renderTab(),
    37. ///渲染页面
    38. tabViews: _renderPage(),
    39. topPageControl: pageController,
    40. backgroundColor: Colors.black45,
    41. indicatorColor: Colors.white,
    42. title: new Text("GSYGithubFlutter"));
    43. }
    44. }

    如果到此结束,你会发现页面点击切换时,StatefulWidget 的子页面每次都会重新调用initState。这肯定不是我们想要的,所以这时你就需要AutomaticKeepAliveClientMixin

    每个 Tab 对应的 StatefulWidget 的 State ,需要通过with AutomaticKeepAliveClientMixin ,然后重写 @override bool get wantKeepAlive => true; ,就可以实不重新构建的效果了,效果如下图。

    页面效果

    既然底部Tab页面都实现了,干脆顶部tab页面也一起完成。如下代码,和底部Tab页的区别在于:

    • 底部tab是放在了 ScaffoldbottomNavigationBar 中。
    • 顶部tab是放在 AppBarbottom 中,也就是标题栏之下。

    同时我们在顶部 TabBar 增加 isScrollable: true 属性,实现常见的顶部Tab的效果,如下方图片所示。

    1. return new Scaffold(
    2. ///设置侧边滑出 drawer,不需要可以不设置
    3. drawer: _drawer,
    4. ///设置悬浮按键,不需要可以不设置
    5. floatingActionButton: _floatingActionButton,
    6. ///标题栏
    7. appBar: new AppBar(
    8. backgroundColor: _backgroundColor,
    9. title: _title,
    10. ///tabBar控件
    11. bottom: new TabBar(
    12. ///顶部时,tabBar为可以滑动的模式
    13. isScrollable: true,
    14. ///必须有的控制器,与pageView的控制器同步
    15. controller: _tabController,
    16. ///每一个tab item,是一个List<Widget>
    17. tabs: _tabItems,
    18. ///tab底部选中条颜色
    19. indicatorColor: _indicatorColor,
    20. ),
    21. ),
    22. ///页面主体,PageView,用于承载Tab对应的页面
    23. body: new PageView(
    24. ///必须有的控制器,与tabBar的控制器同步
    25. controller: _pageController,
    26. ///每一个 tab 对应的页面主体,是一个List<Widget>
    27. children: _tabViews,
    28. ///页面触摸作用滑动回调,用于同步tab选中状态
    29. onPageChanged: (index) {
    30. _tabController.animateTo(index);
    31. },
    32. ),
    33. );

    顶部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,以实现更多配置效果

    1. class _GSYPullLoadWidgetState extends State<GSYPullLoadWidget> {
    2. ///···
    3. final ScrollController _scrollController = new ScrollController();
    4. @override
    5. void initState() {
    6. ///增加滑动监听
    7. _scrollController.addListener(() {
    8. ///判断当前滑动位置是不是到达底部,触发加载更多回调
    9. if (_scrollController.position.pixels == _scrollController.position.maxScrollExtent) {
    10. if (this.onLoadMore != null && this.control.needLoadMore) {
    11. this.onLoadMore();
    12. }
    13. }
    14. });
    15. super.initState();
    16. }
    17. ///根据配置状态返回实际列表数量
    18. ///实际上这里可以根据你的需要做更多的处理
    19. ///比如多个头部,是否需要空页面,是否需要显示加载更多。
    20. _getListCount() {
    21. ///是否需要头部
    22. if (control.needHeader) {
    23. ///如果需要头部,用Item 0 的 Widget 作为ListView的头部
    24. ///列表数量大于0时,因为头部和底部加载更多选项,需要对列表数据总数+2
    25. return (control.dataList.length > 0) ? control.dataList.length + 2 : control.dataList.length + 1;
    26. } else {
    27. ///如果不需要头部,在没有数据时,固定返回数量1用于空页面呈现
    28. if (control.dataList.length == 0) {
    29. return 1;
    30. }
    31. ///如果有数据,因为部加载更多选项,需要对列表数据总数+1
    32. return (control.dataList.length > 0) ? control.dataList.length + 1 : control.dataList.length;
    33. }
    34. }
    35. ///根据配置状态返回实际列表渲染Item
    36. _getItem(int index) {
    37. if (!control.needHeader && index == control.dataList.length && control.dataList.length != 0) {
    38. ///如果不需要头部,并且数据不为0,当index等于数据长度时,渲染加载更多Item(因为index是从0开始)
    39. return _buildProgressIndicator();
    40. } else if (control.needHeader && index == _getListCount() - 1 && control.dataList.length != 0) {
    41. ///如果需要头部,并且数据不为0,当index等于实际渲染长度 - 1时,渲染加载更多Item(因为index是从0开始)
    42. return _buildProgressIndicator();
    43. } else if (!control.needHeader && control.dataList.length == 0) {
    44. ///如果不需要头部,并且数据为0,渲染空页面
    45. return _buildEmpty();
    46. } else {
    47. ///回调外部正常渲染Item,如果这里有需要,可以直接返回相对位置的index
    48. return itemBuilder(context, index);
    49. }
    50. }
    51. @override
    52. Widget build(BuildContext context) {
    53. return new RefreshIndicator(
    54. ///GlobalKey,用户外部获取RefreshIndicator的State,做显示刷新
    55. key: refreshKey,
    56. ///下拉刷新触发,返回的是一个Future
    57. onRefresh: onRefresh,
    58. child: new ListView.builder(
    59. ///保持ListView任何情况都能滚动,解决在RefreshIndicator的兼容问题。
    60. physics: const AlwaysScrollableScrollPhysics(),
    61. ///根据状态返回子孔健
    62. itemBuilder: (context, index) {
    63. return _getItem(index);
    64. },
    65. ///根据状态返回数量
    66. itemCount: _getListCount(),
    67. ///滑动监听
    68. controller: _scrollController,
    69. ),
    70. );
    71. }
    72. ///空页面
    73. Widget _buildEmpty() {
    74. ///···
    75. }
    76. ///上拉加载更多
    77. Widget _buildProgressIndicator() {
    78. ///···
    79. }
    80. }

    效果如图

    3、Loading框

    在上一小节中,我们实现上滑加载更多的效果,其中就需要展示 Loading 状态的需求。默认系统提供了CircularProgressIndicator等,但是有追求的我们怎么可能局限于此,这里推荐一个第三方 Loading 库 :flutter_spinkit ,通过简单的配置就可以使用丰富的 Loading 样式。

    继续上一小节中的 _buildProgressIndicator方法实现,通过 flutter_spinkit 可以快速实现更不一样的 Loading 样式。

    1. ///上拉加载更多
    2. Widget _buildProgressIndicator() {
    3. ///是否需要显示上拉加载更多的loading
    4. Widget bottomWidget = (control.needLoadMore)
    5. ? new Row(mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[
    6. ///loading框
    7. new SpinKitRotatingCircle(color: Color(0xFF24292E)),
    8. new Container(
    9. width: 5.0,
    10. ),
    11. ///加载中文本
    12. new Text(
    13. "加载中···",
    14. style: TextStyle(
    15. color: Color(0xFF121917),
    16. fontSize: 14.0,
    17. fontWeight: FontWeight.bold,
    18. ),
    19. )
    20. ])
    21. /// 不需要加载
    22. : new Container();
    23. return new Padding(
    24. padding: const EdgeInsets.all(20.0),
    25. child: new Center(
    26. child: bottomWidget,
    27. ),
    28. );
    29. }

    loading样式

    4、矢量图标库

    矢量图标对笔者是必不可少的。比起一般的 png 图片文件,矢量图标在开发过程中:可以轻松定义颜色,并且任意调整大小不模糊。矢量图标库是引入 ttf 字体库文件实现,在 Flutter 中通过 Icon 控件,加载对应的 IconData 显示即可。

    Flutter 中默认内置的 Icons 类就提供了丰富的图标,直接通过 Icons 对象即可使用,同时个人推荐阿里爸爸的 iconfont 。如下代码,通过在 pubspec.yaml 中添加字体库支持,然后在代码中创建 IconData 指向字体库名称引用即可。

    1. fonts:
    2. - family: wxcIconFont
    3. fonts:
    4. - asset: static/font/iconfont.ttf
    5. ··················
    6. ///使用Icons
    7. new Tab(
    8. child: new Column(
    9. mainAxisAlignment: MainAxisAlignment.center,
    10. children: <Widget>[new Icon(Icons.list, size: 16.0), new Text("趋势")],
    11. ),
    12. ),
    13. ///使用iconfont
    14. new Tab(
    15. child: new Column(
    16. mainAxisAlignment: MainAxisAlignment.center,
    17. children: <Widget>[new Icon(IconData(0xe6d0, fontFamily: "wxcIconFont"), size: 16.0), new Text("我的")],
    18. ),
    19. )

    5、路由跳转

    Flutter 中的页面跳转是通过 Navigator 实现的,路由跳转又分为:带参数跳转和不带参数跳转。不带参数跳转比较简单,默认可以通过 MaterialApp 的路由表跳转;而带参数的跳转,参数通过跳转页面的构造方法传递。常用的跳转有如下几种使用:

    1. ///不带参数的路由表跳转
    2. Navigator.pushNamed(context, routeName);
    3. ///跳转新页面并且替换,比如登录页跳转主页
    4. Navigator.pushReplacementNamed(context, routeName);
    5. ///跳转到新的路由,并且关闭给定路由的之前的所有页面
    6. Navigator.pushNamedAndRemoveUntil(context, '/calendar', ModalRoute.withName('/'));
    7. ///带参数的路由跳转,并且监听返回
    8. Navigator.push(context, new MaterialPageRoute(builder: (context) => new NotifyPage())).then((res) {
    9. ///获取返回处理
    10. });

      同时我们可以看到,Navigator 的 push 返回的是一个 Future,这个Future 的作用是在页面返回时被调用的。也就是你可以通过 Navigatorpop 时返回参数,之后在 Future 中可以的监听中处理页面的返回结果。

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

    中场休息

    二、数据模块

    数据为王,不过应该不是隔壁老王吧。

    1、网络请求

    当前 Flutter 网络请求封装中,国内最受欢迎的就是 Dio 了,Dio 封装了网络请求中的数据转换、拦截器、请求返回等。如下代码所示,通过对 Dio 的简单封装即可快速网络请求,真的很简单,更多的可以查 Dio 的官方文档,这里就不展开了。(真的不是懒(˶‾᷄ ⁻̫ ‾᷅˵))

    1. ///创建网络请求对象
    2. Dio dio = new Dio();
    3. Response response;
    4. try {
    5. ///发起请求
    6. ///url地址,请求数据,一般为Map或者FormData
    7. ///options 额外配置,可以配置超时,头部,请求类型,数据响应类型,host等
    8. response = await dio.request(url, data: params, options: option);
    9. } on DioError catch (e) {
    10. ///http错误是通过 DioError 的catch返回的一个对象
    11. }

    2、Json序列化

    在 Flutter 中,json 序列化是有些特殊的。不同与 JS ,比如使用上述 Dio 网络请求返回,如果配置了返回数据格式为 json ,实际上的到会是一个Map。而 Map 的 key-value 使用,在开发过程中并不是很方便,所以你需要对Map 再进行一次转化,转为实际的 Model 实体。

    所以 json_serializable 插件诞生了, 中文网Json 对其已有一段教程,这里主要补充说明下具体的使用逻辑。

    1. dependencies:
    2. # Your other regular dependencies here
    3. json_annotation: ^0.2.2
    4. dev_dependencies:
    5. # Your other dev_dependencies here
    6. build_runner: ^0.7.6
    7. 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 编译自动生成转化对象。(个人习惯完成后手动编译)

    1. import 'package:json_annotation/json_annotation.dart';
    2. ///关联文件、允许Template访问 Template.g.dart 中的私有方法
    3. ///Template.g.dart 是通过命令生成的文件。名称为 xx.g.dart,其中 xx 为当前 dart 文件名称
    4. ///Template.g.dart中创建了抽象类_$TemplateSerializerMixin,实现了_$TemplateFromJson方法
    5. part 'Template.g.dart';
    6. ///标志class需要实现json序列化功能
    7. @JsonSerializable()
    8. ///'xx.g.dart'文件中,默认会根据当前类名如 AA 生成 _$AASerializerMixin
    9. ///所以当前类名为Template,生成的抽象类为 _$TemplateSerializerMixin
    10. class Template extends Object with _$TemplateSerializerMixin {
    11. String name;
    12. int id;
    13. ///通过JsonKey重新定义参数名
    14. @JsonKey(name: "push_id")
    15. int pushId;
    16. Template(this.name, this.id, this.pushId);
    17. ///'xx.g.dart'文件中,默认会根据当前类名如 AA 生成 _$AAeFromJson方法
    18. ///所以当前类名为Template,生成的抽象类为 _$TemplateFromJson
    19. factory Template.fromJson(Map<String, dynamic> json) => _$TemplateFromJson(json);
    20. }

    序列化源码部分

    上述操作生成后的 Template.g.dart 下的代码如下,这样我们就可以通过 Template.fromJsontoJson 方法对实体与map进行转化,再结合json.decodejson.encode,你就可以愉悦的在string 、map、实体间相互转化了

    1. part of 'Template.dart';
    2. Template _$TemplateFromJson(Map<String, dynamic> json) => new Template(
    3. json['name'] as String, json['id'] as int, json['push_id'] as int);
    4. abstract class _$TemplateSerializerMixin {
    5. String get name;
    6. int get id;
    7. int get pushId;
    8. Map<String, dynamic> toJson() =>
    9. <String, dynamic>{'name': name, 'id': id, 'push_id': pushId};
    10. }

    注意:新版json序列化中做了部分修改,代码更简单了,详见demo

    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

    1. ///全局Redux store 的对象,保存State数据
    2. class GSYState {
    3. ///用户信息
    4. User userInfo;
    5. ///构造方法
    6. GSYState({this.userInfo});
    7. }
    8. ///通过 Reducer 创建 用于store 的 Reducer
    9. GSYState appReducer(GSYState state, action) {
    10. return GSYState(
    11. ///通过 UserReducer 将 GSYState 内的 userInfo 和 action 关联在一起
    12. userInfo: UserReducer(state.userInfo, action),
    13. );
    14. }

    下面是上方使用的 UserReducer 的实现。这里主要通过 TypedReducer 将 reducer 的处理逻辑与定义的 Action 绑定,最后通过 combineReducers 返回 Reducer<State> 对象应用于上方 Store 中。

    1. /// redux 的 combineReducers, 通过 TypedReducer 将 UpdateUserAction 与 reducers 关联起来
    2. final UserReducer = combineReducers<User>([
    3. TypedReducer<User, UpdateUserAction>(_updateLoaded),
    4. ]);
    5. /// 如果有 UpdateUserAction 发起一个请求时
    6. /// 就会调用到 _updateLoaded
    7. /// _updateLoaded 这里接受一个新的userInfo,并返回
    8. User _updateLoaded(User user, action) {
    9. user = action.userInfo;
    10. return user;
    11. }
    12. ///定一个 UpdateUserAction ,用于发起 userInfo 的的改变
    13. ///类名随你喜欢定义,只要通过上面TypedReducer绑定就好
    14. class UpdateUserAction {
    15. final User userInfo;
    16. UpdateUserAction(this.userInfo);
    17. }

    下面正式在 Flutter 中引入 store,通过 StoreProvider 将创建 的 store 引用到 Flutter 中。

    1. void main() {
    2. runApp(new FlutterReduxApp());
    3. }
    4. class FlutterReduxApp extends StatelessWidget {
    5. /// 创建Store,引用 GSYState 中的 appReducer 创建的 Reducer
    6. /// initialState 初始化 State
    7. final store = new Store<GSYState>(appReducer, initialState: new GSYState(userInfo: User.empty()));
    8. FlutterReduxApp({Key key}) : super(key: key);
    9. @override
    10. Widget build(BuildContext context) {
    11. /// 通过 StoreProvider 应用 store
    12. return new StoreProvider(
    13. store: store,
    14. child: new MaterialApp(
    15. home: DemoUseStorePage(),
    16. ),
    17. );
    18. }
    19. }

    在下方 DemoUseStorePage 中,通过 StoreConnector 将State 绑定到 Widget;通过 StoreProvider.of 可以获取 state 对象;通过 dispatch 一个 Action 可以更新State。

    1. class DemoUseStorePage extends StatelessWidget {
    2. @override
    3. Widget build(BuildContext context) {
    4. ///通过 StoreConnector 关联 GSYState 中的 User
    5. return new StoreConnector<GSYState, User>(
    6. ///通过 converter 将 GSYState 中的 userInfo返回
    7. converter: (store) => store.state.userInfo,
    8. ///在 userInfo 中返回实际渲染的控件
    9. builder: (context, userInfo) {
    10. return new Text(
    11. userInfo.name,
    12. style: Theme.of(context).textTheme.display1,
    13. );
    14. },
    15. );
    16. }
    17. }
    18. ·····
    19. ///通过 StoreProvider.of(context) (带有 StoreProvider 下的 context)
    20. /// 可以任意的位置访问到 state 中的数据
    21. StoreProvider.of(context).state.userInfo;
    22. ·····
    23. ///通过 dispatch UpdateUserAction,可以更新State
    24. StoreProvider.of(context).dispatch(new UpdateUserAction(newUserInfo));

    看到这是不是有点想静静了?先不管静静是谁,但是Redux的实用性是应该比静静更吸引人,作为一个有追求的程序猿,多动手撸撸还有什么拿不下的山头是不?更详细的实现请看:GSYGithubAppFlutter 。

    4、数据库

    在 GSYGithubAppFlutter 中,数据库使用的是 sqflite 的封装,其实就是 sqlite 语法的使用而已,有兴趣的可以看看完整代码 DemoDb.dart 。 这里主要提供一种思路,按照 sqflite 文档提供的方法,重新做了一小些修改,通过定义 Provider 操作数据库:

    • 在 Provider 中定义表名数据库字段常量,用于创建表与字段操作;

    • 提供数据库与数据实体之间的映射,比如数据库对象与User对象之间的转化;

    • 在调用 Provider 时才先判断表是否创建,然后再返回数据库对象进行用户查询。

    如果结合网络请求,通过闭包实现,在需要数据库时先返回数据库,然后通过 next 方法将网络请求的方法返回,最后外部可以通过调用next方法再执行网络请求。如下所示:

    1. UserDao.getUserInfo(userName, needDb: true).then((res) {
    2. ///数据库结果
    3. if (res != null && res.result) {
    4. setState(() {
    5. userInfo = res.data;
    6. });
    7. }
    8. return res.next;
    9. }).then((res) {
    10. ///网络结果
    11. if (res != null && res.result) {
    12. setState(() {
    13. userInfo = res.data;
    14. });
    15. }
    16. });

    三、其他功能

    其他功能,只是因为想不到标题。

    1、返回按键监听

    Flutter 中 ,通过WillPopScope 嵌套,可以用于监听处理 Android 返回键的逻辑。其实 WillPopScope 并不是监听返回按键,如名字一般,是当前页面将要被pop时触发的回调。

    通过onWillPop回调返回的Future,判断是否响应 pop 。下方代码实现按下返回键时,弹出提示框,按下确定退出App。

    1. class HomePage extends StatelessWidget {
    2. /// 单击提示退出
    3. Future<bool> _dialogExitApp(BuildContext context) {
    4. return showDialog(
    5. context: context,
    6. builder: (context) => new AlertDialog(
    7. content: new Text("是否退出"),
    8. actions: <Widget>[
    9. new FlatButton(onPressed: () => Navigator.of(context).pop(false), child: new Text("取消")),
    10. new FlatButton(
    11. onPressed: () {
    12. Navigator.of(context).pop(true);
    13. },
    14. child: new Text("确定"))
    15. ],
    16. ));
    17. }
    18. // This widget is the root of your application.
    19. @override
    20. Widget build(BuildContext context) {
    21. return WillPopScope(
    22. onWillPop: () {
    23. ///如果返回 return new Future.value(false); popped 就不会被处理
    24. ///如果返回 return new Future.value(true); popped 就会触发
    25. ///这里可以通过 showDialog 弹出确定框,在返回时通过 Navigator.of(context).pop(true);决定是否退出
    26. return _dialogExitApp(context);
    27. },
    28. child: new Container(),
    29. );
    30. }
    31. }

    2、前后台监听

    WidgetsBindingObserver 包含了各种控件的生命周期通知,其中的 didChangeAppLifecycleState 就可以用于做前后台状态监听。

    1. /// WidgetsBindingObserver 包含了各种控件的生命周期通知
    2. class _HomePageState extends State<HomePage> with WidgetsBindingObserver {
    3. ///重写 WidgetsBindingObserver 中的 didChangeAppLifecycleState
    4. @override
    5. void didChangeAppLifecycleState(AppLifecycleState state) {
    6. ///通过state判断App前后台切换
    7. if (state == AppLifecycleState.resumed) {
    8. }
    9. }
    10. @override
    11. Widget build(BuildContext context) {
    12. return new Container();
    13. }
    14. }

    3、键盘焦点处理

    一般触摸收起键盘也是常见需求,如下代码所示, GestureDetector + FocusScope 可以满足这一需求。

    1. class _LoginPageState extends State<LoginPage> {
    2. @override
    3. Widget build(BuildContext context) {
    4. ///定义触摸层
    5. return new GestureDetector(
    6. ///透明也响应处理
    7. behavior: HitTestBehavior.translucent,
    8. onTap: () {
    9. ///触摸手气键盘
    10. FocusScope.of(context).requestFocus(new FocusNode());
    11. },
    12. child: new Container(
    13. ),
    14. );
    15. }
    16. }

    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 文件夹即可,记得各个文件夹下提供相对于大小尺寸的文件。

    自此,第二篇终于结束了!(///▽///)

    资源推荐

    • Github : https://github.com/CarGuo
    • 本文代码 :https://github.com/CarGuo/GSYGithubAppFlutter

    完整开源项目推荐:

    • GSYGithubAppWeex
    • GSYGithubApp React Native

    文章

    《Flutter完整开发实战详解(一、Dart语言和Flutter基础)》

    《跨平台项目开源项目推荐》

    《移动端跨平台开发的深度解析》

    我们还会再见的