Live Chat With Pusher Using Provider
Learn how to setup a real-time messaging Flutter App using Pusher API and a Go backend deployed as a containerised web service to GCP Cloud Run. By Wilberforce Uwadiegwu.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Live Chat With Pusher Using Provider
30 mins
- Getting Started
- Setting up and Deploying the Back end
- Setting up Pusher
- Setting up Firebase
- Setting up GCP
- Deploying the Go Service
- Sending and Receiving Messages
- Configuring Pusher
- Receiving Messages
- Sending Messages
- Implementing UI
- Building the Messages Screen
- Adding An Input Field
- Adding the Messages View
- Building the Message Widget
- Supporting Images
- Displaying Images
- Where to Go From Here
Adding the Messages View
Like you did for _InputWidget
in messages_screen.dark
, create another stateless widget that accepts a MessagesViewModel
like this:
class _BodyWidget extends StatelessWidget {
final MessagesViewModel vm;
final double bottom;
const _BodyWidget({required this.vm, required this.bottom, Key? key})
: super(key: key);
@override
Widget build(BuildContext context) {
// 1
if (vm.loading) {
return const Center(
child: CircularProgressIndicator.adaptive(),
);
}
final messages = vm.messages;
// 2
if (messages.isEmpty) {
return const Center(child: Text('You have not sent any messages yet'));
}
// 3
return ListView.builder(
itemCount: messages.length,
controller: vm.scrollController,
padding: EdgeInsets.only(bottom: bottom),
itemBuilder: (_, i) {
return Text(
messages[i].data.text ?? '',
key: ValueKey(messages[i].data.clientId),
);
});
}
}
- Display a progress indicator if the message history is loading.
- Display an error text if there are no messages to display.
- Display a ListView of the messages. In the interim, each message will be a Text.
Lastly, import 'package:provider/provider.dart'
, '../common/get_it.dart'
and '../common/common_scaffold.dart'
. Then replace the build function in MessagesScreen widget with:
Widget build(BuildContext context) {
final bottom = MediaQuery.of(context).viewInsets.bottom;
return ChangeNotifierProvider<MessagesViewModel>(
create: (_) => MessagesViewModel(channel, getIt()),
child: Consumer<MessagesViewModel>(
builder: (ctx, vm, _) {
return CommonScaffold(
title: title,
body: GestureDetector(
onTap: vm.focusNode.unfocus,
child: _BodyWidget(vm: vm, bottom: bottom),
),
bottomNavigationBar: _InputWidget(vm: vm, bottom: bottom),
);
},
),
);
}
This will render _BodyWidget in the body of the scaffold and _InputWidget as the bottom navigation bar. Notice the method supplied to onTap
of the GestureDetector; when the user taps outside the keyboard, this will dismiss it.
Run the app for both accounts, and you should have a similar experience:
The left is the customer account, and the right is the admin account.
Building the Message Widget
You're currently rendering each message in a Text widget; in this section, you'll garnish the UI to make it more informative.
Start by creating a message_widget.dart inside the messaging
package. Create a stateless widget that accepts a Message
object:
import 'package:flutter/material.dart';
import 'message_response.dart';
class MessageWidget extends StatelessWidget {
final Message message;
const MessageWidget({required this.message, Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Container();
}
}
Import '../auth/auth_view_model.dart'
and '../common/get_it.dart'
. Design-wise, the widget should be 75% of the screen width, and messages sent by the currently logged-in user should float to the left and otherwise to the right. Therefore, replace the build function with this:
Widget build(BuildContext context) {
final isSender = message.from.id == getIt<AuthViewModel>().auth.user?.id;
return Align(
alignment: isSender ? Alignment.topRight : Alignment.topLeft,
child: ConstrainedBox(
constraints: BoxConstraints(
maxWidth: MediaQuery.of(context).size.width * 0.75,
),
child: Container(),
),
);
}
Next, add borders, background color and a child
to the empty Container:
Widget build(BuildContext context) {
...
const radius = Radius.circular(10);
return Align(
...
child: ConstrainedBox(
...
child: Container(
padding: const EdgeInsets.all(10),
margin: const EdgeInsets.all(5),
decoration: BoxDecoration(
color: isSender ? Colors.black87 : Colors.grey[50],
border: Border.all(
color: isSender ? Colors.transparent : Colors.grey[300]!),
borderRadius: BorderRadius.only(
topLeft: radius,
topRight: radius,
bottomLeft: isSender ? radius : Radius.zero,
bottomRight: isSender ? Radius.zero : radius,
),
),
child: Column(),
),
),
);
}
Remember how a message
has different states? This needs to reflect on the UI. For each state, display a different widget.
-
sending
: a progress indicator. -
sent
: a double check icon if the current user sent the message. -
failed
: an error icon.
Import '../common/extensions.dart'
and create a method below build()
that switches on these states and returns the appropriate widget:
Widget _getStatus(Message message, bool isSender, BuildContext context) {
switch (message.status) {
case MessageStatus.sending:
return const SizedBox.square(
dimension: 10,
child: CircularProgressIndicator(
strokeWidth: 2,
),
);
case MessageStatus.sent:
return Row(
children: [
if (isSender)
const Icon(
Icons.done_all,
size: 10,
color: Colors.white,
),
if (isSender) const SizedBox(width: 10),
Text(
context.getFormattedTime(message.sentAt),
style: TextStyle(
color: isSender ? Colors.white : Colors.black,
fontSize: 10,
),
)
],
);
case MessageStatus.failed:
return const Icon(
Icons.error_outline,
size: 10,
color: Colors.redAccent,
);
}
}
context.getFormattedTime()
returns a time or date depending on the date of the message.
Now, add properties to the Column widget in build()
:
Widget build(BuildContext context) {
...
final msgData = message.data;
return Align(
...
child: ConstrainedBox(
...
child: Container(
...
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
msgData.text!,
style: TextStyle(
color: isSender ? Colors.white : Colors.black,
),
),
const SizedBox(height: 5),
_getStatus(message, isSender, context),
],
),
),
),
);
}
Lastly, go back to messages_screen.dart and import 'message_widget.dart'
. Then in _BodyWidget
, update the ListView in the build()
with:
Widget build(BuildContext context) {
...
return ListView.builder(
...
itemBuilder: (_, i) {
final message = messages[i];
return MessageWidget(
message: message,
key: ValueKey(message.data.clientId),
);
},
);
}
Run on both devices:
Supporting Images
In addition to texts, you'll add the functionality to send images. The customer will pick images from their photo gallery, and you'll upload these images to the back end. Additionally, you'll also display images from the back end. A message can contain only text, only images or both. You'll use image_picker to select images from the host device.
Go back to the MessageWidget and add these below the other variables in build():
final images = msgData.images ?? msgData.localImages;
final hasText = !msgData.text.isNullOrBlank();
final hasImages = images != null && images.isNotEmpty;
msgData.images
are URLs of the images already uploaded. You'll use Image.network()
to display such images. msgData.localImages
are file handles for images that exist on the host device; you'll display them with Image.file()
.
Next, import 'dart:io'
and 'package:image_picker/image_picker.dart'
. Afterwards, replace the Text widget in build()
with:
if (hasText)
Text(
msgData.text!,
style:
TextStyle(color: isSender ? Colors.white : Colors.black),
),
if (hasImages && hasText) const SizedBox(height: 15),
if (hasImages)
GridView.count(
crossAxisCount: images.length > 1 ? 2 : 1,
crossAxisSpacing: 5,
mainAxisSpacing: 5,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
childAspectRatio: 1,
children: images
.map<Widget>(
(e) => ClipRRect(
borderRadius: BorderRadius.circular(10),
child: e is XFile
? Image.file(File(e.path), fit: BoxFit.cover)
: Image.network('$e', fit: BoxFit.cover)),
)
.toList(),
),
You're displaying the images in a non-scrolling GridView.
Similarly, open messages_view_model.dart and import 'dart:io'
and 'package:image_picker/image_picker.dart'
. Then, add these below the instance variables in MessagesViewModel;
final _picker = ImagePicker();
final _images = <XFile>[];
List<XFile> get images => _images;
Next, add two methods in the view model:
void pickImages() async {
final images = await _picker.pickMultiImage(maxWidth: 1000);
if (images == null || images.isEmpty) return;
_images.addAll(images);
notifyListeners();
}
void removeImage(int index) {
if (index < 0 || ((_images.length - 1) > index)) return;
_images.removeAt(index);
notifyListeners();
}
While you'll call pickImages()
to add images, you'll invoke removeImage()
to remove an image.
Since you'll send the images alongside the text in sendMessage()
, update it like so:
void sendMessage() async {
...
if (text.isEmpty && _images.isEmpty) return;
...
final message = Message(
...
data: MessageData(
...
localImages: _images.map((e) => e).toList(),
),
...
);
_images.clear();
...
}
The last step here is to clear _images
in onDispose()
:
void dispose() {
...
_images.clear();
super.dispose();
}