Flutter Chat – Proof of Concept

Clients often can’t understand why it takes so much time to implement things that exist in almost every application? If it’s in every app, then it must be easy?

In Flutter, you are trying to accelerate development by using one of the oered packages in the pub.dev oicial package repository.

The first step is to choose a package that would be suitable for implementation. The choice of the package depends on the features that the package has.

For example, customization of UI, prebuilt components that manage most operations like data fetching, pagination, sending a message, image and file upload, and more. Finally, I will analyze the most popular chat UI package, flutter_chat_ui.

Flutter Chat UI – Flyer Chat

Flyer Chat is a platform for creating in-app chat experiences using Flutter. For installation add flutter_chat_ui to your package’s pubspec.yaml.

Usage

You start with a Chat widget that will render a chat. In my example Chat widget is rendered like this:

It has 3 required properties:

  • messages – an array of messages to be rendered
  • onSendPressed – a function that will have a partial text message as a parameter
  • user – a User class
StreamBuilder<List<types.Message>>(
    stream: FirebaseChatCore.instance.messages(chatRoom),
    initialData: const [],
    builder: (context, snapshot) {
      return SafeArea(
                bottom: true,
                child: Chat(
                  messages: snapshot.data ?? [],
                  onAttachmentPressed: _handleAtachmentPressed,
                  isAttachmentUploading: _isAttachmentUploading,
                  onMessageTap: _handleMessageTap,
                  onPreviewDataFetched: _handlePreviewDataFetched,
                  onSendPressed: _handleSendPressed,
                  bubbleBuilder: _bubbleBuilder,
                  user: types.User(
                    id: FirebaseChatCore.instance.firebaseUser?.uid ?? '',),
                  showUserAvatars: true,
                  showUserNames: true,
                  theme: const DefaultChatTheme(
                    inputBackgroundColor: Colors.black,
                    backgroundColor: kSecondaryDark,
                    inputTextColor: kSecondaryColor,
                    inputTextCursorColor: kSecondaryColor,
                    emptyChatPlaceholderTextStyle: TextStyle(color: kSecondaryColor),
                    receivedMessageBodyTextStyle: TextStyle(color: Colors.white, fontWeight: FontWeight.bold),
                    sentMessageBodyTextStyle: TextStyle(color: kSecondaryDark, fontWeight: FontWeight.bold),
                  ),
                ),
              );
      }
  )

Other properties

  • bubbleBuilder – Customize the default bubble using this function
Widget _bubbleBuilder(
    Widget child, {
      required message,
      required nextMessageInGroup,
    }) {
  return Bubble(
    child: child,
    color: FirebaseChatCore.instance.firebaseUser?.uid != message.author.id ||
        message.type == types.MessageType.image
        ? Colors.black
        : kSecondaryColor,
    margin: nextMessageInGroup
        ? const BubbleEdges.symmetric(horizontal: 6)
        : null,
    nip: nextMessageInGroup
        ? BubbleNip.no
        : FirebaseChatCore.instance.firebaseUser?.uid != message.author.id
        ? BubbleNip.leftBottom
        : BubbleNip.rightBottom,
  );
}
  • customBottomWidget – [Widget] Allows you to replace the default Input widget
  • customDateHeaderText – [String] If [dateFormat], [dateLocale] and/or [timeFormat] is not enough to customize date headers in your case, use this to return an arbitrary string based on a [DateTime] of a particular message
  • customMessageBuilder – Build a custom message inside the predefined bubble
  • dateFormat – This allows you to customize the date format
  • dateHeaderThreshold – Time (in ms) between two messages while rendering a date header
  • dateLocale – Locale will be passed to the `Intl` package
  • disableImageGallery – Disable automatic image preview on tap
  • emojiEnlargementBehavior – Controls the enlargement behavior of the emojis in the TextMessage
  • emptyState – [Widget] Allows you to change what the user sees when there are no messages
  • fileMessageBuilder – Build a file message inside predefined bubble
  • groupMessagesThreshold – [int] Time (in ms) between two messages while visually grouping them
  • hideBackgroundOnEmojiMessages – [bool] Hide background for messages containing only emojis
  • imageMessageBuilder – [Widget] Build an image message inside predefined bubble
  • isAttachmentUploading – [bool] Whether attachment is uploading. Will replace attachment button with a [CircularProgressIndicator]
  • isLastPage – [bool] Used for pagination (infinite scroll) together with [onEndReached]. When true, it indicates that there are no more pages to load, and pagination will not be triggered
  • l10n – [ChatL10n] Localized copy
  • onAttachmentPressed – [Function()] Callback for attachment button tap event
  • onBackgroundTap – [Function()] Called when user taps on background
  • onEndReached – [Function()] Used for pagination (infinite scroll). Called when user scrolls to the very end of the list
  • onEndReachedThreshold – [double] Used for pagination (infinite scroll) together with [onEndReached]. It can be anything from 0 to 1, where 0 is the immediate load of the next page as soon as the scroll starts, and 1 is the load of the next page only if scrolled to the very end of the list. The default value is 0.75, e.g., start loading the next page when scrolled through about 3/4 of the available content
  • onMessageLongPress – [Function()] Called when user makes a long press on any message
  • onMessageTap – [Function()] Called when user taps on any message
  • onSendPressed – [Function()] Will be called on [SendButton] tap
  • onTextChanged – [Function()] Will be called whenever the text inside [TextField] changes
  • onTextFieldTap – [Function()] Will be called on [TextField] tap
  • sendButtonVisibilityMode – Controls the visibility behavior of the [SendButton] based on the [TextField] state inside the [Input] widget.

Upload media

You can use the image picker package to select an image and send it as a message. For example:

void _handleImageSelection() async {
    final result = await ImagePicker().pickImage(
      imageQuality: 70,
      maxWidth: 1440,
      source: ImageSource.gallery,
    );
    if (result != null) {
      _setAttachmentUploading(true);
      final file = File(result.path);
      final size = file.lengthSync();
      final bytes = await result.readAsBytes();
      final image = await decodeImageFromList(bytes);
      final name = result.name;
      try {
        final reference = FirebaseStorage.instance.ref(name);
        await reference.putFile(file);
        final uri = await reference.getDownloadURL();
        final message = types.PartialImage(
          height: image.height.toDouble(),
          name: name,
          size: size,
          uri: uri,
          width: image.width.toDouble(),
        );
        FirebaseChatCore.instance.sendMessage(
          message,
          context.read(chatProvider).room.id,
        );
        _setAttachmentUploading(false);
      } finally {
        _setAttachmentUploading(false);
      }
    }

Similar to the text message, you will need to create an image message using data from the image picker. In this example, you can use a local path just for demo purposes, but you will upload the image first for the backend service and then send the received URL using the uri property.

The image message renders in two dierent ways to keep the UI clean. First, if the aspect ratio is too low or too high, it renders like a file message, so you don’t see a narrow line on the UI. The second way is a classic image in the chat. Give it a try. On tap, photos will be previewed inside an interactive image gallery. To disable the image gallery, pass disableImageGallery property to the Chat widget.

Files

You can use the file_picker package to select a file and send it as a message.

Example:

void _handleFileSelection() async {
    final result = await FilePicker.platform.pickFiles(
      type: FileType.any,
    );
    if (result != null && result.files.single.path != null) {
      _setAttachmentUploading(true);
      final name = result.files.single.name;
      final filePath = result.files.single.path!;
      final file = File(filePath);
      try {
        final reference = FirebaseStorage.instance.ref(name);
        await reference.putFile(file);
        final uri = await reference.getDownloadURL();
        final message = types.PartialFile(
          mimeType: lookupMimeType(filePath),
          name: name,
          size: result.files.single.size,
          uri: uri,
        );
        FirebaseChatCore.instance.sendMessage(message, context.read(chatProvider).room.id);
        _setAttachmentUploading(false);
      } finally {
        _setAttachmentUploading(false);
      }
    }
  }

Like the text message, you will need to create a file message using data from the document picker. In this example, uri will point to the local file system just for demo purposes, but you will upload the file first for the backend service and then send the received URL using the uri property.

Opening file

Right now, nothing will happen when a user taps on a file message, so I will need to add another dependency. I will use an open file package. Now, you can open a file:

void _handleMessageTap(types.Message message) async {
    if (message is types.FileMessage) {
      var localPath = message.uri;
      if (message.uri.startsWith('http')) {
        final client = http.Client();
        final request = await client.get(Uri.parse(message.uri));
        final bytes = request.bodyBytes;
        final documentsDir = (await getApplicationDocumentsDirectory()).path;
        localPath = '$documentsDir/${message.name}';
        if (!File(localPath).existsSync()) {
          final file = File(localPath);
          await file.writeAsBytes(bytes);
        }
      }
      await OpenFile.open(localPath);
    }
  }

Link preview

Link preview works automatically, and it can be disabled by setting usePreviewData to false. Usually, however, you’ll want to save the preview data so it stays the same. You can do that using onPreviewDataFetched.

Q Chat App (Flyer Chat) with Firebase as backend

Using the chat UI, I made a simple chat application using the Flutter chat UI package. The application uses firebase as the backend. By registering the user, I can chat with other ones. I have a contacts dialog with a list of all contacts, and to start chatting, all I have to do is on the user.

In addition to messages, you can send pictures and files. I just have to pick what I will be sending, a photo or file.

Flutter chat with custom backend

All solutions for chats can be split into two categories:

1. Ready-made chat platforms (SendBirdGetStream.io)

They have prepared functionality for main chat options. The developer needs to put a small eort and time into adding a chat into an app. But the behavior of functionality has strict borders by the platform creator, and possibilities for customization are limited.

2. Technologies for exchanging data between client and server, such as Firebase Cloud Messaging, Socket.io.

They allow building a very flexible chat solution upon them. Those technologies take time to implement but oer unlimited possibilities for customization.

Firebase

For implementing Firebase only for the chat functionality and using your custom backend, you can register/login users using a custom JWT token and save the received uid to the users’ table. Then you can have a screen with all users from your user’s table where each of them will have an assigned uid that will be used to start a chat.

When you have access to that uid, create a room (channel) or group room and start chatting.

Advantages

Free for products with up to 100 simultaneous users. With comprehensive documentation and high popularity, developers can quickly find answers, which saves time and money. No need to buy an additional server. You can do everything on the Firebase server.

Disadvantages

The developer needs experience working with NoSQL to build a convenient and eective database structure.

Complex search inquiries still pose a bigger problem than those in SQL databases or NoSQL databases like MongoDB. Some are impossible, and others will take much more time to develop.

Socket.IO

Advantages

Server platform Node.js, going with the real-time data exchange library Socket.io helps build a chat with unlimited customization possibilities.

The developer has complete control over all components: interface, logic, and server data. An endpoint can be whatever is needed. In the future product, you will see many complex search inquiries, and you can choose the SQL database instead of using Firestore.

Client and server exchange messages directly, resulting in fast work. When you use Firebase Cloud Messaging, the message goes from your server to the Firebase server, and only from there will it travel further to the device.

It’s important to mention that if there is a 0,5s delay, it’s not critical for a text chat. But, again, open-source, free solution – you don’t have to pay monthly for this technology.

Disadvantages

The only disadvantage is that it requires more time and money, as the developer creates all functions from scratch.

Links: socket.io packages

Sendbird

Advantages

Sendbird is a ready-made chat platform. It oers many prepared features, including an indication of the number of unread messages, blocking users, and admin dashboard.

Many features are already realized and work right away: message or file exchange, invitations, users blocking, typing indication, editing messages, automatic translation, admin dashboard, group chats for up to 100 users.

Disadvantages

Chat and functionality behaviors are strictly set. A user logs in and gets access to a list of channels. They can choose an existing channel or create their own. Tracks are either public or private. Users can exchange messages in either one.

This is the standard structure for the majority of chats. If you need something more exotic for your product, it will be problematic to realize that with SendBird. Not as popular as Firebase and Socket.io. The price depends on the number of users. The base package costs $400 monthly for 5k active users. With 100k active monthly users, the price will increase to $5000..

Links: sendbird packages

GetStream.io

The Flutter SDK lets you build any chat or messaging experience for Android, iOS, Web, and Desktop.

It consists of 4 packages:

  • stream_chat: A pure Dart package that can be used on any Dart project. It provides a low-level client to access the Stream Chat service.
  • stream_chat_flutter_core: Provides business logic to fetch everyday things required for integrating Stream Chat into your application. The core package allows more customization and provides business logic but no UI components.
  • stream_chat_flutter: This library includes a low-level chat SDK and a set of reusable and customizable UI components.
  • stream_chat_persistence: Provides a persistence client for fetching and saving chat data locally.

Advantages

Integrated reply in their SDK:

  •  Giphy, emoticons and file attachments
  • Threads links
  • Search in messages and conversations
  • Video playback
  • Online statuses

Disadvantages

Although it has almost all the functionality for chat, same as Sendbird, there may be a problem if you need something specific. Also, high price (499 USD/month).

Links: Getstream packages

Conclusion

Before I conclude, I need to consider which solution is the best. For example, does my chat require minimal functionality, or do I need something more advanced?

Ready-made solutions, which I analyzed, oer a wide range of functionality, and with them, you can do almost anything a chat should have. But the most significant disadvantage is that these platforms are not free. Also, if you have some specific functionality that the platform does not oer, you still have to opt for some custom solutions like Socket.io.

With Socket.io you have complete freedom to achieve full customization, but you will have to develop from zero, and it takes time.

The conclusion is that if your chat requires a minimum base functionality and you don’t have a high level of requirements to it, Firebase Cloud Messaging-based solution is the way to go. On the other hand, if you need a quick implementation and money is not an issue, Sendbird or GetStream.io is a perfect solution.

But if there are a lot of requirements for a chat or you need lots of customized options, Socket.io is your friend.

See Links: hackernoon ; flyer chat


Leave a Reply

Your email address will not be published. Required fields are marked *