Building a Messaging App in Flutter — Part III: Redux

Monday, Oct 21, 2019| Tags: flutter

This is part of a series of articles describing the work we did for Timy Messenger, a messaging app for groups to communicate and organize themselves. Build with flutter.

In this article I will explain how and why we used Redux as our state management solution in Timy Messenger.

The app is open source and the code is available for you get here:

janoodleFTW/timy-messenger

Why you need state management

In most software applications, we can split what we consider User Interaction or Interface (UI) and Business Logic. This separation allows us to understand better what the app does and how it is doing it, creating a structure that allows the codebase to grow in a stable way.

This separation can be extended to separating data sources from business logic, and as well, separating the different app features in their own individual and replaceable parts. This allows us to test each part one by one.

This is what we call Architecture, the way we split and connect the different parts of a code-base.

The different components can go through different states depending on the user cases. For example, a list of users can go from empty to having some values. A message can go from draft to sending to being sent. A user can be logged in or logged out. This different logic states are handled by what we call state management, which is going to take care to update our app UI when the logic states change.

In Flutter, we call State many different concepts, which is why we have long discussions in the community about this topic. We call State to:

  • The individual state of a Widget, for example, the value displayed on a Text Widget.
  • The state of a screen or complex component, for example, a list of items or a message draft.
  • The state of the whole application, for example the logged in user information or the shopping cart content.

The individual Widget state is handled with a StatefulWidget in Flutter, and it is something that you are going to still be using even if you use Redux or any other state management.

The state of the screen, also called local state, can be managed with state management solutions like BLoC or ScopedModel. This project does not use it, and instead, uses Redux to handle all the state of the screens globally.

Finally, the application state or global state. In this case, we use Redux. And Redux is probably the only well known state management solution with takes care of the full application state.

You can perfectly build an application that does not manage a global state and instead only cares about the local component state, but growing into a larger product is going to be more complicated in the future.

Why Redux

We went with Redux for many different reasons:

  • It is a pattern we were both familiar with.
  • We liked the way Redux separated Actions, making clear what different features the app had.
  • A clear separation between the Middleware and the rest of the app allowed us to easily test business logic.
  • All app business logic can be accessed from a single point. This facilitates dependency injection in the app and allows easy composition of features, allowing the app to grow.
  • We think it is a pattern that allows you to build large scale applications. In our case, we ended with 180 dart files and almost 30.000 lines of code.
  • Having a single global state allows us to easily debug what is wrong based on the current application state.
  • We liked having ViewModels defining the UI state as part of Redux.
  • The did not mind the apparent boilerplate.

The Basics of Redux

A typical Redux solution is structured in four parts:

  • The application state or AppState. It stores the global state in a single place and it is located in what is called the Store.
  • A set of Actions. They define what you can do and which parameters you need to pass. For example, SendMessage. Actions trigger Reducers and Middleware functions.
  • A set of Reducers: These are functions that change the AppState.
  • A set of Middleware functions: These are functions that run “side effects” like updating a database or send data online.

Some solutions even consider the Reducer and Middleware the same thing, simplifying the implementation.

Example: SendMessage Action

Let’s look at one example in the Timy Messenger app: In the message_actions.dart file we define the SendMessage action:

class SendMessage {  
  String message;  
}

Code samples in this article are simplified, see the app source for the full example.

This is a simple class that contains a message String, that the user wants to send to the currently selected channel.

This Action is going to be processed by some Reducer or Middleware functions. In our case, is the message_middleware.dart the one that defines the Middleware function that takes care of it.

When we created the Messages Middleware, we told Redux that we want to run the _sendMessage function when we dispatch the SendMessage action.

List<Middleware<AppState>> createMessagesMiddleware(   
  MessageRepository messagesRepository,  
) {   
  return [   
    TypedMiddleware<AppState, SendMessage  
                            (_sendMessage(messagesRepository)),   
  ];  
}

_sendMessage is going to perform the action to send messages with the MessagesRepository:

void Function(   
  Store<AppState> store,   
  SendMessage action,   
  NextDispatcher next,  
) _sendMessage(   
  MessageRepository messageRepository,  
) {   
  return (store, action, next) async {   
    next(action);   
    final message = Message(.. create a message ..);   
    try {   
      await messageRepository.sendMessage(message);  
    } catch (e) {   
      // log error  
    }   
  };  
}

As you can see, this Middleware function is running the messageRepository.sendMessage method asynchronously.

But we didn’t change the AppState here, right? That’s because it will happen somewhere else.

Example: UpdateAllMessages Action

In the previous example you saw how we are sending messages using the SendMessage Action, and how it triggers a function in the Middleware to call to messageRepository.sendMessage.

To receive messages, we are constantly listening to a Stream of messages from the MessageRepository. And when we receive a new message, we dispatch the UpdateAllMessages action to update the AppState.

// from \_listenMessages

messageRepository.getMessagesStream(  
  groupId,  
  channelId,  
  userId,   
).listen((data) {   
  store.dispatch(UpdateAllMessages(data));   
});

UpdateAllMessages is another Action that takes a list of messages to be displayed on the screen:

class UpdateAllMessages {  
  List<Message> data;  
}

In this case, is the message_reducer.dart the one taking care of processing the Action to update the AppState.

First we declare the Reducer function that is going to process the UpdateAllMessages Actions:

final messageReducers = <AppState Function(AppState, dynamic)>[  
  TypedReducer<AppState, UpdateAllMessages>(_onMessageUpdated),  
]; 

Then the function _onMessageUpdate is going to be called each time:

AppState _onMessageUpdated(AppState state, UpdateAllMessages action) {   
  return state.rebuild((a) => a..messagesOnScreen = (action.data));  
}

This function is rebuilding the AppState with a new value for messagesOnScreen which is the list of messages displayed.

We have seen how the business logic of the app works using Redux. We can dispatch Actions that will run Middleware or Reducer functions.

Example: Updating Messages on Screen

Reducer functions modify the AppState, which will also update the UI automatically, let’s see how we accomplish that.

In our project we have the MessagesList Widget, in the messages_list.dart file.

In this Widget, we are using a StoreConnector to display a list of messages:

StoreConnector<AppState, MessagesListViewModel>(  
  builder: (context, vm) {   
    return ListView.builder(.. create list with vm ..);   
  },   
  converter: MessagesListViewModel.fromStore,   
  distinct: true,   
)
  • StoreConnector is a Widget that will create another Widget (the ListView) based on the AppState.
  • converter takes a function that creates a MessagesListViewModel out of the AppState.
  • MessagesListViewModel is a data class that contains the list of messages to be displayed.
  • distinct is an optimization, by setting it to true, we only refresh the ListView when the content of the MessagesListViewModel changes.

The MessagesListViewModel is located in messages_list_viewmodel.dart.

abstract class MessagesListViewModel implements Built<MessagesListViewModel, MessagesListViewModelBuilder> {  
   
  @nullable User get currentUser;  
  BuiltList<Message> get messages;  
  bool get userIsMember;  
  BuiltMap<String, User> get authors; 

  static MessagesListViewModel fromStore(Store<AppState> store) {  
    return MessagesListViewModel((m) => m   
      ..messages = store.state.messagesOnScreen.toBuilder()  
      ..currentUser = store.state.user?.toBuilder()  
      ..authors = MapBuilder(.. create authors ..)   
      ..userIsMember = getSelectedChannel( .. );  
   }  
}

And it contains more information than just the list of messages, for example, it also contains the current user or the list of authors.

The fromStore method takes the AppState and uses it to create a MessagesListViewModel. Because the MessagesListViewModel also implements the equals operator through the Built abstract class, the StoreConnector will only refresh the Widgets when the view model changes.

In summary, with Redux we were able to separate the actions of sending and receiving messages in different parts of the app, while making them accessible from anywhere.

We were able to split each logic section individually in small parts that are easy to understand, update and test.

The amount of boilerplate can scare developers away from Redux: having to declare different Reducers and Middleware and having to use immutable data classes to contain the AppState and view models is an extra effort. But it pays on the long run by creating a more scalable and stable product.

I hope you enjoyed the third part of this series and it helped you see a real example with Redux, and maybe see why Redux could be the right solution for you.

In the next article I want to continue looking at the Redux implementation and how we did the testing.

Thanks for reading!

Want to learn more Android and Flutter? Check my courses here.

INTERESTED IN WORKING TOGETHER?

Contact with me