preface

The state management research group, sponsored by the -Flutter Group, will discuss state management related topics for two months. The group will disband in two months and release the results of its discussions.

So far only 5 people have been selected to participate in the discussion, if you have a unique insight into state management, or would like to participate. You can publish an article on your cognition of status management, as the “ticket” to join the group, welcome to communicate with us.


Discuss the first topic in the first two weeks:

Your view and understanding of state managementCopy the code

State management, state management. As the name suggests, state + management, so the question is, what is state? Why manage?

First, what is the state

1. Thinking about the concept of states

It’s very difficult to say what something is. It’s not like mathematics can give you a specific definition, for example

Parallelogram: a closed graph triangle composed of two groups of parallel line segments in the same two-dimensional plane: a closed graph composed of three line segments in the same plane that are not on the same lineCopy the code

If we have a clearly defined concept, we can easily understand its characteristics and functions. But when it comes to more general terms like state, it’s a matter of opinion. I looked it up and there are explanations for states:

A state is the form in which a person or thing appears. It refers to the form or situation of real (or virtual) things when they are in the period of generation, survival, development, extinction or the critical point of transformation.Copy the code

In programming, the state is the expression of the interface at various times, and the change of the state will lead to the change of the interface after refreshing. So what’s the difference and connection between interfaces and states?

For example, a seed germination, growth, flowering, fruit, wither, this is the external representation, is the outside world to see the morphological change. But fundamentally, these changes are the result of internal data changes caused by the exchange of resources between seeds and the outside world. That is, one is face, one is lining.

Florists don’t care about the internal logic of the seed, they just need to meet the needs of the flower. That is to say the interface is the performance, is used to show the user; State is the essence and needs to be maintained by the programmer. If a developer can only see face, and ignore that we are the seed, how can we talk about state, how can we think about management? .


2. State, interaction and interface

For an application, the most fundamental purpose is: through the operation interface, users can carry out the correct logic processing, and get a certain response feedback.


From the user’s point of view, the inner workings of an app are a black box, and the user doesn’t need or need to know the details. But this black box internal logic processing needs to be implemented by the programmer, we can not escape.

Take the counter we are most familiar with, click the button, modify the status information, after rebuilding, to achieve the effect of changing the number on the interface.


Second, why management

Speaking of management, when do you think management is needed? Is complex, only complex management is necessary. What are the benefits of management?

For example, if Zhang SAN opens a restaurant and employs four people, they all do their own jobs. They all need to recruit customers, cook food, deliver goods, clean and other tasks at the same time. That efficiency will be very low. If a bug is found in a dish, it is not easy to locate the source of the problem. It’s a lot like everything is crammed into an XXXState that handles not only component build logic, but also a lot of business logic.

It is more efficient to delegate complex tasks to different people at different levels, each doing his or her job, than to four people doing his or her job. The purpose of management is to deal with tasks hierarchically and incrementally.


1. Scope of status

Let’s start with a question: Do all states need to be managed? For example, in the following FloatingActionButton, a ripple of water is created when clicked, and a change in the interface means a change in the state.

However, the FloatingActionButton component inherits from the StatelessWidget, meaning that it has no ability to change its state. So when I click, why does the state change? Because it uses the RawMaterialButton component in build, RawMaterialButton uses InkWell, and InkWell inherits InkResponse, InkResponse uses the _InkResponseStateWidget in the build, which maintains the state change logic for the water ripple in the gesture.

class FloatingActionButton extends StatelessWidget{

---->[FloatingActionButton#build]----
Widget result = RawMaterialButton(
  onPressed: onPressed,
  mouseCursor: mouseCursor,
  elevation: elevation,
  focusElevation: focusElevation,
  hoverElevation: hoverElevation,
  highlightElevation: highlightElevation,
  disabledElevation: disabledElevation,
  constraints: sizeConstraints,
  materialTapTargetSize: materialTapTargetSize,
  fillColor: backgroundColor,
  focusColor: focusColor,
  hoverColor: hoverColor,
  splashColor: splashColor,
  textStyle: extendedTextStyle,
  shape: shape,
  clipBehavior: clipBehavior,
  focusNode: focusNode,
  autofocus: autofocus,
  enableFeedback: enableFeedback,
  child: resolvedChild,
);
Copy the code

In other words: when clicked, the water ripple effect is encapsulated in the _InkResponseStateWidget component state. Private states like this don’t need to be managed because they can do their own work and the outside world doesn’t need to know about them. For example, the center and radius of water ripples are not cared about by the outside world. The State in a Flutter is itself a means of State management. Because:

1.State has the ability to build components based on State information2.State has the ability to rebuild componentsCopy the code

As is the case with all StatefulWidgets, the change logic and the amount of state are encapsulated in the corresponding XXXState class. It is local, private, the outside world does not need to know about changes in internal state information, and there is no direct access. This is generally used to encapsulate components, which encapsulate complex and relatively independent state changes to simplify user use.


2. Status sharing and modification synchronization

The State management State mentioned above is very small and convenient. There are drawbacks, however, because the state quantity is maintained inside XXXState, making it difficult for outsiders to access or modify. For example, the following page1, C is digital information, jump to page2, also want to display this value, and press the R button can make page1, page2, the number reset to 0. So there’s state there’s sharing and modification and synchronous updating, how do you do that?


Let’s start by writing the following setup interface:

class SettingPage extends StatelessWidget {
  const SettingPage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Setup screen'),),
      body: Container(
        height: 54,
        color: Colors.white,
        child: Row(
          children: [
            const SizedBox(width: 10,),
            Text('Current count is :'),
            Spacer(),
            ElevatedButton(child: Text('reset'),onPressed: (){} ),
            const SizedBox(width: 10() [(), ((), ((). }}Copy the code

So how do I know the current value, and how will the reset operation affect the numeric state of page1 when clicked? In fact, constructor parameters and callback functions can solve all data sharing and modification synchronization problems.


3. Code implementation – setState version:Source location

Because page2’s count is also cleared when reset is clicked, it means that its state quantity needs to change, and the state is maintained using the StatefulWidget. At construction time, initialCounter is passed through the constructor so that the number of Page2 is consistent with page1. Through the onReset callback function to listen to reset button trigger, in order to reset the number of page1 state, so that the number of PAGe1 can be consistent with page2. This is to keep the same amount of state in both interfaces. The diagram below:

class SettingPage extends StatefulWidget {
  final int initialCounter;
  final VoidCallback onReset;

  const SettingPage({
    Key? key,
    required this.initialCounter,
    required this.onReset,
  }) : super(key: key);

  @override
  State<SettingPage> createState() => _SettingPageState();
}
Copy the code
Jump to the Settings page Settings page reset
class _SettingPageState extends State<SettingPage> {
  int _counter = 0;

  @override
  void initState() {
    super.initState();
    _counter = widget.initialCounter;
  }
  
  // Same as above, omitted...
  
  void _onReset() {
    widget.onReset();
    setState(() {
      _counter = 0;
    });
  }
Copy the code

Maintain the _counter state quantity in _SettingPageState and execute the _onReset method when reset is clicked, triggering the onReset callback. Listen on onReset in interface 1 to reset the numeric state of interface 1. This ensures the synchronization of the digital state information between the two interfaces by constructing input parameters and callback functions.

-- -- -- - > [interface1----- navigator. push(Context, MaterialPageRoute(Builder: (Context) => SettingPage(initialCounter: _counter, onReset: (){ setState(() { _counter=0; }); })));Copy the code

But in this way, the determination is also very obvious, the data passed, transferred to and from, very troublesome, chaos is prone to error. If there are a few more pieces of information that need to be shared, or if the state needs to be shared in other interfaces, the code will get even messier.


4. Code implementation – ValueListenableBuilder version:Source location

The setState version above, which implements data sharing and modification synchronization, has some other drawbacks besides code clutter. First of all, in SettingPage we maintain a state message. The two interfaces have the same information, but they are identical. If the state information is a large object, this can be an unnecessary waste of memory.


Secondly, there is the much-criticized scope of setState refactoring. When State#setState executes, the build method is triggered to rebuild the component. For example, in page1, _MyHomePageState#build builds that Scaffold. When setState is triggered, all the underlying components are built again. Why is Scaffold not taken outside? The reason is that the FloatingActionButton component needs to modify the state value _counter and perform a rebuild, so it has to expand the build to include FloatingActionButton.


There is a component in Flutter that can solve both of these problems: ValueListenableBuilder. To use it, we create a ValueNotifier listener, _counter.

class _MyHomePageState extends State<MyHomePage> {

  final ValueNotifier<int> _counter = ValueNotifier(0);

  @override
  void dispose() {
    super.dispose();
    _counter.dispose();
  }
  
  void _incrementCounter() {
    _counter.value++;
  }
Copy the code

The following uses the ValueListenableBuilder component to listen on the _counter object. When the value of the listener changes, the listener can be notified and the component in the Builder method can be rebuilt. The biggest benefit of this is that you don’t need to build the whole of the interior with _MyHomePageState#setState, just rebuild the parts that need to be changed.

ValueListenableBuilder(
  valueListenable: _counter,
  builder: (ctx, int value, __) => Text(
    '$value',
    style: Theme.of(context).textTheme.headline4,
  ),
),
Copy the code

You can pass a visible listener to _counter into Page2 and also listen for counter through ValueListenableBuilder. This is equivalent to observer mode, where two subscribers listen to a publisher at the same time. Value =0. ValueListenableBuilder in both places triggers a local rebuild.

This achieves the same effect as the setState version, with simplified input and callback notifications through ValueListenableBuilder and the ability to partially refactor components. ValueListenableBuilder beats State in State sharing and modification synchronization. Then again, that’s not what State is supposed to do, it’s more focused on handling private states. ValueListenableBuilder, for example, is essentially a private State encapsulation implemented by State, so there is no good or bad, only fit or not fit.


Use status management tools

1. The necessity of status management tools

In fact, the previous ValueListenableBuilder effect and good, but in some cases there are still insufficient. Because _counter needs to be passed through constructors, it can also complicate code handling if there is too much state, or if there are too many shared occasions or too many levels of passing. Most damningly: the business logic processing and interface components are coupled in _MyHomePageState, which is not a good thing for expansion and maintenance. So management is necessary for state sharing and modification synchronization under complex logic.


2. State management through Flutter_bloc:Source location

As mentioned earlier, the purpose of state management is to make state shareable and to update the display of related components synchronously when updating the state, and to separate state change logic from interface building. Flutter_bloc is one of the tools to achieve State management. Its core is: Converting the Event operation into State through Bloc; At the same time, local component builds are performed by listening for state changes through BlocBuilder.

In this way, the programmer can concentrate the state-change logic in Bloc. When an Event is triggered, the Bloc driver State is changed by sending the Event instruction. For this small case, there are two main events: autoadd and reset. Enumerations can be used to distinguish events that require no parameters, such as defining events:

enum CountEvent {
  add, / / since
  reset, / / reset
}
Copy the code

State is the information on which the interface is built. I’m going to define CountState and hold value.

class CountState {
  final int value;
  const CountState({this.value = 0});
}
Copy the code

Finally, Bloc, a new version of Flutter_BLOC listens for events via ON and produces new states via EMIT. The CountEvent event is handled by the _onCountEvent method and CountState is changed. When event == countevent.add, a new CountState object with the original state +1 is produced.

class CountBloc extends Bloc<CountEvent.CountState> {
  CountBloc() : super(const CountState()){
    on<CountEvent>(_onCountEvent);
  }

  void _onCountEvent(CountEvent event, Emitter<CountState> emit) {
    if (event == CountEvent.add) {
      emit(CountState(value: state.value + 1));
    }

    if (event == CountEvent.reset) {
      emit (const CountState(value: 0)); }}}Copy the code

Here’s a simple diagram: When you click _incrementCounter, all you need to do is trigger the countevent.add directive. The core’s state processing logic takes place in the CountBloc, generating new states and triggering local updates through the BlocBuilder component. In this way, the logic of state change and the logic of interface construction are well separated.

// Send autoadd event specified
void _incrementCounter() {
  BlocProvider.of<CountBloc>(context).add(CountEvent.add);
}

// Use BlocBuilder to partially update the numeric Text:
BlocBuilder<CountBloc, CountState>(
  builder: _buildCounterByState,
),

Widget _buildCounterByState(BuildContext context, CountState state) {
  return Text(
    '${state.value}',
    style: Theme.of(context).textTheme.headline4,
  );
}
Copy the code

Similarly, the reset button in the Settings screen simply issues the countevent. reset command. The core state processing logic will proceed in the CountBloc, generating a new state and triggering local updates via the BlocBuilder component.


Since BlocProvider. Of

(context) gets the Bloc object, a superior context is required to exist in the BlocProvider, which can be provided at the top level. In this way, the Bloc can be captured and its state shared in any interface.

It’s a small case and probably doesn’t capture the essence of Bloc, but it’s a good entry level experience. You need to feel it for yourself:

[1]. State [share] and [Modify State] are updated synchronously. [2]. Separation of state change logic and interface build logic.Copy the code

Personally, these two points are the core of state management. Everyone may have their own ideas, but at least you can’t do what appears to be state management without knowing what you’re managing. To summarize my point of view: State is the information on which the interface is built; Management, by division of labor, makes this state information easier to maintain, easier to share, more synchronous, and more ‘efficient’. Flutter_bloc is just one of the tools for state management, and the others are not out of the core.


Iv. Interpretation of official case – Github_search

1. Case Introduction:Source location

In order to have a deeper understanding of the logical stratification of Flutter_bloc, an official case of Flutter_bloc is selected here for interpretation. Here is a brief look at the interface effect:

[1] Github project [2] displays different interfaces under different states, such as no input, searching, search success, and no data. [3] Debounce when typing. Avoid requesting the interface for every character entered.Copy the code

Note: debounce: The action is not executed until n milliseconds after it has been called, and the execution time will be recalculated if it is called again within n milliseconds.

Search state change Status display when no data is available

The project structure

├─ ├─ Exercises # ├─ Exercises # ├─ Exercises # ├─ exercises # ├─ exercises # ├─ exercises # ├─ exercises #Copy the code

2. The storage layerrepository

Let’s start with the repository layer, which separates the data retrieval logic from the relevant data entity classes under the Model package and the data retrieval operations under the API package.

Some people may ask why a repository layer is necessary when business logic can only be processed in Bloc. In fact, it is quite arbitrary to understand that Bloc core deals with state changes. If all the interface request codes are placed in Bloc, it will be very bloated. More importantly, the Repository layer is relatively independent, so you can test it separately to ensure that the data fetching logic is correct.

This brings another benefit when the data model is determined. The Repository layer and the interface layer can be developed simultaneously. Finally, the repository and interface can be integrated through Bloc layer. Layering is a means of management, just as different departments deal with different things. Once something goes wrong, it is easy to locate the fault. When one department expands and upgrades, it can also minimize the impact on other departments.

The repository layer is also generic, no matter Bloc or Provider, it is only a means of management. The repository layer is completely independent of the data acquisition method. For example, in the case of Todo, Bloc edition and Provider can share a repository layer, because the data acquisition method remains the same even if the framework is used in different ways.


For a quick look at the repository layer logic, GithubRepository relies on two objects and has only one search method. The GithubCache type cache object is used for the record cache. It is first checked from the cache during query. If it already exists, the cache data is returned. Otherwise, a client object of type GithubClient is used for the search.


GithubClient obtains network data mainly through HTTP.


GithubClient maintains a Map of search characters and search results. This process is relatively simple, and can be extended based on this: for example, set a limit on the number of caches, otherwise the cache will continue to be added as the search; Or add the cache to the database, support offline cache. By isolating the Repository layer, these extensions can be decoupled from the interface layer. Because the interface only cares about the data itself, not how the data is cached or retrieved.


3. The bloc layer

Looking at events first, the entire search function has only one event: TextChanged for text input, which is triggered with a string of information for the search.

abstract class GithubSearchEvent extends Equatable {
  const GithubSearchEvent();
}

class TextChanged extends GithubSearchEvent {
  const TextChanged({required this.text});

  final String text;

  @override
  List<Object> get props => [text];

  @override
  String toString() => 'TextChanged { text: $text} ';
}
Copy the code

As for states, there are four types of states throughout the process:

  • [1]. SearchStateEmpty: Indicates the status when the input character is empty and there is no maintenance data.
  • [2]. SearchStateLoading: Indicates the wait state between the request and the response. No maintenance data is available.
  • [3]. SearchStateSuccess: Indicates the request success status. MaintenanceSearchResultItemList of items.
  • [4]. SearchStateError: Indicates the failed state and maintains error information.


And finally Bloc, the logic used to integrate changes in state. The TextChanged event is listened on in the constructor, triggering _onTextChanged to produce state. For example, searchTerm.isempty indicates no character input and produces SearchStateEmpty. The output SearchStateLoading represents the wait state before githubrepository.search retrieves the data. A successful request produces the SearchStateSuccess state with result data, or a failed request produces the SearchStateError state.

class GithubSearchBloc extends Bloc<GithubSearchEvent.GithubSearchState> {
  GithubSearchBloc({required this.githubRepository})
      : super(SearchStateEmpty()) {
    on<TextChanged>(_onTextChanged);
  }

  final GithubRepository githubRepository;

  void _onTextChanged(
    TextChanged event,
    Emitter<GithubSearchState> emit,
  ) async {
    final searchTerm = event.text;

    if (searchTerm.isEmpty) return emit(SearchStateEmpty());

    emit(SearchStateLoading());

    try {
      final results = await githubRepository.search(searchTerm);
      emit(SearchStateSuccess(results.items));
    } catch (error) {
      emit(error is SearchResultError
          ? SearchStateError(error.message)
          : const SearchStateError('something went wrong')); }}}Copy the code

At this point, the entire business logic is complete, and the state changes from time to time are complete. All you need to do is listen for the state changes through BlocBuilder and build the component. Also to illustrate the role of debounce: without buffering, each character input triggers a request for data, which can result in very frequent requests and most of the input is useless. In this case, debounce can be used, for example, to enter 300 ms before the request is performed, and to re-time if there is new input in the meantime. This is essentially a convective transformation operation, handled in the stream_transform plug-in, with dependencies added in pubspec.yaml

stream_transform: ^2.0. 0
Copy the code

In the transformer parameter of on

you can specify the event stream converter to achieve the anti-shake effect:

const Duration _duration = Duration(milliseconds: 300);

EventTransformer<Event> debounce<Event>(Duration duration) {
  return (events, mapper) => events.debounce(duration).switchMap(mapper);
}

class GithubSearchBloc extends Bloc<GithubSearchEvent.GithubSearchState> {
  GithubSearchBloc({required this.githubRepository})
      : super(SearchStateEmpty()) {
    // Use debounce for conversion
    on<TextChanged>(_onTextChanged, transformer: debounce(_duration));
  }
Copy the code

4. The interface layer

The interface layer is very simple. You can use BlocBuilder to listen for state changes and build different interface elements based on different states.


The event is triggered when the text is typed. The input box is wrapped separately into the SearchBar component, which fires the TextChanged method of _githubSearchBloc in the onChanged method of TextField, thus driving the point and setting the entire state-changing “gear set” in motion.

---->[search_bar.dart]----
@override
void initState() {
  super.initState();
  _githubSearchBloc = context.read<GithubSearchBloc>();
}

return TextField(
   //....
   onChanged: (text) {
    _githubSearchBloc.add(TextChanged(text: text));
   },
Copy the code

Such a simple search requirement was completed, flutter_bloc also passed a lot of examples and documents, those who are interested can do more research on their own.


Five, the summary

Here is a summary of my understanding of state management:

[1[State] is the information on which the interface is built. [2[Management] is a layered treatment of complex scenarios that makes the state-change logic independent of the view-build logic.Copy the code

Back to the original question, do all states need to be managed? How do you distinguish which states need to be managed? Take front-end Redux state management, as described in You Might Not Need Redux: People often use Redux before they really Need it. For state management, in fact, is the same, often beginners “rush”, do not understand why state management, why a very simple function, must be a big circle to achieve. Even if I see no use, I will use it, this is not rational.

We should understand before using:

[1]. Whether the status needs to be shared and synchronized. If not, encapsulation as internal State via [State] may be a better option. [2[Business logic] and [interface state changes] are complex enough to warrant layering. If it's not too complicated, a small, local build component like FutureBuilder or ValueListenableBuilder might be a better choice.Copy the code

That’s all I want to share with you in this article, and finally: If you have a unique insight into state management, or want to get involved. You can publish an article on your cognition of status management, as the “ticket” to join the group, welcome to communicate with us.