美文网首页
Flutter主题切换 flutter redux

Flutter主题切换 flutter redux

作者: 易含 | 来源:发表于2022-01-20 09:56 被阅读0次

    redux主要由Store、Action、Reducer三部分组成

    • Store用于存储和管理State
    • Action用于用户触发的一种行为
    • Reducer用于根据Action产生新的State

    flutter redux流程

    1.Widget通过StoreConnector绑定Store中的State数据
    2.Widget通过Action触发一种新的行为
    3.Reducer根据收到的Action更新State
    4.更新Store中的State绑定的Widget
    根据以上流程,我们实现项目中的主题切换功能。

    项目集成

    flutter redux库

    集成flutter redux

    修改项目根目录下pubspec.yaml,并添加依赖

    flutter_redux: ^0.5.3
    
    

    初始化Store

    首先看下Store的构造函数,如下面代码所示

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

    根据上面的构造函数,我们首先需要创建State,并且还需要完成State初始化;然后需要创建Reducer;最后需要创建Middleware(暂不是本文需要讲解的内容);

    创建State

    创建一个State对象AppState,用于储存需要共享的主题数据,并且完成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的相关操作。

    创建Reducer

    创建一个Reducer方法appReducer,为AppState类里的每一个参数创建一个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 redux的combineReducers与TypedReducer将RefreshThemeDataAction和_refresh绑定在一起,当用户每次发出RefreshThemeDataAction时,都会触发_refresh,用来更新themeData。

    创建Action

    创建一个Action对象RefreshThemeDataAction,如下面代码所示

    class RefreshThemeDataAction{
      final ThemeData themeData;
    
      RefreshThemeDataAction(this.themeData);
    }
    

    RefreshThemeDataAction的参数themeData是用来接收新切换的主题。

    代码集成

    创建Store所有的准备工作都已准备,下面创建Store,如下面代码所示

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

    然后用StoreProvider加载store,MaterialApp通过StoreConnector与Store保持连接。到此我们已经完成了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中转化store.state的数据,最后通过builder返回实际需要更新主题的控件,这样就完成了数据和控件的绑定。_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)));
          },
        );
      }
    }
    

    运行效果

    执行代码,效果如下


    相关文章

      网友评论

          本文标题:Flutter主题切换 flutter redux

          本文链接:https://www.haomeiwen.com/subject/vecjhrtx.html