In any typical application, including the Flutter application, data is always transferred within a class or between classes. Data produced by a class or function is consumed, either internally or by another class or function. Data is most likely passed from one part to another through constructors.

Sometimes, you may have to pass data across several layers of parts before it finally reaches its target part. In this case, the middle layers of widgets do not need the data, but rather serve as a tool to pass the data to the widgets that need it.

This is a very inefficient in-application data management technique, especially on a large scale. It leads to a lot of template code and can cause your application’s performance to degrade.

This article will explore the application of Flutter state management and some of its techniques. We will also delve into how Redux can be used to efficiently manage data in our Flutter application.

What is a Redux?

Redux is a state management architecture library that successfully distributes data across widgets in a repetitive manner. It manages the state of the application through a one-way flow of data. Let’s explore the figure below.

In this example, the data generated in the main widget is required in child widget 8. Typically, this data passes through child part 2 to child part 6, and then, finally, to child part 8. The same is true for parts that need to generate or save data in the state of any part higher in the hierarchy.

With Redux, you can structure your application so that state is extracted in a centrally located store. This centrally stored data can be accessed by any widget that needs it, without going through any other widget chain in the tree.

Any widget that needs to add, modify, or retrieve data in the state managed by the Redux store must request it with the appropriate parameters.

Similarly, for each change in state, the dependent widget responds to the change through the user interface or any other configuration.

Why is state management important?

In a medium or large application with many widgets, it is common to manage data from the main.dart file when a child widget needs it.

This data can be distributed as parameters through the widget’s constructor until the data arrives at the receiving widget, but as we discussed in the introduction, this can lead to a long chain of data transfers through widgets that don’t need it.

Passing data through constructors can not only be cumbersome and difficult, but can also affect application performance. This is because when you manage data from the master widget — or any root widget — the entire widget tree is rebuilt every time any of its children change. You only want to run build methods in widgets that need to change data.

Redux is a single source of truth

In your application, there should be only one information store. Not only does this help with debugging, but every time data changes in your application, you’ll be able to more easily detect where and why it changed.

invariance

The state of your application should be immutable and accessible only by reading it. Part of what this means is that if you want to change a value in a state, you have to completely replace that state with a new state that contains your new value.

This helps ensure the security of the storage and allows state changes only through operations. It also makes the application transparent internally, because you can always detect the reasons for state changes and the objects responsible for those changes.

Functions should be state changers

Changes to state should occur only by functions. These functions, called reducers, are the only entities that are allowed to make changes to your application’s state.

@immutable
class AppState{
  final value;
  AppState(this.value);
}

enum Actions {Add, Subtract};

AppState reducer(AppState previousState, action){
  if(action == Actions.Add){
    return new AppState(previousState.value +  1);
  }
  if(action == Actions.Subtract){
    return new AppState(previousState.value -  1);
  }
  return previousState;
}
Copy the code

In the above code, AppState is an immutable class that holds the state of the value variable.

The operations allowed in the state are Add and Subtract.

Changes to the state are made using the Reducer function, which receives the state and the actions that are taken on the state.

The architecture of Flutter Redux

The store

This is the central place where application state exists. Storage holds information about the entire application state or any other single state at any given time.

final Store<AppState> store = Store<AppState>(
reducer, initialState: AppState.initialState()
);
Copy the code

Reducer is a function that updates the store with new state. It takes status and action as parameters and updates the status based on the action.

To recap, states are immutable and are updated by creating a new state. The restorer is the only way to update the state.

Flutter uses inherited widgets to manipulate storage. Some of these inherited parts are.

  • StoreProvider: This widget injects the store into the application’s widget tree.
class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { ... return StoreProvider<AppState>( store: store, child: MaterialApp( title: 'Flutter Demo', theme: ThemeData.dark(), home: StoreBuilder<AppState>( ... ) ,),); }}Copy the code
  • StoreBuilder: this listens to the entire store and rebuilds the entire widget tree with each update; It is fromStoreProviderAnd receive storage.StoreConnector
@override
 Widget build(BuildContext context) {

  ...

   return StoreProvider<AppState>(
     store: store,
     child: MaterialApp(
       title: 'Flutter Demo',
       theme: ThemeData.dark(),
       home: StoreBuilder<AppState>(
           onInit: (store) => store.dispatch(Action()),
         builder: (BuildContext context, Store<AppState> store) => MyHomePage(store),
       ),

     ),
   );
 }
Copy the code
  • StoreConnector: This is a substitute forStoreBuilderThe widget. It receives information fromStoreProviderTo read data from our store and send it to itbuilderFunction. And then, whenever the data changes,builderThe function recreates the widget.
class MyHomePage extends StatelessWidget{ final Store<AppState> store; MyHomePage(this.store); @override Widget build(BuildContext context){ return Scaffold( appBar: AppBar( title: Text('Redux Items'), ), body: StoreConnector<AppState, Model>( builder: (BuildContext context, Model model) { ... }}}Copy the code

What is a Redux action?

Typically, when a state is stored, there are widgets and sub-widgets around the application that monitor the state and its current value. An action is an object that determines what event to perform on that state.

After events are performed on this state, the widgets that track the data in the state are rebuilt and the data they present is updated to the current value in the state. Actions include any events passed to the store to update the status of the application.

When any widget wants to use an action to change the state, it uses the store’s Dispatch method to communicate the action to the state — the store calls the action.

final Store<AppState> store = Store<AppState>(
reducer, initialState: AppState.initialState()
);


store.dispatch(Action());
Copy the code

How do state changes affect the user interface?

Updates to the state are reflected in the user interface — with each state update, it triggers the logic to rebuild the user interface in the StoreConnector Widget, which rebuilds the widget tree with each state change.

Suppose you have a pop-up window on the screen that requires the user to react in the form of clicking or tapping a button. We think of this pop-up window as the “view” in the figure above.

The effect of clicking a button is action. This action is wrapped and sent to the Reducer, which processes it and updates the data in the Store. The storage space then holds the state of the application, which detects such changes in data values.

Because the data presented on the screen is state-managed, this change in the data is reflected in the view, over and over again.

Redux middleware

The middleware is a Redux component that processes an action before the Reducer function receives it. It receives the application’s state and scheduled actions, and then uses the actions to perform the custom behavior.

Let’s say you want to perform an asynchronous operation, such as loading data from an external API. The middleware intercepts the action, then performs the asynchronous task and logs any side effects that may occur or any other custom behavior that may be displayed.

This is what a Redux process with middleware looks like.

Put it all together

Let’s take everything we’ve learned so far and build a basic application to implement Redux in Flutter.

Our demo application will include an interface with buttons that retrieve the current time of a location with each click. The application sends a request to the World Time API to get the time and location data required to enable the feature.

The dependence of Flutter Redux

Run the command on your terminal.

flutter create time_app
Copy the code

Add the following dependencies to your pubspec.yaml file and run flutter pub get.

  • ReduxContains and provides the basic tools needed to use Redux in the Flutter application, including.
    • The store that will be used to define the initial state of the store
    • reducerfunction
    • The middleware
  • flutter_redux: complements the Redux package, which installs a set of additional utility gadgets, including
    • StoreProvider
    • StoreBuilder
    • StoreConnector
  • Flutter_redux_dev_tools. This package functions similar to the Flutter Redux package, but contains more tools that you can use to track related state and motion changes
  • Redux_thunk: Used for middleware injection
  • HTTP: Enables external API calls through middleware
  • International: Enables the formatting of time data received from the API.

Next, add the following code for AppState.

class AppState {
  final String _location;
  final String _time;

  String get location => _location;
  String get time => _time;

  AppState(this._location, this._time);

  AppState.initialState() : _location = "", _time = "00:00";

}
Copy the code

The application state here has fields to show the location and time. These fields are initially set to null values. We also provide a getter method for each field to retrieve their respective values.

Next, we’ll write our action class, FetchTimeAction.

class FetchTimeAction {
  final String _location;
  final String _time;

  String get location => _location;
  String get time => _time;

  FetchTimeAction(this._location, this._time);
}
Copy the code

The Action class also has the same fields as AppState. When this action is called, we update the status with the value in the field.

We will now write the AppState Reducer function.

AppState reducer(AppState prev, dynamic action) { if (action is FetchTimeAction) { return AppState(action.location, action.time); } else { return prev; }}Copy the code

The Reducer function receives the state and actions. If the action is a FetchTimeAction, it returns a new state using the value in the action field. Otherwise, it will return to its previous state.

The code for the middleware is as follows.

ThunkAction<AppState> fetchTime = (Store<AppState> store) async {

  List<dynamic> locations;

  try {
    Response response = await get(
        Uri.parse('http://worldtimeapi.org/api/timezone/'));
    locations = jsonDecode(response.body);
  }catch(e){
    print('caught error: $e');
    return;
  }

  String time;
  String location = locations[Random().nextInt(locations.length)] as String;
  try {
    Response response = await get(
        Uri.parse('http://worldtimeapi.org/api/timezone/$location'));
    Map data = jsonDecode(response.body);

    String dateTime = data['datetime'];
    String offset = data['utc_offset'].substring(1, 3);

    DateTime date = DateTime.parse(dateTime);
    date = date.add(Duration(hours: int.parse(offset)));
    time = DateFormat.jm().format(date);
  }catch(e){
    print('caught error: $e');
    time = "could not fetch time data";
    return;
  }

  List<String> val = location.split("/");
  location = "${val[1]}, ${val[0]}";

  store.dispatch(FetchTimeAction(location, time));

};
Copy the code

FetchTime is an asynchronous function that takes store as its only argument. In this function, we make an asynchronous request to the API to get a list of available locations.

We then use the DartRandom() function to select a random location in the list and make another asynchronous request to get the time of the selected location. We also formatted the date and location values received from the API to suit our application.

Finally, we send a FetchTimeAction to the store so that we can update the new status.

Now, let’s build the rest of our application.

void main() => runApp(MyApp()); class MyApp extends StatelessWidget { final store = Store<AppState>(reducer, initialState: AppState.initialState(), middleware: [thunkMiddleware]); // root widget @override Widget build(BuildContext context) { return StoreProvider<AppState>( store: store, child: MaterialApp( title: 'Flutter Redux Demo', theme: ThemeData( primarySwatch: Colors.blue, ), home: MyHomePage(), ), ); } } class MyHomePage extends StatefulWidget { @override _MyHomePageState createState() => _MyHomePageState(); } class _MyHomePageState extends State<MyHomePage> { @override Widget build(BuildContext context) { return Scaffold( AppBar: appBar (title: const Text("Flutter Redux demo"),), body: Center(child: Container(height: 400.0, child: Column( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: <Widget>[ // display time and location StoreConnector<AppState, AppState>( converter: (store) => store.state, builder: (_, state) { return Text( 'The time in ${state.location} is ${state.time}', textAlign: TextAlign.center, style: Const TextStyle (fontSize: 40.0, fontWeight: FontWeight.bold), ); }, ), // fetch time button StoreConnector<AppState, FetchTime>( converter: (store) => () => store.dispatch(fetchTime), builder: (_, fetchTimeCallback) { return SizedBox( width: 250, height: 50, child: RaisedButton( color: Colors.amber, textColor: Colors.brown, onPressed: fetchTimeCallback, child: const Text( "Click to fetch time", style: TextStyle( color: Colors.brown, fontWeight: FontWeight.w600, fontSize: 25),),);},),),),),); } } typedef FetchTime = void Function();Copy the code

We first assign an instance of Store

. We then wrap the MaterialApp around StoreProvider

. This is because it is a base widget that will pass a given Redux store to all descendants that request it.

The Text widget that renders location and time is one of the descendant widgets that depends on the store, so we wrap it around StoreConnector to enable communication between the store and the widget.

The RaisedButton widget is the second widget that relies on the store. We also wrap it in StoreConnector. Each click triggers the middleware to run its functionality, updating the state of the application.

This is what our final application will look like.

conclusion

In the Flutter application, or front-end application in general, managing your data and reflecting its user interface is key.

Data is a pretty broad term. It can refer to any value displayed on your application, and its meaning can range from determining whether a user is logged in to any form of interaction resulting from users of your application.

When building your next application, I hope this article provides a comprehensive guide on how to build Flutter Redux efficiently using it.

The postFlutter Redux: Complete tutorial with examplesappeared first onLogRocket Blog.