Flutter+Mobx實戰,寫一個App應用

Tecode發表於2019-05-14

說明

目前增加了路由跳轉,可以帶引數跳轉頁面。下拉可以自定義重新整理樣式,IOS點選Status Bar回到頂部,目前已經測試過。狀態管理器使用Mobx,我自己覺得對於Redux使用起來會複雜一點,下面是提供的預覽GIF圖,卡頓現象是因為螢幕錄製的幀率有點低。

專案地址:github.com/Tecode/flut…,不定時的更新,歡迎start。

安卓預覽

Flutter+Mobx實戰,寫一個App應用

IOS預覽

Flutter+Mobx實戰,寫一個App應用

依賴庫

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)
複製程式碼

修改系統狀態列顏色

Flutter+Mobx實戰,寫一個App應用

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左側導航顯示的內容

Flutter+Mobx實戰,寫一個App應用

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元件,資料放專門的檔案去管理。

Flutter+Mobx實戰,寫一個App應用

Flutter+Mobx實戰,寫一個App應用

來看看如何實現的

通過點選然後改變資料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遇到的一些坑,歡迎大家一踩坑,大家有什麼意見和建議都可以提出來,謝謝。

相關文章