This article was first published on the political cloud front blog: 【Flutter Skills 】 You have to be a state management Provider

preface

Provider, Google’s official recommendation for the Flutter page state management component, is essentially a wrapper around inheritedWidgets to make them easier to use and reuse. There’s not much to say about InheritedWidget, but this article will give you a comprehensive overview of the use of InheritedWidget to use in your business scenario.

Example source code for this article: github.com/xiaomanziji…

usage

Step1: Add a dependency

dependencies:
  flutter:
    sdk: flutter
  # The following adds the Cupertino Icons font to your application.
  # Use with the CupertinoIcons class for iOS style icons.
  provider: ^ 4.0.4Copy the code

Step2: observe the structure

After executing the Flutter pub get, you can see the source code of the PROVIDER SDK in the project. The structure is as follows:

Step3: example introduction

This example will show you how to use the basic components of Provider, Including but not limited to ChangeNotifier, NotifierProvider, Consumer, Selector, ProxyProvider, FutureProvider, StreamProvider.

Step4: create a ChangeNotifier

Let’s create a new Model1, inherit from ChangeNotifier, and make it one of our data providers.

class Model1 extends ChangeNotifier {
  int _count = 1;
  int get count => _count;
  set count(intvalue) { _count = value; notifyListeners(); }}Copy the code

Tracing the ChangeProvider source code, we found that it does not belong to a Provider. It is actually a Change_provider. Dart file defined under the Flutter SDK Foundation. ChangeNotifier implements the Listenable abstract class, which maintains an ObserverList. Listenable class source:

abstract class Listenable {
  const Listenable();
  factory Listenable.merge(List<Listenable> listenables) = _MergingListenable;
  void addListener(VoidCallback listener);
  void removeListener(VoidCallback listener);
}
Copy the code

As you can see, the main methods provided are addListener and removeListener. ChangeNotifier class source:

class ChangeNotifier implements Listenable {
  ObserverList<VoidCallback> _listeners = ObserverList<VoidCallback>();
  bool _debugAssertNotDisposed() {
    assert(() {
      if (_listeners == null) {
        throw FlutterError.fromParts(<DiagnosticsNode>[
          ErrorSummary('A $runtimeType was used after being disposed.'),
          ErrorDescription('Once you have called dispose() on a $runtimeType, it can no longer be used.')]); }return true; } ());return true;
  }
  @protected
  bool get hasListeners {
    assert(_debugAssertNotDisposed());
    return _listeners.isNotEmpty;
  }
  void addListener(VoidCallback listener) {
    assert(_debugAssertNotDisposed());
    _listeners.add(listener);
  }
  @override
  void removeListener(VoidCallback listener) {
    assert(_debugAssertNotDisposed());
    _listeners.remove(listener);
  }
  @mustCallSuper
  void dispose() {
    assert(_debugAssertNotDisposed());
    _listeners = null;
  }
  @protected
  @visibleForTesting
  void notifyListeners() {
    assert(_debugAssertNotDisposed());
    if(_listeners ! =null) {
      final List<VoidCallback> localListeners = List<VoidCallback>.from(_listeners);
      for (VoidCallback listener in localListeners) {
        try {
          if (_listeners.contains(listener))
            listener();
        } catch (exception, stack) {
          FlutterError.reportError(FlutterErrorDetails(
            exception: exception,
            stack: stack,
            library: 'foundation library',
            context: ErrorDescription('while dispatching notifications for $runtimeType'),
            informationCollector: () sync* {
              yield DiagnosticsProperty<ChangeNotifier>(
                'The $runtimeType sending notification was'.this, style: DiagnosticsTreeStyle.errorProperty, ); })); } } } } }Copy the code

In addition to implementing addListener and removeListener, two methods are provided, dispose and notifyListeners. In Model1, when we change the count value, the notifyListeners method is called to notify the UI of the update.

Step5: create ChangeNotifierProvider

Sample introduction

Method 1: Use ChangeNotifierProvider

return ChangeNotifierProvider(
      create: (context) {
        return Model1();
      },
      child: MaterialApp(
        theme: ArchSampleTheme.theme,
        home: SingleStatsView(),
      ),
);
Copy the code

Here, the ChangeNotifier (Model1) is connected through the Create of ChangeNotifierProvider. The scope is the MaterialApp specified by the Child. Here we use SingleStatsView as the home page, and Model1 is used in SingleStatsView as the data source. Note that do not place the scope of all the states in the MaterialApp. Strictly control the scope based on the actual service requirements. A large number of global states may seriously affect the application performance.

class SingleStatsView extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: <Widget>[
          FlatButton(
            color: Colors.blue,
            child: Text('Model1 count++'),
            onPressed: () {
              Provider.of<Model1>(context, listen: false).count++;
            },
          ),
          Padding(
            padding: const EdgeInsets.only(bottom: 8.0),
            child: Text('Listen for Model count changes',
                style: Theme.of(context).textTheme.title),
          ),
          Padding(
            padding: const EdgeInsets.only(bottom: 8.0),
            child: Text('Model1 count:${Provider.of<Model1>(context).count}', style: Theme.of(context) .textTheme .subhead .copyWith(color: Colors.green)), ), ], ), ); }}Copy the code

The second way: by ChangeNotifierProvider. The value

return ChangeNotifierProvider.value(
        value: Model1(),
        child: MaterialApp(
          theme: ArchSampleTheme.theme,
          home: SingleStatsView(),
     ));
Copy the code

As you can see, the same as the method is similar. The first method uses create to create the ChangeNotifier, here uses value to create. To trace the ChangeNotifierProvider source code:

class ChangeNotifierProvider<T extends ChangeNotifier> extends ListenableProvider<T> {
  static void_dispose(BuildContext context, ChangeNotifier notifier) { notifier? .dispose(); } ChangeNotifierProvider({ Key key,@required Create<T> create,
    bool lazy,
    Widget child,
  }) : super(
          key: key,
          create: create,
          dispose: _dispose,
          lazy: lazy,
          child: child,
        );
        
  ChangeNotifierProvider.value({
    Key key,
    @required T value,
    Widget child,
  }) : super.value(
          key: key,
          value: value,
          child: child,
        );
}
Copy the code

Step6: Listen for state changes in the page and use other methods

The sample

ValueListenableBuilder (ValueListenableBuilder) is a UI builder that listens for changes to specified values.

ValueNotifier

1. New ValueNotifier

final ValueNotifier<int> _counter = ValueNotifier<int> (0);
Copy the code

Specify it to the count in Model1 in the Builder method so that _counter will also listen when the count in Model1 changes.

_counter.value = Provider.of<Model1>(context).count;
Copy the code

2. Associated with ValueListenableBuilder valueListenable of ValueListenableBuilder can be bound with a ValueNotifier to listen to the change of ValueNotifier value.

ValueListenableBuilder(
            valueListenable: _counter,
            builder: (context, count, child) => Text(
                'ValueListenableBuilder count:$count'),),Copy the code

ValueNotifier source code:

class ValueNotifier<T> extends ChangeNotifier implements ValueListenable<T> {
  ValueNotifier(this._value);
  @override
  T get value => _value;
  T _value;
  set value(T newValue) {
    if (_value == newValue)
      return;
    _value = newValue;
    notifyListeners();
  }
  @override
  String toString() => '${describeIdentity(this)}($value) ';
}
Copy the code

As can be seen, ValueNotifer inherits from ChangeNotifier and implements ValueListenable. The special feature is that it calls notifyListeners when setting value, thus realizing the monitoring of state changes.

MultiProvider

Sample introduction

The MultiProvider comes in handy when business scenarios are complex and our pages need to listen to multiple ChangeNotifier data sources. The example is extended on SingleStatsView, where we create a new MultiStatsView that listens for data changes in Model1 and Model2.

Model2

class Model2 extends ChangeNotifier {
  int _count = 1;
  int get count => _count;
  set count(intvalue) { _count = value; notifyListeners(); }}Copy the code

MultiStatsView

class MultiStatsView extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: <Widget>[
          FlatButton(
            color: Colors.blue,
            child: Text('Model1 count++'),
            onPressed: () {
              Provider.of<Model1>(context, listen: false).count++;
            },
          ),
          FlatButton(
            color: Colors.blue,
            child: Text('Model2 count++'),
            onPressed: () {
              Provider.of<Model2>(context, listen: false).count++;
            },
          ),
          Padding(
            padding: const EdgeInsets.only(bottom: 8.0),
            child: Text('Listen for Model count changes',
                style: Theme.of(context).textTheme.title),
          ),
          Padding(
            padding: const EdgeInsets.only(bottom: 8.0),
            child: Text('Model1 count:${Provider.of<Model1>(context).count}',
                style: Theme.of(context)
                    .textTheme
                    .subhead
                    .copyWith(color: Colors.green)),
          ),
          Padding(
            padding: const EdgeInsets.only(bottom: 24.0),
            child: Text('Model2 count:${Provider.of<Model2>(context).count}', style: Theme.of(context) .textTheme .subhead .copyWith(color: Colors.red)), ), ], ), ); }}Copy the code

Using the MultiProvider association with the MultiStatsView, we can see that the MultiProvider provides an array of providers to which we can put the ChangeNotifierProvider.

return MultiProvider(
      providers: [
        ChangeNotifierProvider.value(value: Model1()),
        ChangeNotifierProvider.value(value: Model2()),
      ],
      child: MaterialApp(
        theme: ArchSampleTheme.theme,
        localizationsDelegates: [
          ArchSampleLocalizationsDelegate(),
          ProviderLocalizationsDelegate(),
        ],
        home: MultiStatsView(),
      ),
);
Copy the code

If only the Model1-associated ChangeNotifierProvider is provided for MultiStatsView, you will see an error like this:

═ ═ ╡ EXCEPTION CAUGHT BY WIDGETS LIBRARY ╞ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ The following ProviderNotFoundException was thrown building MultiStatsView(dirty, dependencies: [_LocalizationsScope-[GlobalKey#48c61], _InheritedTheme, _DefaultInheritedProviderScope<Model1>]):
Error: Could not find the correct Provider<Model2> above this MultiStatsView Widget
To fix, please:
  * Ensure the Provider<Model2> is an ancestor to this MultiStatsView Widget
  * Provide types to Provider<Model2>
  * Provide types to Consumer<Model2>
  * Provide types to Provider.of<Model2>()
  * Ensure the correct `context` is being used.
Copy the code

This is because Model2 is not registered.

ProxyProvider

Starting from 3.0.0, ProxyProvider is provided. The ProxyProvider can aggregate the values of other providers into a new object and pass the result to the provider. New objects are updated when their dependent host provider is updated.

Create a new User class

class User {
  var name;
  User(this.name);
}
Copy the code

We aggregate Model1 into a User object through the ProxyProvider, and then take the name in the User object for rendering. s

ProxyProvider<Model1, User>(
              update: (context, value, previous) => User("change value ${value.count}"),
              builder: (context, child) => Text(
                  'ProxyProvider: ${Provider.of<User>(context).name}',
                  style: Theme.of(context).textTheme.title),
            )
Copy the code

It can be seen that through ProxyProvider, we directly call provider.of

(context) value. The Provider associated with the User is not registered, but it can also run effectively.

FutureProvider

As the name suggests, this Provider is associated with asynchronous execution and is used similarly to FutureBuilder. Here, the initial values of Model1 are updated after 2 seconds of simulation with FutureProvider. You can specify the initial value in initialData, the create method specifies the specific asynchronous task, and the Builder method can use Provider.of to fetch the value returned by the asynchronous task execution to render the page. The new create/update callback functions are lazily loaded, meaning that they are not called until the corresponding value is first read. Not when the provider is first created. If this feature is not required, you can set the value of the lazy property to false.

FutureProvider<Model1>(
              create: (context) {
                return Future.delayed(Duration(seconds: 2)) .then((value) => Model1().. count =11);
              },
              initialData: Model1(),
              builder: (context, child) => Text(
                  'FutureProvider ${Provider.of<Model1>(context).count}',
                  style: Theme.of(context).textTheme.title),
            ),
Copy the code

StreamProvider

As the name suggests, StreamProvider is also a Provider for asynchronous execution and is used similarly to StreamBuilder. The StreamProvider simulation is used here to update the initial values of Model1 every second. The rest of the parameters are used similarly to FutureProvider.

StreamProvider(create: (context) {
              return Stream.periodic(Duration(seconds: 1), (data) => Model1().. count = data); }, initialData: Model1(), builder: (context, child) => Text('StreamProvider: ${Provider.of<Model1>(context).count}',
                  style: Theme.of(context).textTheme.title),
            ),
Copy the code

Consumer

The specific usage is as follows. The parameters in builder are Context Context, T value, Widget Child and value, namely Model1. The type of value is the same as Model1. The Builder method returns the Widget, which is packaged by the Consumer and is rebuilt when the monitored Model value changes.

Consumer<Model1>(
        builder: (context, model, child) {
          return Text('Model1 count:${model.count}'); },)Copy the code

Selector

As you can see, a Selector is very similar to a Consumer, except that a Selector can customize its return type. In the Selector below, we’re listening for a change in count in Model1, so the return type is defined as Int. So the arguments in the Builder method are Context Context, T value, Widget Child, and the value here is the same type as the return type defined in the Selector. The Builder method returns the Widget, that is, the Widget wrapped in a Selector. We can trigger the Widget Rebuild by specifying to listen for a change in a value in the ChangeNotifier.

Selector<Model1, int>(
  builder: (context, count, child) => Text(
    "Selector example demo:$count",
    style: Theme.of(context).textTheme.title,
  ),
  selector: (context, model) => model.count,
),
Copy the code

Source:

class Selector<A.S> extends Selector0<S> {
  Selector({
    Key key,
    @required ValueWidgetBuilder<S> builder,
    @required S Function(BuildContext, A) selector,
    ShouldRebuild<S> shouldRebuild,
    Widget child,
  })  : assert(selector ! =null),
        super(
          key: key,
          shouldRebuild: shouldRebuild,
          builder: builder,
          selector: (context) => selector(context, Provider.of(context)),
          child: child,
        );
}
Copy the code

So you can see that Selector inherits from Selector0, and you can trace the source of Selector0, and it creates widgets through buildWithChild

class _Selector0State<T> extends SingleChildState<Selector0<T>> {
  T value;
  Widget cache;
  Widget oldWidget;
  @override
  Widget buildWithChild(BuildContext context, Widget child) {
    final selected = widget.selector(context);
    varshouldInvalidateCache = oldWidget ! = widget || (widget._shouldRebuild ! =null && widget._shouldRebuild.call(value, selected)) ||
        (widget._shouldRebuild == null&&!const DeepCollectionEquality().equals(value, selected));
    if (shouldInvalidateCache) {
      value = selected;
      oldWidget = widget;
      cache = widget.builder(
        context,
        selected,
        child,
      );
    }
    returncache; }}Copy the code

So here’s A, S, and you can see that A is the input to the selector function, and S is the return value, and we’re converting A to Provider through Provider.of(context). Compare the Selector example above, where A corresponds to Model1 and S corresponds to count. There’s also a shouldRebuild, so look at the function definition:

typedef ShouldRebuild<T> = bool Function(T previous, T next);
Copy the code

If true is returned, the page will be rerendered. If false is returned, the page will not be rerendered. See _Selector0State’s buildWithChild method for the logic.

As you can see, compared to consumers, selectors reduce the scope of data listening and can customize whether to refresh the page according to their own business logic, thus avoiding many unnecessary page flushers and improving performance.

In the selector. Dart file in the Provider SDK code structure, you can see that Selector2, Selector3, Selector4, Selector5, and Selector6 are also defined. Here’s Selector2 as an example:

class Selector2<A.B.S> extends Selector0<S> {
  Selector2({
    Key key,
    @required ValueWidgetBuilder<S> builder,
    @required S Function(BuildContext, A, B) selector,
    ShouldRebuild<S> shouldRebuild,
    Widget child,
  })  : assert(selector ! =null),
        super(
          key: key,
          shouldRebuild: shouldRebuild,
          builder: builder,
          selector: (context) => selector(
            context,
            Provider.of(context),
            Provider.of(context),
          ),
          child: child,
        );
}
Copy the code

As you can see, Selector2 also inherits Selector0, except that the selector function has two input arguments, A and B, and S is the return value of the function. Selector2 allows you to listen to two providers, and you can customize the value of S from these two providers. Other selectors are just listening to more providers. If there are more than six providers to listen to, you need to customize the Selector method. In the example, we use Selector2 to listen for changes in Both Model1 and Model2 and calculate the sum of the count in both models.

Selector2<Model1, Model2, int>(
              selector: (context, model1, model2) {
                return model1.count + model2.count;
              },
              builder: (context, totalCount, child) {
                return Text(
                    'Model1 'and' Model2 'count:$totalCount');
       }
Copy the code

Selector3 Selector4… Consumer2, Consumer3… I won’t repeat it here.

Consumer and Selector performance validation

From the example above, we already know something about consumers and selectors. The Consumer can avoid unnecessary rebuild of widgets. When the value monitored by the Consumer does not change, the widget is not rebuilt. Selector provides more precise listening than Consumer, and supports custom rebuild, which gives you more flexibility to control widget rebuild issues.

Let’s verify the Consumer and Selector rebuild cases. In the figure of Step3, we define two buttons, one for summing count in Model1 and one for summing count in Model2. Demonstrates both Selector2 and Consumer; Defines Widget1, Widget2, and Widget4 with Selector to verify the rebuild situation. The count value of Model1 is marked in green, and the count value of Model2 is marked in red.

Example source code for this article: github.com/xiaomanziji…

Widget1, print “Widget1 build” in the build method.

class Widget1 extends StatefulWidget {
  @override
  State<StatefulWidget> createState() => StateWidget1();
}
class StateWidget1 extends State<Widget1> {
  @override
  Widget build(BuildContext context) {
    print('Widget1 build');
    return Text('Widget1', style: Theme.of(context).textTheme.subhead); }}Copy the code

Widget2, print “Widget2 build” in the build method. Widget3, listening for the Change in Model2 count, prints “Widget3 build” in the Builder method.

Widget4, print “Widget4 build” in the build method, the build method returns a Selector, print “Widget4 Selector build” in the Selector builder method, The Selector listens for changes in Model1 count.

class Widget4 extends StatefulWidget {
  @override
  State<StatefulWidget> createState() => StateWidget4();
}
class StateWidget4 extends State<Widget4> {
  @override
  Widget build(BuildContext context) {
    print('Widget4 build');
    return Selector<Model1, int>(
      builder: (context, count, child) {
        print('Widget4 Selector build');
        return Text('Widget4 Model1 count:$count', style: Theme.of(context).textTheme.subhead.copyWith(color: Colors.green),); }, selector: (context, model) => model.count, ); }}Copy the code

All conditions are met, we run the StatsView page, the log print is as follows:

Selector2 build
Model1 Consumer build
Model2 Consumer build
Widget1 build
Widget2 build
Widget3 build
Widget4 build
Widget4 Selector build
Copy the code

As you can see, the widget renders from top to bottom according to the layout order of the page elements. Click the Model1 Count ++ button, and you can see that the count has been updated in all the green places. The following logs are displayed:

Selector2 build
Model1 Consumer build
Model2 Consumer build
Widget1 build
Widget2 build
Widget3 build
Widget4 build
Widget4 Selector build
Copy the code

What!!!!!! How about updating Model1’s count, just like loading the page log for the first time? Shouldn’t build only listen for Model1 related widgets? Let’s change the code to make Widget4 a global variable and initialize it with initState.

class StateStatsView extends State<StatsView> {
  final ValueNotifier<int> _counter = ValueNotifier<int> (0);
  var widget4;
  @override
  void initState() {
    widget4 = Widget4();
    super.initState();
  }
  @override
  Widget build(BuildContext context) {
    _counter.value = Provider.of<Model1>(context).count;
    return Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: <Widget>[
          ..., // Omit part of the instance sourceWidget1(), Widget2(), Widget3(), widget4, ], ), ); }}Copy the code

After running, click Model1 count++ button again, and the log will be printed as follows:

Selector2 build
Model1 Consumer build
Model2 Consumer build
Widget1 build
Widget2 build
Widget3 build
Widget4 Selector build
Copy the code

As you can see, the “Widget4 Build” log is no longer printed, but the “Widget4 Selector build” log is still printed. Let’s change the code to make the selector in Widget4 a global variable and initialize it with initState.

class Widget4 extends StatefulWidget {
  @override
  State<StatefulWidget> createState() => StateWidget4();
}
class StateWidget4 extends State<Widget4> {
  Selector<Model1, int> selector;
  @override
  void initState() {
    selector = buildSelector();
    super.initState();
  }
  @override
  Widget build(BuildContext context) {
    print('Widget4 build');
    return selector;
  }
  Selector<Model1, int> buildSelector() {
    return Selector<Model1, int>(
    builder: (context, count, child) {
      print('Widget4 Selector build');
      return Text('Widget4 Model1 count:$count', style: Theme.of(context).textTheme.subhead.copyWith(color: Colors.green),); }, selector: (context, model) => model.count, ); }}Copy the code

After running, click Model1 count++ button again, and the log will be printed as follows:

Selector2 build
Model1 Consumer build
Model2 Consumer build
Widget1 build
Widget2 build
Widget3 build
Copy the code

As you can see, the log for “Widget4 Selector build” is not printed, and the count in Model1 that Widget4 is listening on is updated properly. From the previous three steps of validation, we know that when ChangeNotifier (in this case Model1) notifyListener, all widgets in Model1 scope will trigger build, Selector, A Consumer is essentially a Widget. When we need a Selector or Consumer package for our data, it is recommended to create the Widget before initState to avoid unnecessary builds.

other

1.listen

If we change the “Model1 count++” button and click the event code down

FlatButton(
            color: Colors.blue,
            child: Text('Model1 count++'), onPressed: () { Provider.of<Model1>(context).count++; },),Copy the code

The difference is that there is no LISTEN :false. Click the button and you will see the following error log:

Tried to listen to a value exposed with provider, from outside of the widget tree.
This is likely caused by an event handler (like a button's onPressed) that called
Provider.of without passing `listen: false`.
To fix, write:
Provider.of<Model1>(context, listen: false);
It is unsupported because may pointlessly rebuild the widget associated to the
event handler, when the widget tree doesn't care about the value.
Copy the code

Listen =true indicates that the widget is rebuilt if the value in the listened ChangeNotifier changes. Listen =false indicates that the widget is not rebuilt. To call the Provider. Of method outside the Widget tree, you must add LISTEN :false.

2. The extension

Provider has supported extension methods since version 4.1.0. The current example is based on version 4.0.5+1. For details, see Changelog.

before after
Provider.of(context, listen: false) context.read()
Provider.of(context) context.watch

Other state management components

component introduce
Provider Official recommendation based on InheritedWidget
ScopedModel Based on the InheritedWidget implementation, andProviderThe principle and the writing are very similar
BLoC Stream based implementation, this pattern requires some understanding of reactive programming such as RxDart, RxJava. Core concept: Input eventsSink<Event> input, output eventsStream<Data> output
Redux The Redux package implementation of Flutter in the React ecosystem in Web development is popular at the front end, a one-way data flow architecture. Core concept: Store objectsStore, event operationAction, process and distribute eventsReducer, component refreshView
Mobx Originally a JavaScript state management library, it was migrated to the DART version. Core concepts:Observables,Actions,Reactions

The rest of the components are not covered here, but readers are interested in studying the implementation of the other components.

conclusion

This article mainly introduces the official recommended Provider components, combined with the source code and the problems encountered in the process of business development, introduced several commonly used ways to use, hope that you can be skilled in using, in business scenarios can be flexible use.

Recommended reading

React source code, five minutes with you to master priority queues

Writing maintainable quality code: Component abstraction and granularity

, recruiting

ZooTeam (ZooTeam), a young and creative team, belongs to the product RESEARCH and development department of ZooTeam, based in picturesque Hangzhou. The team now has more than 40 front end partners, the average age of 27 years old, nearly 30% are full stack engineers, no problem youth storm team. The membership consists of “old” soldiers from Alibaba and netease, as well as fresh graduates from Zhejiang University, University of Science and Technology of China, Hangzhou Electric And other universities. In addition to the daily business connection, the team also carried out technical exploration and actual practice in the direction of material system, engineering platform, building platform, performance experience, cloud application, data analysis and visualization, promoted and implemented a series of internal technical products, and continued to explore the new boundary of the front-end technology system.

If you want to change the things you’ve been doing, you want to start doing things. If you want to change, you’ve been told you need to think more, but you can’t change; If you want to change, you have the power to achieve that result, but you are not needed; If you want to change what you want to accomplish, you need a team to support you, but there is no place for you to bring people; If you want a change of pace, it’s “3 years of experience in 5 years”; If you want to change the original savvy is good, but there is always a layer of fuzzy window paper… If you believe in the power of belief, that ordinary people can achieve extraordinary things, that you can meet a better version of yourself. If you want to get involved in the growth of a front end team with a deep understanding of the business, a sound technology system, technology that creates value, and spillover impact as the business takes off, I think we should talk about it. Any time, waiting for you to write something, to [email protected]