How to use a stream which is listening firestore collection in multiple screens with minimal rebuild using Riverpod in flutter.
Table Of Contents::
Proposition
My idea of a good implementation
Behavior check
Conclusion
Appendix
Proposition
There was one thing I didn’t like about developing with flutter.When I see the source which I wrote and others wrote,I think so.The following things I think so.
- Multiple listings of the same collection (or document) in firestore.
- Rebuild more than necessary.
- And that these are not being cared for.
I will explain these in detail using a chat implementation.
1. Multiple listings of the same collection (or document) in firestore.
Imagine that you would implement a chat function.a user who use this chat function is notified receiving a new message in a screen.And the user who is notified receiving a new message moves to another screen to see a new message which the user receives. Messages are written to a chat collection in firestore.
The following is the screen we will use.The left is the message count confirmation screen, and the right is the message display screen.
How would you implement it? In particular, Where do you listen to the chat collection in firestore?In the case of the BLoC pattern, the screen class and the business logic class exist as a pair, and listening to the chat collection is done in the business logic, isn’t it? And You will listen to the chat collection on both the message confirmation screen and the message display screen.It looks like a tidy implementation, but in this implementation, when there is a change in the chat collection, the implementation that is listening to the chat collection is processed twice. It is wasteful and not beautiful.
2.Rebuild more than necessary.
It’s a very small feature, but aren’t you rebuilding the entire screen to update this display? Using the chat function as an example, the message count display on the message count confirmation screen should be a small feature. In a real application, there should be other main functions. Nevertheless, do you rebuild the entire screen every time you receive a message? This is an easy implementation, but may not cause any problems if the screen is not complex. It’s useless, and it’s not beautiful.
3.And that these are not being cared for.
The above can be seen even in experienced programmers. The reason they leave the problem alone is that the program is apparently working fine. Trying to avoid 1 and 2 above while implementing a screen with a complex UI and many functions will take extra time to think about. I understand that it is tempting to cut corners, but as a professional, I want to do it right.
My idea of a good implementation.
Now, let’s take the example of implementing the chat function to avoid 1 and 2 above.
To do so, I will first introduce a package called Riverpod. See the link for details.
Now let’s look at the implementation.
The folder structure looks like this.
ChatService class Implementation
Create a function in the ChatService class to listen to the Chat collection, retrieve it, and return a snapshot in a stream.
Since we are starting to listen in _init(), we need to have a single instance of ChatService class. If there are multiple instances of ChatService, a single change in the chat collection will cause the app to respond multiple times. Let’s prepare to prevent this from happening. First, use Riverpod’s StreamProvider to create a ChatService class StreamProvider, chatMessageStreamProvider. It will return the message as a stream.
Next, we will use Riverpod’s StreamProvider to create a chatCounterStreamProvider that will watch the chatMessageStreamProvider we created above. Here, we count the number of messages in the chatMessageStreamProvider stream and return the count result as a stream.
Since we watch the chatMessageStreamProvider with the chatCounterStreamProvider, we can tell the UI the message and the number of messages at the same time only once, without the need for the listen process to run twice when the chat collection is changed.
Message count display screen Implementation
Be careful not to rebuild the entire view when the number of messages changes, rebuild only the widget that displays the number of messages. In this case, I created a ChatCounter class just to display the number of messages. contexts.read in Riverpod is used to get the stream of the chatCounterStreamProvider. Then we will use StreamBuilder to display the number of messages.
The ChatCountView class that calls the ChatCounter class looks like this. I use a single class to represent the screen, but I always implement Riverpod’s ProviderListener and Consumer(builder: (context, watch, child). ProviderListener displays indicators, screen transitions, and snack bars depending on the status changed as a result of business logic. Consumer(builder: (context, watch, child) is used to make relatively large changes to the screen UI, depending on the value that the status has changed as a result of business logic. For example, when switching the UI between phone number input and SMS code input on a screen for phone number authentication. This is when it is difficult to share information between screens if the screen transitions are used. It is important to note that using Consumer(builder: (context, watch, child) in this location will cause the entire rebuild to occur.
The chatCountNotifierProvider is a StateNotifierProvider that handles the business logic specific to ChatCountView class. This is not an important part of this article, so I will just post the source. Get and process the information we want to display in the ChatCountView class.
Message display screen Implementation
Same as ChatCountView class, avoid rebuilding the whole screen every time a message is received. We created a ChatMessenger class to display the message, using Riverpod’s context.read to get the chatMessageStreamProvider’s stream, and StreamBuilder to display the message. By the way, we used flutter_chat_ui for the chat ui.
The ChatMessageView class that calls the ChatMessenger class looks like this.
The chatMessageNotifierProvider is a StateNotifierProvider that handles the business logic specific to ChatMessageView class. This is not an important part of this article, so I will just post the source. Get and process the information we want to display in the ChatMessageView class.
final _messages = await chatService.getChatModels();
This part of the process retrieves the message from the firestore in order to display the message immediately after the screen is created. The chatMessageStreamProvider returns a stream only when there is a change in the message, so we can’t get the message on the first display. firestore’s listen can get the value right after the start of the listen, but in the implementation shown, the The firestore listen can get the value right after the start of the listen, but in the implementation shown, the chatCounterStreamProvider gets the value right after the start of the listen, so the chatMessageStreamProvider can’t get it.
Behavior check
Let’s see in the video that the listen is only executed once, change the chat collection in firestore. We can see that it passes through the ChatService class it is listening to once, then through the StreamBuilder of the ChatCounter class and the StreamBuilder of the ChatMessenger class. We can see that two StreamProviders are responding to a single listen.
In the video, we can see that the rebuild of the entire screen is not executed, even though the debugging is done in the build part of the ChatMessageView class and the ChatCountView class. The program does not stop there. This is the ideal behavior.
Conclusion
In the example of the chat function implementation, I showed how to optimize listening to the firestore collection and how to avoid unnecessary rebuild. The implementation to optimize listening to the firestore collection is to use the ChatService class to listen to the firestore collection, and to keep an instance of the ChatService class in the chatMessageStreamProvider, which is a StreamProvider. The StreamProvider for counting messages, chatCounterStreamProvider, does not create an instance of the ChatService class, but watches chatMessageStreamProvider to get the number of messages. We can get the number of messages. The important thing to notice is that we were able to implement a StreamProvider that just watches the StreamProvider.
As for the implementation without unnecessary rebuild, it is important to be able to use Riverpod’s context.read to implement StreamBuilder in the minimum necessary parts: ChatCounter class and ChatMessenger class. Never do this at the top-level position of a screen class.
I used the chat function as an example. This implementation concept can be reused for other functions as well. For example, it can be used for full-screen notifications of user role changes, notification notifications, etc.
This is Full source code. https://github.com/ishikurak73/chat_count
Appendix
I am satisfied that I was able to present a sample implementation across multiple classes and screens. I am not sure if I have conveyed what I wanted to say properly. I have not been able to show you all the sources I prepared in the explanation. Since some of you may want to know, all classes and a brief description are listed below.
ChatCountView class
Build a UI to display the number of messages.
ChatCounter class
Widget to display the number of messages. Update the number of messages in StreamBuilder.
ChatCountState class
Define the state that manages the message count display screen.
ChatCountNotifier class
Business logic of the message count display screen.
ChatMessageView class
Build a UI to display messages.
ChatMessenger class
Widget to display messages. Update messages in StreamBuilder.
ChatMessageState class
Define the state that manages the message display screen.
ChatMessageNotifier class
Business logic of the message display screen.
ChatModel class
Model definition for chat collection in firestore.
ChatService class
Interface with firestore’s chat collection. Listen here.
The package we used and its version