Flutter+Mobx實(shí)戰(zhàn)卡乾,寫(xiě)一個(gè)App應(yīng)用

說(shuō)明

目前增加了路由跳轉(zhuǎn),可以帶參數(shù)跳轉(zhuǎn)頁(yè)面姆钉。下拉可以自定義刷新樣式说订,IOS點(diǎn)擊Status Bar回到頂部,目前已經(jīng)測(cè)試過(guò)潮瓶。狀態(tài)管理器使用Mobx,我自己覺(jué)得對(duì)于Redux使用起來(lái)會(huì)復(fù)雜一點(diǎn)陶冷,下面是提供的預(yù)覽GIF圖,卡頓現(xiàn)象是因?yàn)槠聊讳浿频膸视悬c(diǎn)低。

項(xiàng)目地址:https://github.com/Tecode/flutter_book,不定時(shí)的更新毯辅,歡迎start埂伦。

安卓預(yù)覽

image

IOS預(yù)覽

image

依賴(lài)庫(kù)

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依賴(lài)
  mobx_codegen: // Mobx依賴(lài)

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)

修改系統(tǒng)狀態(tài)欄顏色

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() {
  // 修改系統(tǒng)狀態(tài)欄顏色
  SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle(
    systemNavigationBarColor: Color(AppColors.themeColor), // navigation bar color
    statusBarColor: Color(AppColors.themeColor), // status bar color
  ));
  runApp(MyApp());wenti
}

自定義appBar左側(cè)導(dǎo)航顯示的內(nèi)容

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");
          },
        )
...
)

媒體查詢(xún)

MediaQuery.of(context)

資源配置

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

路由配置

這里我使用的是fluro配置路由,這里我偷一下懶了思恐,就沒(méi)有使用原生的方法沾谜,不過(guò)他幫我們封裝了好多的方法我們可以很方便的去使用它,下面說(shuō)一下路由的配置胀莹。

lib\routers\routers.dart

配置路由對(duì)應(yīng)的模塊基跑,可以理解成Vue-routerReact-router一樣,先要將對(duì)應(yīng)的路由配置到你要跳轉(zhuǎn)的模塊去描焰。

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

在這里可以處理一些傳過(guò)來(lái)的參數(shù)媳否,然后我們將參數(shù)放入類(lèi)中實(shí)例化。

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('熱門(mén)圖書(shū)')}",
    transition: TransitionType.native
);

路由傳參

路由不支持中文字符需要編碼再解碼

import 'dart:convert';

/// fluro 參數(shù)編碼解碼工具類(lèi)
class FluroConvertUtils {
  /// fluro 傳遞中文參數(shù)前篱竭,先轉(zhuǎn)換,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 傳遞后取出參數(shù)步绸,解析
  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('熱門(mén)圖書(shū)')}",
    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狀態(tài)管理器

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:

多個(gè)頁(yè)面使用一個(gè)store

這里要使用到provider: ^2.0.1掺逼,類(lèi)似ReactProvider。使用Provider來(lái)包裹我們的組件瓤介,使Mobx和我們的React聯(lián)系起來(lái)吕喘。

React Provider

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

Dart Provider

Dart Provider也是一樣的道理,將MobxFlutter聯(lián)系起來(lái)刑桑,lib/main.dart完整代碼兽泄,這樣使用可以保證你實(shí)例化的的store是同一個(gè)類(lèi)。

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

如何使用

我的導(dǎo)航發(fā)現(xiàn)那一欄和下面的內(nèi)容是分開(kāi)的漾月,當(dāng)我點(diǎn)擊導(dǎo)航的切換按鈕就會(huì)改變顯示的頁(yè)面,這樣我們可以復(fù)用顯示層的UI組件胃珍,數(shù)據(jù)放專(zhuān)門(mén)的文件去管理梁肿。

image
image

來(lái)看看如何實(shí)現(xiàn)的

通過(guò)點(diǎn)擊然后改變數(shù)據(jù)findStore.setTile('tile', true);

導(dǎo)航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("發(fā)現(xiàn)"),
            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,
          ),
    );
  }
}

內(nèi)容lib/containers/Find.dart

檢測(cè)到數(shù)據(jù)發(fā)生變化蜓陌,頁(yè)面重新渲染得到新的頁(yè)面

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

注意

如果你是很多個(gè)頁(yè)面共享一個(gè)Store不要直接導(dǎo)入然后實(shí)例化,例如:

第一個(gè)頁(yè)面 demo1.dart

這個(gè)頁(yè)面我們導(dǎo)入了counter.dart這個(gè)store而且我們將它實(shí)例化,當(dāng)我們點(diǎn)擊的時(shí)候數(shù)據(jù)發(fā)生變化頁(yè)面會(huì)重新渲染

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(
              '數(shù)值是:',
            ),
            // 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),
      ),
    );
  }
}

第二個(gè)頁(yè)面 demo2.dart

這個(gè)頁(yè)面我們也導(dǎo)入了counter.dart吩蔑,我們要的結(jié)果是第一個(gè)頁(yè)面的數(shù)據(jù)變化了也影響這個(gè)頁(yè)面钮热,但是顯然是不能的。因?yàn)?code>store雖然是一個(gè)烛芬,但是實(shí)例化的時(shí)候是兩個(gè)不同的隧期,所以第一個(gè)頁(yè)面的數(shù)據(jù)變化了也不會(huì)影響到這里。

怎么解決呢赘娄?我們可以使用之前提到的Provider去將MobxFlutter聯(lián)系起來(lái)然后通過(guò)上下關(guān)系去的到我們想要的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(
              '第二個(gè)頁(yè)面顯示第一個(gè)頁(yè)面的數(shù)是:',
            ),
            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++;
  }
}

正確的使用方法

頁(yè)面1-導(dǎo)航欄

頁(yè)面2-內(nèi)容

公共Store

將Mobx和Flutter聯(lián)系起來(lái)

結(jié)束語(yǔ)

感謝你的圍觀,目前是我寫(xiě)Flutter遇到的一些坑遣臼,歡迎大家一踩坑性置,大家有什么意見(jiàn)和建議都可以提出來(lái),謝謝揍堰。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末鹏浅,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子屏歹,更是在濱河造成了極大的恐慌隐砸,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,591評(píng)論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件蝙眶,死亡現(xiàn)場(chǎng)離奇詭異季希,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)械馆,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,448評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門(mén)胖眷,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人霹崎,你說(shuō)我怎么就攤上這事珊搀。” “怎么了尾菇?”我有些...
    開(kāi)封第一講書(shū)人閱讀 162,823評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵境析,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我派诬,道長(zhǎng)劳淆,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,204評(píng)論 1 292
  • 正文 為了忘掉前任默赂,我火速辦了婚禮沛鸵,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己曲掰,他們只是感情好疾捍,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,228評(píng)論 6 388
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著栏妖,像睡著了一般乱豆。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上吊趾,一...
    開(kāi)封第一講書(shū)人閱讀 51,190評(píng)論 1 299
  • 那天宛裕,我揣著相機(jī)與錄音,去河邊找鬼论泛。 笑死揩尸,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的孵奶。 我是一名探鬼主播疲酌,決...
    沈念sama閱讀 40,078評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼了袁!你這毒婦竟也來(lái)了朗恳?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 38,923評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤载绿,失蹤者是張志新(化名)和其女友劉穎粥诫,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體崭庸,經(jīng)...
    沈念sama閱讀 45,334評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡怀浆,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,550評(píng)論 2 333
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了怕享。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片执赡。...
    茶點(diǎn)故事閱讀 39,727評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖函筋,靈堂內(nèi)的尸體忽然破棺而出沙合,到底是詐尸還是另有隱情,我是刑警寧澤跌帐,帶...
    沈念sama閱讀 35,428評(píng)論 5 343
  • 正文 年R本政府宣布首懈,位于F島的核電站,受9級(jí)特大地震影響谨敛,放射性物質(zhì)發(fā)生泄漏究履。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,022評(píng)論 3 326
  • 文/蒙蒙 一脸狸、第九天 我趴在偏房一處隱蔽的房頂上張望最仑。 院中可真熱鬧,春花似錦、人聲如沸盯仪。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,672評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)全景。三九已至,卻和暖如春牵囤,著一層夾襖步出監(jiān)牢的瞬間爸黄,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,826評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工揭鳞, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留炕贵,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,734評(píng)論 2 368
  • 正文 我出身青樓野崇,卻偏偏與公主長(zhǎng)得像称开,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子乓梨,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,619評(píng)論 2 354

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