Flutter主題切換之flutter redux

Flutter主題切換之flutter redux

本文詳細講述怎樣在flutter中集成和使用redux,關于redux的概念揪罕、原理和實現(xiàn)淹朋,讀者可自行百度歧斟,本文不做累述。

個人博客

flutter redux

flutter redux組成

redux主要由Store梗肝、ActionReducer三部分組成

  • Store用于存儲和管理State
  • Action用于用戶觸發(fā)的一種行為
  • Reducer用于根據(jù)Action產(chǎn)生新的State

flutter redux流程

  1. Widget通過StoreConnector綁定Store中的State數(shù)據(jù)
  2. Widget通過Action觸發(fā)一種新的行為
  3. Reducer根據(jù)收到的Action更新State
  4. 更新Store中的State綁定的Widget

根據(jù)以上流程,我們實現(xiàn)項目中的主題切換功能齐唆。

項目集成

flutter redux庫

pub.dev地址
github地址

集成flutter redux

修改項目根目錄下pubspec.yaml,并添加依賴

flutter_redux: ^0.5.3

初始化Store

首先看下Store的構造函數(shù)冻河,如下面代碼所示

 Store(
    this.reducer, {
    State initialState,
    List<Middleware<State>> middleware = const [],
    bool syncStream: false,
    bool distinct: false,
  })
    : _changeController = new StreamController.broadcast(sync: syncStream) {
    _state = initialState;
    _dispatchers = _createDispatchers(
      middleware,
      _createReduceAndNotify(distinct),
    );
  }

根據(jù)上面的構造函數(shù)箍邮,我們首先需要創(chuàng)建State茉帅,并且還需要完成State初始化;然后需要創(chuàng)建Reducer锭弊;最后需要創(chuàng)建Middleware(暫不是本文需要講解的內(nèi)容)堪澎;

創(chuàng)建State

創(chuàng)建一個State對象AppState,用于儲存需要共享的主題數(shù)據(jù),并且完成AppState初始化工作味滞,如下面代碼所示

class AppState {
  ThemeData themeData;

  AppState({this.themeData});

  factory AppState.initial() => AppState(themeData: AppTheme.theme);
}

AppTheme類中定義了一個默認主題theme樱蛤,如下面代碼所示

class AppTheme {
  static final ThemeData _themeData = new ThemeData.light();

  static get theme {
    return _themeData.copyWith(
      primaryColor: Colors.black,
    );
  }
}

到此,完成了State的相關操作剑鞍。

創(chuàng)建Reducer

創(chuàng)建一個Reducer方法appReducer,為AppState類里的每一個參數(shù)創(chuàng)建一個Reducer昨凡,如下面代碼所示

AppState appReducer(AppState state, action) {
  return AppState(
    themeData: themeReducer(state.themeData, action),
  );
}

themeReducer將ThemeData和所有跟切換主題的行為綁定在一起,如下面代碼所示

final themeReducer = combineReducers<ThemeData>([
  TypedReducer<ThemeData, RefreshThemeDataAction>(_refresh),
]);

ThemeData _refresh(ThemeData themeData, action) {
  themeData = action.themeData;
  return themeData;
}

通過flutter reduxcombineReducersTypedReducerRefreshThemeDataAction_refresh綁定在一起,當用戶每次發(fā)出RefreshThemeDataAction時蚁署,都會觸發(fā)_refresh,用來更新themeData便脊。

創(chuàng)建Action

創(chuàng)建一個Action對象RefreshThemeDataAction,如下面代碼所示

class RefreshThemeDataAction{
  final ThemeData themeData;

  RefreshThemeDataAction(this.themeData);
}

RefreshThemeDataAction的參數(shù)themeData是用來接收新切換的主題。

代碼集成

創(chuàng)建Store所有的準備工作都已準備光戈,下面創(chuàng)建Store哪痰,如下面代碼所示

 final store = new Store<AppState>(
    appReducer,
    initialState: AppState.initial(),
 );

然后用StoreProvider加載store,MaterialApp通過StoreConnectorStore保持連接。到此我們已經(jīng)完成了flutter redux的初始化工作田度,如下面代碼所示

void main() {
  final store = new Store<AppState>(
    appReducer,
    initialState: AppState.initial(),
  );

  runApp(OpenGitApp(store));
}

class OpenGitApp extends StatelessWidget {
  final Store<AppState> store;

  OpenGitApp(this.store);

  @override
  Widget build(BuildContext context) {
    return new StoreProvider<AppState>(
      store: store,
      child: StoreConnector<AppState, _ViewModel>(
        converter: _ViewModel.fromStore,
        builder: (context, vm) {
          return new MaterialApp(
            theme: vm.themeData,
            routes: AppRoutes.getRoutes(),
          );
        },
      ),
    );
  }
}

StoreConnector通過converter_ViewModel中轉(zhuǎn)化store.state的數(shù)據(jù)妒御,最后通過builder返回實際需要更新主題的控件,這樣就完成了數(shù)據(jù)和控件的綁定镇饺。_ViewModel的代碼如下面所示

class _ViewModel {
  final ThemeData themeData;

  _ViewModel({this.themeData});

  static _ViewModel fromStore(Store<AppState> store) {
    return _ViewModel(
      themeData: store.state.themeData,
    );
  }
}

用戶行為

最后乎莉,只需要添加切換主題部分的代碼即可,這部分代碼是從官方gallery demo里的Style/Colors copy出來的奸笤,不做過多分析惋啃,如下面代碼所示

const double kColorItemHeight = 48.0;

class Palette {
  Palette({this.name, this.primary, this.accent, this.threshold = 900});

  final String name;
  final MaterialColor primary;
  final MaterialAccentColor accent;
  final int
      threshold; // titles for indices > threshold are white, otherwise black

  bool get isValid => name != null && primary != null && threshold != null;
}

final List<Palette> allPalettes = <Palette>[
  new Palette(
      name: 'RED',
      primary: Colors.red,
      accent: Colors.redAccent,
      threshold: 300),
  new Palette(
      name: 'PINK',
      primary: Colors.pink,
      accent: Colors.pinkAccent,
      threshold: 200),
  new Palette(
      name: 'PURPLE',
      primary: Colors.purple,
      accent: Colors.purpleAccent,
      threshold: 200),
  new Palette(
      name: 'DEEP PURPLE',
      primary: Colors.deepPurple,
      accent: Colors.deepPurpleAccent,
      threshold: 200),
  new Palette(
      name: 'INDIGO',
      primary: Colors.indigo,
      accent: Colors.indigoAccent,
      threshold: 200),
  new Palette(
      name: 'BLUE',
      primary: Colors.blue,
      accent: Colors.blueAccent,
      threshold: 400),
  new Palette(
      name: 'LIGHT BLUE',
      primary: Colors.lightBlue,
      accent: Colors.lightBlueAccent,
      threshold: 500),
  new Palette(
      name: 'CYAN',
      primary: Colors.cyan,
      accent: Colors.cyanAccent,
      threshold: 600),
  new Palette(
      name: 'TEAL',
      primary: Colors.teal,
      accent: Colors.tealAccent,
      threshold: 400),
  new Palette(
      name: 'GREEN',
      primary: Colors.green,
      accent: Colors.greenAccent,
      threshold: 500),
  new Palette(
      name: 'LIGHT GREEN',
      primary: Colors.lightGreen,
      accent: Colors.lightGreenAccent,
      threshold: 600),
  new Palette(
      name: 'LIME',
      primary: Colors.lime,
      accent: Colors.limeAccent,
      threshold: 800),
  new Palette(
      name: 'YELLOW', primary: Colors.yellow, accent: Colors.yellowAccent),
  new Palette(name: 'AMBER', primary: Colors.amber, accent: Colors.amberAccent),
  new Palette(
      name: 'ORANGE',
      primary: Colors.orange,
      accent: Colors.orangeAccent,
      threshold: 700),
  new Palette(
      name: 'DEEP ORANGE',
      primary: Colors.deepOrange,
      accent: Colors.deepOrangeAccent,
      threshold: 400),
  new Palette(name: 'BROWN', primary: Colors.brown, threshold: 200),
  new Palette(name: 'GREY', primary: Colors.grey, threshold: 500),
  new Palette(name: 'BLUE GREY', primary: Colors.blueGrey, threshold: 500),
];

class ColorItem extends StatelessWidget {
  const ColorItem(
      {Key key,
      @required this.index,
      @required this.color,
      this.prefix = '',
      this.onChangeTheme})
      : assert(index != null),
        assert(color != null),
        assert(prefix != null),
        super(key: key);

  final int index;
  final Color color;
  final String prefix;
  final Function(Color) onChangeTheme;

  String colorString() =>
      "#${color.value.toRadixString(16).padLeft(8, '0').toUpperCase()}";

  @override
  Widget build(BuildContext context) {
    return new Semantics(
      container: true,
      child: new Container(
        height: kColorItemHeight,
        padding: const EdgeInsets.symmetric(horizontal: 16.0),
        color: color,
        child: new SafeArea(
          top: false,
          bottom: false,
          child: FlatButton(
            child: new Row(
              mainAxisAlignment: MainAxisAlignment.spaceBetween,
              crossAxisAlignment: CrossAxisAlignment.center,
              children: <Widget>[
                new Text('$prefix$index'),
                new Text(colorString()),
              ],
            ),
            onPressed: () {
              onChangeTheme(color);
            },
          ),
        ),
      ),
    );
  }
}

class PaletteTabView extends StatelessWidget {
  static const List<int> primaryKeys = const <int>[
    50,
    100,
    200,
    300,
    400,
    500,
    600,
    700,
    800,
    900
  ];
  static const List<int> accentKeys = const <int>[100, 200, 400, 700];

  PaletteTabView({Key key, @required this.colors, this.onChangeTheme})
      : assert(colors != null && colors.isValid),
        super(key: key);

  final Palette colors;
  final Function(Color) onChangeTheme;

  @override
  Widget build(BuildContext context) {
    final TextTheme textTheme = Theme.of(context).textTheme;
    final TextStyle whiteTextStyle =
        textTheme.body1.copyWith(color: Colors.white);
    final TextStyle blackTextStyle =
        textTheme.body1.copyWith(color: Colors.black);
    final List<Widget> colorItems = primaryKeys.map((int index) {
      return new DefaultTextStyle(
        style: index > colors.threshold ? whiteTextStyle : blackTextStyle,
        child: new ColorItem(
            index: index,
            color: colors.primary[index],
            onChangeTheme: onChangeTheme),
      );
    }).toList();

    if (colors.accent != null) {
      colorItems.addAll(accentKeys.map((int index) {
        return new DefaultTextStyle(
          style: index > colors.threshold ? whiteTextStyle : blackTextStyle,
          child: new ColorItem(
              index: index,
              color: colors.accent[index],
              prefix: 'A',
              onChangeTheme: onChangeTheme),
        );
      }).toList());
    }

    return new ListView(
      itemExtent: kColorItemHeight,
      children: colorItems,
    );
  }
}

class ThemeSelectPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return StoreConnector<AppState, _ViewModel>(
        converter: _ViewModel.fromStore,
        builder: (context, vm) {
          return new DefaultTabController(
            length: allPalettes.length,
            child: new Scaffold(
              appBar: new AppBar(
                elevation: 0.0,
                title: const Text("主題色"),
                bottom: new TabBar(
                  isScrollable: true,
                  tabs: allPalettes
                      .map((Palette swatch) => new Tab(text: swatch.name))
                      .toList(),
                ),
              ),
              body: new TabBarView(
                children: allPalettes.map((Palette colors) {
                  return new PaletteTabView(
                    colors: colors,
                    onChangeTheme: vm.onChangeTheme,
                  );
                }).toList(),
              ),
            ),
          );
        });
  }
}

class _ViewModel {
  final Function(Color) onChangeTheme;

  _ViewModel({this.onChangeTheme});

  static _ViewModel fromStore(Store<AppState> store) {
    return _ViewModel(
      onChangeTheme: (color) {
        SharedPrfUtils.saveInt(SharedPrfKey.SP_KEY_THEME_COLOR, color.value);
        store.dispatch(RefreshThemeDataAction(AppTheme.changeTheme(color)));
      },
    );
  }
}

運行效果

執(zhí)行代碼,效果如下


enter image description here

源代碼

項目地址-OpenGit客戶端

參考文章

http://www.reibang.com/p/34a6224e0cf1

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末监右,一起剝皮案震驚了整個濱河市边灭,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌健盒,老刑警劉巖绒瘦,帶你破解...
    沈念sama閱讀 206,839評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異扣癣,居然都是意外死亡惰帽,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,543評論 2 382
  • 文/潘曉璐 我一進店門父虑,熙熙樓的掌柜王于貴愁眉苦臉地迎上來该酗,“玉大人,你說我怎么就攤上這事∥仄牵” “怎么了悔叽?”我有些...
    開封第一講書人閱讀 153,116評論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長爵嗅。 經(jīng)常有香客問我娇澎,道長,這世上最難降的妖魔是什么操骡? 我笑而不...
    開封第一講書人閱讀 55,371評論 1 279
  • 正文 為了忘掉前任九火,我火速辦了婚禮赚窃,結果婚禮上册招,老公的妹妹穿的比我還像新娘。我一直安慰自己勒极,他們只是感情好是掰,可當我...
    茶點故事閱讀 64,384評論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著辱匿,像睡著了一般键痛。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上匾七,一...
    開封第一講書人閱讀 49,111評論 1 285
  • 那天絮短,我揣著相機與錄音,去河邊找鬼昨忆。 笑死丁频,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的邑贴。 我是一名探鬼主播席里,決...
    沈念sama閱讀 38,416評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼拢驾!你這毒婦竟也來了奖磁?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,053評論 0 259
  • 序言:老撾萬榮一對情侶失蹤繁疤,失蹤者是張志新(化名)和其女友劉穎咖为,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體稠腊,經(jīng)...
    沈念sama閱讀 43,558評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡躁染,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,007評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了麻养。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片褐啡。...
    茶點故事閱讀 38,117評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖鳖昌,靈堂內(nèi)的尸體忽然破棺而出备畦,到底是詐尸還是另有隱情低飒,我是刑警寧澤,帶...
    沈念sama閱讀 33,756評論 4 324
  • 正文 年R本政府宣布懂盐,位于F島的核電站褥赊,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏莉恼。R本人自食惡果不足惜拌喉,卻給世界環(huán)境...
    茶點故事閱讀 39,324評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望俐银。 院中可真熱鬧尿背,春花似錦、人聲如沸捶惜。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,315評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽吱七。三九已至汽久,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間踊餐,已是汗流浹背景醇。 一陣腳步聲響...
    開封第一講書人閱讀 31,539評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留吝岭,地道東北人三痰。 一個月前我還...
    沈念sama閱讀 45,578評論 2 355
  • 正文 我出身青樓,卻偏偏與公主長得像苍碟,于是被迫代替她去往敵國和親酒觅。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 42,877評論 2 345

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