As the fourth part of this series, this article mainly introduces the use of Redux in Flutter, and combines Redux with Flutter to achieve real-time topic switching and multi-language switching.

Article summary address:

A complete series of articles on Flutter

A series of articles on the world outside Flutter

As a responsive framework, Flutter realizes the logic of cross-frame rendering through state, which inevitably makes people associate with React and React Native. The Redux state management * “well-known” * under React is also applicable to Flutter.

We will eventually achieve the effect shown below. The corresponding code can be found in GSYGithubAppFlutter. The Redux library used in this Flutter is Flutter_redux.

A story,

The concept of Redux is state management, so why do you need Redux when you already have state? The advantage of using Redux is to share state and single data.

Imagine that the data of the logged-in user is used in multiple places in the App. In this case, if the user data is modified somewhere, it will be troublesome to synchronize the updates of each page.

However, after Redux is introduced, if a page modifies the current user information, all controls bound to Redux will be automatically refreshed synchronously by Redux. See! This saves us a certain amount of work, and a single data source is easy to manage in some scenarios, as are topics and multi-language switches we’ll talk about later.

As shown in the figure above, Redux consists of three parts: Store, Action and Reducer.

  • Action is used to define the request behavior for a data change.
  • Reducer is used to generate new state based on Action, and is generally a method.
  • Store stores and manages state.

Therefore, the general process is:

1. The Widget binds state data in the Store.

2. Widget publishes an Action via Action.

Reducer update state based on Action.

4. Update the state-bound Widget in the Store.

Following this process, we first create a Store. In the figure below, creating a Store requires a Reducer, which is actually a reducer method with state and action and returns the new state.

So we need to create a State object GSYState class to store the data that needs to be shared. For example, the following code: user information, theme, locale, etc.

Next, we need to define the Reducer method appReducer: bind each parameter in GSYState to the corresponding action and return the complete GSYState. Thus we have determined the State and Reducer to create the Store.

Class GSYState {/// User info User userInfo; /// ThemeData ThemeData; /// Locale Locale Locale; /// constructor GSYState({this.userinfo, this.themedata, this.locale}); Typedef State Reducer<State>(State State, dynamic action); GSYState appReducer(GSYState state, action) {returnGSYState(/// reduce the actions and userInfo in GSYState by customizingUserInfo) UserReducer(state.userinfo, action), use custom ThemeDataReducer to associate themeData and action in GSYState. ThemeDataReducer(state.themeData, action), // Use custom LocaleReducer to associate the locale in GSYState with the action locale: LocaleReducer(state.locale, action), ); }Copy the code

In the code above, each parameter of GSYState is returned by an independent custom Reducer. For example, themeData is generated using the ThemeDataReducer method. ThemeDataReducer binds themeData to a series of theme-related actions that are used to separate it from other arguments. In this way, each parameter in GSYState can be maintained and managed independently.

Continue the process as shown below, bind RefreshThemeDataAction class and _refresh method via Flutter_redux combineReducers and TypedReducer, An instance of ThemeData is eventually returned. Each time the user issues a RefreshThemeDataAction, the _refresh method is eventually triggered, and themeData in GSYState is updated.

import 'package:flutter/material.dart';
import 'package:redux/redux.dart'; // Via flutter_redux combineReducers, Create Reducer<State> final ThemeDataReducer = combineReducers<ThemeData>( State bind TypedReducer<ThemeData, RefreshThemeDataAction>(_refresh),]); State ThemeData _refresh(ThemeData ThemeData, Action) {ThemeData = action.themedata;returnthemeData; } class RefreshThemeDataAction {final ThemeData ThemeData; RefreshThemeDataAction(this.themeData); }Copy the code

OK, now we can have fun creating a Store. As shown in the code below, while creating a Store, we initialize GSYState with initialState, then load the Store with StoreProvider and wrap the MaterialApp. We have now completed our initial build in Redux.

void main() { runApp(new FlutterReduxApp()); } class FlutterReduxApp extends StatelessWidget {// create Store, State final Store = new store <GSYState>(appReducer, initialState: new GSYState( userInfo: User.empty(), themeData: new ThemeData( primarySwatch: GSYColors.primarySwatch, ), locale: Locale('zh'.'CH'))); FlutterReduxApp({Key key}) : super(key: key); @override Widget Build (BuildContext Context) {/// Through StoreProvider app Storereturnnew StoreProvider( store: store, child: new MaterialApp(), ); }}Copy the code

And then, use. As shown in the code below, the data is bound to the control by using StoreConnector in the build, converting the store.state data to converter, and finally returning the actual control to be rendered to the Builder. Of course, you can also use StoreBuilder.

Class DemoUseStorePage extends StatelessWidget {@override Widget Build (BuildContext Context) {/// Associated through StoreConnector The User GSYStatereturnNew StoreConnector<GSYState, User>(/// Return userInfo in GSYState to Converter via Converter: (store) => store.state.userInfo, // return the actual rendered control Builder in userInfo: (context, userInfo) {returnnew Text( userInfo.name, ); }); }}Copy the code

Finally, when you need to trigger an update, all you need is the following code.

 StoreProvider.of(context).dispatch(new UpdateUserAction(newUserInfo));
Copy the code

So, or simple business logic, Redux has no advantage and is even cumbersome. But once the framework is put together, it is particularly pleasant to work with complex business logic.

Second, the theme

Theme setting is officially supported by Flutter by default. The MaterialApp provides the theme parameter to set the theme. You can then obtain the current ThemeData using theme.

The creation of ThemeData provides many parameters, mainly the primarySwatch parameter. PrimarySwatch is a MaterialColor object, internally composed of 10 different shades of color, ideal for theme hues.

Flutter provides a number of theme colors by default, as shown in the following figure and code. You can also customize the theme colors from the MaterialColor.

MaterialColor primarySwatch = const MaterialColor(
    primaryValue,
    const <int, Color>{
      50: const Color(primaryLightValue),
      100: const Color(primaryLightValue),
      200: const Color(primaryLightValue),
      300: const Color(primaryLightValue),
      400: const Color(primaryLightValue),
      500: const Color(primaryValue),
      600: const Color(primaryDarkValue),
      700: const Color(primaryDarkValue),
      800: const Color(primaryDarkValue),
      900: const Color(primaryDarkValue),
    },
  );
Copy the code

So how do you achieve real-time topic switching? Through Redux, of course!

Previously we created themeData in GSYState and set it to the Theme parameter of the MaterialApp. Then we can switch the theme by changing themeData with Dispatch.

Note that since your MaterialApp is also a StatefulWidget, as shown in the code below, it also needs to be wrapped with StoreBuilder so that we can modify the theme via Dispatch, Get the Theme color from theme.of (context).primaryColor.

@override Widget Build (BuildContext Context) {/// Through StoreProvider app Storereturn new StoreProvider(
      store: store,
      child: new StoreBuilder<GSYState>(builder: (context, store) {
        returnnew MaterialApp( theme: store.state.themeData); })); } ···· ThemeData ThemeData = new ThemeData(primarySwatch: colors[index]); store.dispatch(new RefreshThemeDataAction(themeData));Copy the code

Iii. Internationalization

Internationalization of Flutter looks slightly complicated according to the official documentation and there is no mention of real-time switching, so here is a quick implementation. And, of course, Redux!

As shown in the figure above, the general process is also through the default MaterialApp Settings, and the customized multi-language needs to achieve: LocalizationsDelegate and Localizations. The final process loads the delegate with Localizations using Locale. So what we’re going to do is:

  • Implement LocalizationsDelegate.
  • Realize the Localizations.
  • Switching languages by Store Locale.

As shown in the code below, creating a custom delegate requires inheriting the LocalizationsDelegate object, which implements the load method primarily. We can use the locale parameter of the method to determine the language to be loaded, and then return the GSYLocalizations class that we have customized for many languages, and finally provide the LocalizationsDelegate externally through the static delegate.

/** * Created by guoshuyu * Date: 2018-08-15 */ class GSYLocalizationsDelegate extends LocalizationsDelegate<GSYLocalizations> { GSYLocalizationsDelegate(); @override bool isSupported(Locale Locale) {/// Chinese and English are supportedreturn ['en'.'zh'].contains(locale.languageCode); Override Future<GSYLocalizations> Load (locale locale) {override Future<GSYLocalizations> load(locale locale) {return new SynchronousFuture<GSYLocalizations>(new GSYLocalizations(locale));
  }
  
  @override
  bool shouldReload(LocalizationsDelegate<GSYLocalizations> old) {
    return false; GSYLocalizationsDelegate = new GSYLocalizationsDelegate(); }Copy the code

GSYLocalizations is a custom object, as shown in the following code, which returns the locale.languagecode, the implementation of GSYStringBase, based on the Locale in which it was created.

Because GSYLocalizations objects are eventually loaded with Localizations, Locale is also given by the delegate at that time. In this context, you can obtain GSYLocalizations by using Localizations. Of, for example, GSYLocalizations. Of (context).currentcurrentcurrent_name.

Classes GSYLocalizations {final Locale Locale; GSYLocalizations(this.locale); //GSYStringEn and GSYStringEn both inherit GSYStringBase static Map<String, GSYStringBase> _localizedValues = {'en': new GSYStringEn(),
    'zh': new GSYStringZh(),
  };

  GSYStringBase get currentLocalized {
    return_localizedValues[locale.languageCode]; GSYStringBase static GSYLocalizations of(BuildContext context) {// GSYStringBase static GSYLocalizations of(BuildContext context) {returnLocalizations.of(context, GSYLocalizations); }} /// language entity base class abstract class GSYStringBase {String app_name; } class GSYStringEn extends GSYStringBase {@override String app_name ="GSYGithubAppFlutter"; } /// Use GSYLocalizations. Of (context).currentcurrentcurrent_nameCopy the code

Now that we’re talking about delegates, we’re talking about Localizations. As can be seen from the above flow chart, Localizations provides a override method to construct Localizations, which can set locale, and what we need is the real-time dynamic display of switching languages.

As follows, we create a GSYLocalizations Widget, bind Store with StoreBuilder, and wrap Localizations. Override with the page we want to build. Bind locale in Store to locale of Localizations.

class GSYLocalizations extends StatefulWidget {
  final Widget child;

  GSYLocalizations({Key key, this.child}) : super(key: key);

  @override
  State<GSYLocalizations> createState() {
    return new _GSYLocalizations();
  }
}
class _GSYLocalizations extends State<GSYLocalizations> {

  @override
  Widget build(BuildContext context) {
    returnNew StoreBuilder<GSYState>(Builder: (context, store) {/// Implement real-time multilanguage switching with StoreBuilder and Localizationsreturnnew Localizations.override( context: context, locale: store.state.locale, child: widget.child, ); }); }}Copy the code

The following code is used, and finally GSYLocalizations is used in the MaterialApp. Switch Locale using store.dispatch.

@override Widget Build (BuildContext Context) {/// Through StoreProvider app Storereturn new StoreProvider(
      store: store,
      child: new StoreBuilder<GSYState>(builder: (context, store) {
        returnNew MaterialApp(/// Multilanguage implementation of localizationsDelegates: [ GlobalMaterialLocalizations.delegate, GlobalWidgetsLocalizations.delegate, GSYLocalizationsDelegate.delegate, ], locale: store.state.locale, supportedLocales: [store.state.locale], routes: { HomePage.sName: (context) {/// Wrap the first layer with Localizations. Override. - herereturnnew GSYLocalizations( child: new HomePage(), ); }}); })); } / / / switch static changeLocale (Store < GSYState > Store, int index) {Locale Locale = Store. State. PlatformLocale; switch (index) {case 1:
        locale = Locale('zh'.'CH');
        break;
      case 2:
        locale = Locale('en'.'US');
        break;
    }
    store.dispatch(RefreshLocaleAction(locale));
  }
Copy the code

Finally, the topic and multilingual Settings are complete when the status is logged when the change is made and dispatched after it is taken out at startup.

Since then, the fourth chapter is finally over! (/ / / del / / /)

Resources to recommend

  • Making: github.com/CarGuo/
  • Open Source Flutter complete project:Github.com/CarGuo/GSYG…
  • Open Source Flutter Multi-case learning project:Github.com/CarGuo/GSYF…
  • Open Source Fluttre Combat Ebook Project:Github.com/CarGuo/GSYF…

Full open source project recommendation:

  • GSYGithubAppWeex
  • GSYGithubApp React Native