Building a Messaging App in Flutter — Part III: Redux

Posted on Oct 21, 2019

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 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:

The Basics of Redux

A typical Redux solution is structured in four parts:

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,   
)

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!

Share Tweet