Flutter+Mobx实战,写一个App应用

说明

目前增加了路由跳转,可以带参数跳转页面。下拉可以自定义刷新样式,IOS点击Status Bar回到顶部,目前已经测试过。状态管理器使用Mobx,我自己觉得对于Redux使用起来会复杂一点,下面是提供的预览GIF图,卡顿现象是因为屏幕录制的帧率有点低。

项目地址:https://github.com/Tecode/flutter_book,不定时的更新,欢迎start。

安卓预览

image

IOS预览

image

依赖库

environment:
  sdk: ">=2.1.0 <3.0.0"
dependencies:
  flutter:
    sdk: flutter
  mobx:
  flutter_mobx: // Mobx
  cupertino_icons: ^0.1.2
  flutter_svg: ">=0.12.4" // 处理SVG图片
  carousel_slider: ^1.3.0 // 轮播图
  fluro: "^1.4.0" // 路由
  provider: ^2.0.1 // 用于包裹mobx

dev_dependencies:
  flutter_test:
    sdk: flutter
  build_runner: ^1.3.1 //Mobx依赖
  mobx_codegen: // Mobx依赖

Flutter版本

Flutter 1.5.9-pre.223 • channel master • https://github.com/flutter/flutter.git
Framework • revision b76a1e8312 (25 hours ago) • 2019-05-13 09:06:30 +0100
Engine • revision 816d3fc586
Tools • Dart 2.3.1 (build 2.3.1-dev.0.0 a0290f823c)

修改系统状态栏颜色

image
import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter_book/containers/Entrance.dart';
import 'package:flutter_book/helpers/constants.dart' show AppColors;
import 'package:flutter/services.dart';

void main() {
  // 修改系统状态栏颜色
  SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle(
    systemNavigationBarColor: Color(AppColors.themeColor), // navigation bar color
    statusBarColor: Color(AppColors.themeColor), // status bar color
  ));
  runApp(MyApp());wenti
}

自定义appBar左侧导航显示的内容

image
appBar: AppBar(
...
        leading: IconButton(
          alignment: Alignment.centerRight,
          icon: SvgPicture.asset(
            'assets/icon/icon_trophy.svg',
            width: Constants.appBarIconSize + 5.0,
            height: Constants.appBarIconSize + 5.0,
          ),
          onPressed: () {
            print("ok");
          },
        )
...
)

媒体查询

MediaQuery.of(context)

资源配置

  assets:
   - assets/icon/
   - lib/containers/
   - lib/model/
   - lib/helpers/
   - lib/routers/
   - assets/images/

路由配置

这里我使用的是fluro配置路由,这里我偷一下懒了,就没有使用原生的方法,不过他帮我们封装了好多的方法我们可以很方便的去使用它,下面说一下路由的配置。

lib\routers\routers.dart

配置路由对应的模块,可以理解成Vue-routerReact-router一样,先要将对应的路由配置到你要跳转的模块去。

import 'package:fluro/fluro.dart';
import 'package:flutter/material.dart';
import 'package:flutter_book/routers/route_handlers.dart';

class Routes {
  static String root = "/";
  static String setting = "/setting";
  static String detail = "/detail";
  static String demoSimpleFixedTrans = "/demo/fixedtrans";
  static String demoFunc = "/demo/func";
  static String deepLink = "/message";

  static void configureRoutes(Router router) {
    router.notFoundHandler = Handler(
        handlerFunc: (BuildContext context, Map<String, List<String>> params) {
      print("ROUTE WAS NOT FOUND !!!");
    });
    router.define(root, handler: rootHandler);
    router.define(setting, handler: settingRouteHandler);
    router.define(detail, handler: detailRouterHandler);
  }
}

lib\routers\route_handlers.dart

在这里可以处理一些传过来的参数,然后我们将参数放入类中实例化。

import 'package:flutter_book/containers/Setting.dart';
import 'package:flutter_book/containers/FirstScreen.dart';
import 'package:flutter_book/containers/Detail.dart';
import 'package:fluro/fluro.dart';
import 'package:flutter/material.dart';
import 'package:flutter_book/helpers/fluro_convert_util.dart';

Handler rootHandler = Handler(
    handlerFunc: (BuildContext context, Map<String, List<String>> params) {
  return FirstScreen();
});

Handler settingRouteHandler = Handler(
    handlerFunc: (BuildContext context, Map<String, List<String>> params) {
    return Setting();
});

Handler detailRouterHandler = Handler(
    handlerFunc: (BuildContext context, Map<String, List<String>> params) {
  return Detail(
      title: FluroConvertUtils.fluroCnParamsDecode(params["title"]?.first));
});

lib\main.dart

将路由与Flutter绑定,这样你的路由就可以生效了

class MyApp extends StatelessWidget {
  MyApp() {
    final router = new Router();
    Routes.configureRoutes(router);
    Application.router = router;
  }
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Book',
      theme: ThemeData(
          primaryColor: Color(AppColors.themeColor),
          accentColor: Color(AppColors.themeColor),
          scaffoldBackgroundColor: Color(AppColors.themeColor)),
      home: Entrance(),
      onGenerateRoute: Application.router.generator,
    );
  }
}

使用

import 'package:fluro/fluro.dart';
import 'package:flutter_book/routers/application.dart';
import 'package:flutter_book/helpers/fluro_convert_util.dart';

...代码省略了

 Application.router.navigateTo(
    context,
    "/detail?title=${FluroConvertUtils.fluroCnParamsEncode('热门图书')}",
    transition: TransitionType.native
);

路由传参

路由不支持中文字符需要编码再解码

import 'dart:convert';

/// fluro 参数编码解码工具类
class FluroConvertUtils {
  /// fluro 传递中文参数前,先转换,fluro 不支持中文传递
  static String fluroCnParamsEncode(String originalCn) {
    StringBuffer sb = StringBuffer();
    var encoded = Utf8Encoder().convert(originalCn);
    encoded.forEach((val) => sb.write('$val,'));
    return sb.toString().substring(0, sb.length - 1).toString();
  }

  /// fluro 传递后取出参数,解析
  static String fluroCnParamsDecode(String encodedCn) {
    var decoded = encodedCn.split('[').last.split(']').first.split(',');
    var list = <int>[];
    decoded.forEach((s) => list.add(int.parse(s.trim())));
    return Utf8Decoder().convert(list);
  }
}

编码

import 'package:flutter_book/helpers/fluro_convert_util.dart';

Application.router.navigateTo(
    context,
    "/detail?title=${FluroConvertUtils.fluroCnParamsEncode('热门图书')}",
    transition: TransitionType.native,
    // transitionDuration: const Duration(milliseconds: 300),
);

解码

import 'package:flutter_book/helpers/fluro_convert_util.dart';

Handler detailRouterHandler = Handler(
    handlerFunc: (BuildContext context, Map<String, List<String>> params) {
  return Detail(
      title: FluroConvertUtils.fluroCnParamsDecode(params["title"]?.first));
});

使用Mobx状态管理器

pubspec.yaml配置

environment:
  sdk: ">=2.1.0 <3.0.0"

dependencies:
  flutter:
    sdk: flutter
  mobx:
  flutter_mobx:


  # The following adds the Cupertino Icons font to your application.
  # Use with the CupertinoIcons class for iOS style icons.
  cupertino_icons: ^0.1.2
  flutter_svg: ">=0.12.4"
  carousel_slider: ^1.3.0
  fluro: "^1.4.0"
  provider: ^2.0.1

dev_dependencies:
  flutter_test:
    sdk: flutter
  build_runner: ^1.3.1
  mobx_codegen:

多个页面使用一个store

这里要使用到provider: ^2.0.1,类似ReactProvider。使用Provider来包裹我们的组件,使Mobx和我们的React联系起来。

React Provider

<Provider {...store}>
    <Router history={browserHistory}
        <App />
    </Router>
</Provider>

Dart Provider

Dart Provider也是一样的道理,将MobxFlutter联系起来,lib/main.dart完整代码,这样使用可以保证你实例化的的store是同一个类。

  runApp(MultiProvider(
    providers: [
      Provider<FindStore>(
        builder: (_) => FindStore(),
      )
    ],
    child: MyApp(),
  ));

如何使用

我的导航发现那一栏和下面的内容是分开的,当我点击导航的切换按钮就会改变显示的页面,这样我们可以复用显示层的UI组件,数据放专门的文件去管理。

image
image

来看看如何实现的

通过点击然后改变数据findStore.setTile('tile', true);

导航lib/widgets/NavBar/FindNavBar.dart

import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:flutter_book/helpers/constants.dart';
import 'package:flutter_book/stores/findStore.dart';
import 'package:provider/provider.dart';

class FindNavBar extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
  // 我们的store
    final findStore = Provider.of<FindStore>(context);

    return Observer(
      builder: (_) => AppBar(
            title: Text("发现"),
            actions: <Widget>[
              IconButton(
                alignment: Alignment.centerRight,
                onPressed: () {
                  findStore.setTile('tile', true);
                  findStore.counter();
                },
                icon: SvgPicture.asset(
                  'assets/icon/icon_more.svg',
                  width: Constants.appBarIconSize + 2.0,
                  height: Constants.appBarIconSize + 2.0,
                  color: Color(findStore.tile
                      ? AppColors.fontColor
                      : AppColors.fontColorGray),
                ),
              ),
              IconButton(
                alignment: Alignment.centerLeft,
                onPressed: () {
                  findStore.setTile('tile', false);
                },
                icon: SvgPicture.asset(
                  'assets/icon/icon_cube.svg',
                  width: Constants.appBarIconSize + 2.0,
                  height: Constants.appBarIconSize + 2.0,
                  color: Color(findStore.tile
                      ? AppColors.fontColorGray
                      : AppColors.fontColor),
                ),
              ),
            ],
            centerTitle: true,
            elevation: 0,
          ),
    );
  }
}

内容lib/containers/Find.dart

检测到数据发生变化,页面重新渲染得到新的页面

import 'package:flutter/material.dart';
import 'package:flutter_book/widgets/Find/BookTile.dart';
import 'package:flutter_book/widgets/Find/BookCover.dart';

import 'package:flutter_book/stores/findStore.dart';
import 'package:provider/provider.dart';
import 'package:flutter_mobx/flutter_mobx.dart';

class Find extends StatefulWidget {
  @override
  _FindState createState() => _FindState();
}

class _FindState extends State<Find> {
  @override
  Widget build(BuildContext context) {
    final findStore = Provider.of<FindStore>(context);
    return Observer(builder: (_) => findStore.tile ? BookTile() : BookCover());
  }
}

FindStore lib/stores/findStore.dart

import 'package:mobx/mobx.dart';

// Include generated file
part 'findStore.g.dart';

// This is the class used by rest of your codebase
class FindStore = _FindStore with _$FindStore;

// The store-class
abstract class _FindStore implements Store {
  @observable
  bool tile = false;

  @observable
  num count = 0;

  @action
  void setTile(String key, dynamic value) => tile = value;

  @action
  num counter() => this.count++;
}

注意

如果你是很多个页面共享一个Store不要直接导入然后实例化,例如:

第一个页面 demo1.dart

这个页面我们导入了counter.dart这个store而且我们将它实例化,当我们点击的时候数据发生变化页面会重新渲染

import 'package:flutter/material.dart';
import 'package:flutter_mobx/flutter_mobx.dart';

import 'counter.dart'; // Import the Counter

final counter = Counter(); // Instantiate the store

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'MobX',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(),
    );
  }
}

class MyHomePage extends StatelessWidget {
  const MyHomePage();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('MobX Counter'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              '数值是:',
            ),
            // Wrapping in the Observer will automatically re-render on changes to counter.value
            Observer(
              builder: (_) => Text(
                    '${counter.value}',
                    style: Theme.of(context).textTheme.display1,
                  ),
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: counter.increment,
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ),
    );
  }
}

第二个页面 demo2.dart

这个页面我们也导入了counter.dart,我们要的结果是第一个页面的数据变化了也影响这个页面,但是显然是不能的。因为store虽然是一个,但是实例化的时候是两个不同的,所以第一个页面的数据变化了也不会影响到这里。

怎么解决呢?我们可以使用之前提到的Provider去将MobxFlutter联系起来然后通过上下关系去的到我们想要的Store,例如final findStore = Provider.of<FindStore>(context);

import 'package:flutter/material.dart';
import 'package:flutter_mobx/flutter_mobx.dart';

import 'counter.dart'; // Import the Counter

final counter = Counter(); // Instantiate the store

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'MobX',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(),
    );
  }
}

class MyHomePage extends StatelessWidget {
  const MyHomePage();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('MobX Counter'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              '第二个页面显示第一个页面的数是:',
            ),
            Observer(
              builder: (_) => Text(
                    '${counter.value}',
                    style: Theme.of(context).textTheme.display1,
                  ),
            ),
          ],
        ),
      ),
    );
  }
}

公共的Store counter.dart

import 'package:mobx/mobx.dart';

// Include generated file
part 'counter.g.dart';

// This is the class used by rest of your codebase
class Counter = _Counter with _$Counter;

// The store-class
abstract class _Counter implements Store {
  @observable
  int value = 0;

  @action
  void increment() {
    value++;
  }
}

正确的使用方法

页面1-导航栏

页面2-内容

公共Store

将Mobx和Flutter联系起来

结束语

感谢你的围观,目前是我写Flutter遇到的一些坑,欢迎大家一踩坑,大家有什么意见和建议都可以提出来,谢谢。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,165评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,720评论 1 298
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,849评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,245评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,596评论 3 288
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,747评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,977评论 2 315
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,708评论 0 204
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,448评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,657评论 2 249
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,141评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,493评论 3 258
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,153评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,108评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,890评论 0 198
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,799评论 2 277
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,685评论 2 272

推荐阅读更多精彩内容