Switch between Flutter themes and flutter redux

Posted by bluethundr on Thu, 20 Jan 2022 08:30:36 +0100

redux is mainly composed of Store, Action and Reducer

  • Store is used to store and manage State
  • Action is a kind of behavior triggered by the user
  • Reducer , is used to generate a new , State according to the Action

    Fluent Redux process

    1. The Widget binds the "State" data in the "Store" through the "StoreConnector"
    2. Widget triggers a new behavior through Action
    3. Reducer updates the State according to the received Action
    4. Update the State bound Widget in the Store
    According to the above process, we realize the theme switching function in the project.

    Project integration

    Fluent Redux Library

    Integrated fluent Redux

    Modify pubspec. In the root directory of the project Yaml and add dependencies

    flutter_redux: ^0.5.3
    ​

    Initialize Store

    First, take a look at the constructor of {Store}, as shown in the following code

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

    According to the above constructor, we first need to create a State and complete the initialization of the State; Then you need to create a} Reducer; Finally, you need to create} Middleware (not the content to be explained in this article for the time being);

Create State

Create a "State" object "AppState", which is used to store the subject data to be shared, and complete the "AppState" initialization, as shown in the following code

class AppState {
  ThemeData themeData;

  AppState({this.themeData});

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

The AppTheme} class defines a default theme} theme, as shown in the following code

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

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

At this point, the related operations of State have been completed.

Create Reducer

Create a {Reducer} appReducer method to create a} Reducer for each parameter in the} AppState class, as shown in the following code

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

themeReducer binds ThemeData with all behaviors related to topic switching, as shown in the following code

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

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

Use # combineReducers # and # TypedReducer # of # fluent reduce # to connect # RefreshThemeDataAction # and # TypedReducer #_ refresh is bound with , which will be triggered every time the user issues , RefreshThemeDataAction ,_ refresh, used to update} themeData.

Create Action

Create an Action object RefreshThemeDataAction, as shown in the following code

class RefreshThemeDataAction{
  final ThemeData themeData;

  RefreshThemeDataAction(this.themeData);
}

The parameter themeData of RefreshThemeDataAction is used to receive the newly switched topic.

Code integration

All preparations for creating a Store are ready. Create a Store below, as shown in the following code

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

Then, load the Store with the StoreProvider, and the MaterialApp maintains a connection with the StoreProvider through the StoreConnector. So far, we have completed the initialization of {fluent Redux}, as shown in the following code

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 , through , converter , in_ Convert the store in ViewModel The data of state , and finally return the control that actually needs to update the theme through , builder , so as to complete the binding of data and control. ​​_ The code of ViewModel is shown below

class _ViewModel {
  final ThemeData themeData;

  _ViewModel({this.themeData});

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

User behavior

Finally, you only need to add the code for switching topics, which is derived from the Style/Colors copy in the official gallery demo without too much analysis, as shown in the following code

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("Theme color"),
                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)));
      },
    );
  }
}

Operation effect

Execute the code with the following effect

Topics: Android Flutter