Dwell Chat With Pusher Utilizing Supplier

Buyer satisfaction may make or break a product. A technique you may improve buyer satisfaction is thru a correct battle decision channel.
As a Software program Engineer, you may not work together straight with clients, however you’ll be able to construct a channel for them to simply attain out to buyer expertise (CX) specialists and vice versa. On this tutorial, you’ll construct Petplus, a cellular app for a veterinary firm that doubles as an animal shelter. You’ll flesh out the real-time messaging performance of the app, which is able to encompass two shoppers; one for customers and the opposite for CX specialists. On this course of, you’ll discover ways to:
- Construct advanced interactive UIs.
- Construct end-to-end messaging performance.
- Deploy a containerized internet service to GCP Cloud Run.
Getting Began
Obtain the undertaking by clicking Obtain Supplies on the prime or backside of this tutorial. Unzip the undertaking, and also you’ll discover two folders: backend and cellular. Written in Go, the backend listing accommodates the code that’ll energy the cellular app. Other than deploying it, you received’t be interacting with it a lot.
The cellular listing is the place you’ll work from; open it and open the starter folder inside with the newest model of Android Studio or Visible Studio Code. A part of the cellular app, just like the API integration, is already full so you’ll be able to give attention to the subject material of this tutorial.
Open pubspec.yaml and click on the Pub get tab that seems in your IDE. Open lib/fundamental.dart and run the undertaking to see this in your goal emulator or system:
For those who attempt to enroll, you’ll get an error since you nonetheless have to deploy the again finish. You’ll try this within the subsequent part.
Be aware: This tutorial assumes that you simply’re working from a Unix-like workstation corresponding to macOS or Ubuntu. Moreover, it is best to have some expertise with the Terminal and Firebase.
Establishing and Deploying the Again finish
On this part, you’ll arrange Pusher, Firebase, and GCP. You’ll additionally deploy the recordsdata within the backend listing to GCP Cloud Run. Pusher offers a hosted pub/sub messaging API referred to as Channels. This API lets the Petplus app create and hearken to occasions on a channel after which act upon them straight. The app will implement every customer support message as an occasion, thus making a real-time messaging performance between shoppers. GCP describes Cloud Run as a “serverless compute platform that abstracts away all infrastructure administration, so you’ll be able to give attention to what issues most — constructing nice functions.”
Establishing Pusher
Pusher will energy the real-time messaging again finish for the apps. Go to Pusher and join. After signup, click on “Get Began” on the setup web page:
Subsequent, full the Channels arrange by filling within the type like so:
Lastly, scroll all the way down to Step 2 on the web page and word down the next values: AppID, Key, Secret and Cluster:
Establishing Firebase
You’ll use Firebase for person account administration and persisting person messages.
Observe steps 1 and a pair of on this page to arrange Firebase Undertaking and allow Firebase Authentication. Be aware that Google authentication shouldn’t be required.
Subsequent, click on Firestore Database from the left pane on the Firebase Console underneath the Construct part. Allow the database.
Lastly, click on the Indexes tab and create a composite index like proven under:
When fetching the message historical past, the net service orders the question by the sentAt area; therefore you created an index so Firestore can course of the question.
Establishing GCP
When you’ve completed with Firebase, it’s important to arrange GCP for a similar undertaking. The online service makes use of two core GCP providers: Cloud Run and Cloud Storage. You’ll deploy the net service to Cloud Run, and the pictures uploaded by customers in messages shall be hosted on Cloud Storage. What’ll this price you? For those who comply with the steps on this tutorial precisely, it is best to keep throughout the free tier, so it’s free. Properly, free to you; Google is choosing up the invoice!
Now, open GCP Console. Settle for the phrases and circumstances should you nonetheless want to take action. Choose the Firebase undertaking you created earlier and enable billing for it. For brand new accounts, you may be eligible for a free trial; allow it.
Deploying the Go Service
Now, you’ll construct and deploy the net service app. The complexities of the deployment course of have been abstracted right into a bespoke Makefile to allow simpler facilitation. So that you solely must run two make
instructions to deploy. Nevertheless, it’s important to set up some software program:
- Golang: the net service is written in Go; therefore it’s wanted to compile it.
- Docker: to containerize the Go app earlier than deploying it with gcloud. Begin Docker after the set up.
- gcloud cli: to deploy the Docker container to cloud Run.
- yq: to parse the YAML configuration within the Makefile.
Subsequent, fill within the config file. Contained in the folder you unzipped earlier, utilizing any textual content editor, open the config.yaml file inside backend listing. Fill it like so:
-
port
: Go away this empty; it’ll be learn from Cloud Run’s setting variables. -
gcpProject
: The Firebase or GCP undertaking id. Yow will discover it within the Firebase undertaking settings. -
messageImagesBucket
: The identify of the bucket the place photos from messages shall be saved. You’ll be able to select a reputation your self utilizing these guidelines. -
pusherId
: Pusher AppId from earlier step. -
pusherKey
: Pusher key from earlier step. -
pusherSecret
: Pusher Secret from earlier step. -
pusherCluster
: Pusher Cluster from earlier step. -
firebaseAPIKey
: Firebase Internet API key. Yow will discover it within the Firebase undertaking settings, just like the Firebase undertaking id.
Contained in the backend listing is a Makefile; that is the deploy script. Utilizing Terminal, run these instructions sequentially from this listing:
-
make setup-gcp
: creates the storage bucket with the identify you crammed in above and permits Cloud Run for the undertaking. -
make deploy
: builds and deploys the docker container to Cloud Run.
If each instructions full efficiently, you’ll see this on the command line:
The cellular app wants the service URL, so copy it.
Good job on finishing this step!
Sending and Receiving Messages
Within the earlier part, you deployed the Go service and bought the service URL. On this part, you’ll arrange Pusher on the cellular and implement the messaging performance.
Configuring Pusher
In Android Studio or Visible Studio Code, open fundamental.dart, in fundamental()
, replace the appConfig
:
-
apiUrl
: the service URL from the deployment step. -
pusherAPIKey
: the Pusher API key from the Pusher step. -
pusherCluster
: the Pusher cluster from the Pusher step.
Contained in the messaging package deal, create a messages_view_model.dart file. Then create a category inside:
import 'package deal:flutter/materials.dart';
import 'package deal:pusher_channels_flutter/pusher_channels_flutter.dart';
import '../widespread/get_it.dart';
class MessagesViewModel extends ChangeNotifier {
PusherChannelsFlutter? pusher;
MessagesViewModel() {
_setUpClient();
}
void _setUpClient() async {
pusher = await getIt.getAsync<PusherChannelsFlutter>();
await pusher!.join();
}
@override
void dispose() {
pusher?.disconnect();
tremendous.dispose();
}
}
Supplier is getting used for state administration; therefore the view mannequin extends ChangeNotifier
.
In _setUpClient()
, you retrieved the Pusher shopper from getIt service locator and opened a connection. Since you’re citizen, you cleaned up after your self and closed this connection in dispose()
.
In idea, every part ought to work positive, however you’ll take a look at this within the subsequent step.
Receiving Messages
You’ll want two situations of the app operating on totally different units. One among which is an admin account and the opposite a buyer account. Keep in mind the admin checkbox on the signup web page earlier? Test it to create an admin account, and uncheck it to create a buyer account.
Run the app and join. It’s best to see this:
The left one is operating the person account, and the correct is the admin account:
Nonetheless in MessagesViewModel
, import 'message_response.dart'
, add extra occasion variables under pusher
then replace the constructor like so:
ultimate String channel;
ultimate _messages = <Message>[];
Record<Message> get messages => _messages;
MessagesViewModel(this.channel) {
...
}
channel
is a singular identifier for the road of communication between the client and the CX specialist. And _messages
is an inventory of despatched or acquired messages. You’ll use these within the following steps.
In _setUpClient()
, subscribe to new messages after the connection:
void _setUpClient() async {
...
pusher!.subscribe(channelName: channel, onEvent: _onNewMessage);
}
_onNewMessage()
shall be referred to as at any time when a brand new message is available in. Inside it, you’ll parse the information from Pusher right into a Message
object and replace the messages listing. So import 'dart:convert'
and declare _onNewMessage()
under _setUpClient()
:
void _onNewMessage(dynamic occasion) {
ultimate information = json.decode(occasion.information as String) as Map<String, dynamic>;
ultimate message = Message.fromJson(information);
_updateOrAddMessage(message);
}
Equally, declare _updateOrAddMessage()
under _onNewMessage()
:
void _updateOrAddMessage(Message message) {
ultimate index = _messages.indexOf(message);
if (index >= 0) {
_messages[index] = message;
} else {
_messages.add(message);
}
notifyListeners();
}
The directions above replace the listing if the message already exists, and it appends to it in any other case.
Subsequent, replace dispose()
to cease listening to new messages and clear the messages listing.
void dispose() {
pusher?.unsubscribe(channelName: channel);
pusher?.disconnect();
_messages.clear();
tremendous.dispose();
}
Sending Messages
Contained in the messaging
package deal, there’s a messages_repository.dart file which accommodates the MessagesRepository class. It’ll make all messaging-related API calls to your internet service on Cloud Run. You’ll invoke its sendMessage()
to ship a brand new message.
Now, import 'messages_repository.dart'
to MessagesViewModel. Then add two new occasion variables under the earlier ones and replace the constructor:
ultimate textController = TextEditingController();
ultimate MessagesRepository repo;
MessagesViewModel(this.channel, this.repo) {
...
}
Add these import statements:
import 'package deal:uuid/uuid.dart';
import '../auth/auth_view_model.dart';
Declare an async sendMessage()
under _onNewMessage()
. Later, you’ll invoke this methodology from the widget when the person hits the ship icon. Then retrieve the textual content and at the moment logged-in person like so:
void sendMessage() async {
ultimate textual content = textController.textual content.trim();
if (textual content.isEmpty) return;
ultimate currentUser = getIt<AuthViewModel>().auth.person;
}
Subsequent, create an occasion of the Message
class, clear the textual content from textController
and replace Supplier as follows:
void sendMessage() async {
...
ultimate message = Message(
sentAt: DateTime.now(),
information: MessageData(
clientId: const Uuid().v4(),
channel: channel,
textual content: textual content,
),
from: currentUser!,
standing: MessageStatus.sending,
);
textController.clear();
notifyListeners();
}
The app makes use of clientId
to determine all of the messages it sends uniquely. Two situations of message
are equal if their information.clientId
are the identical. For this reason ==
was overridden in each Message and MessageData.
A message
has three states which might be enumerated in MessageStatus
and right here’s what they imply:
-
sending
: there’s a pending API name to ship this message. -
despatched
: the API name returned, and the message was efficiently despatched. -
failed
: the API name returned, however the message didn’t ship.
Subsequent, in the identical methodology under the earlier items of code, ship the message and replace the messages listing.
void sendMessage() async {
...
ultimate success = await repo.sendMessage(message);
ultimate replace = message.copy(
standing: success ? MessageStatus.despatched : MessageStatus.failed,
);
_updateOrAddMessage(replace);
}
Construct and run the app, however don’t anticipate any modifications at this level. You’ll begin engaged on the UI subsequent.
Implementing UI
You’ve completed the heavy lifting, and now it’s time to color some pixels!
On this part, you’ll construct a textual content area to enter new messages and a ListView to show these messages.
Constructing the Messages Display screen
You’ll begin with the textual content area. Nonetheless in MessagesViewModel
, add one other occasion variable under the others:
ultimate focusNode = FocusScopeNode();
Including An Enter Subject
You’ll use this to regulate the visibility of the keyboard.
Open messages_screen.dart within the messaging package deal, import 'messages_view_model.dart'
and create a stateless widget like this:
class _InputWidget extends StatelessWidget {
ultimate MessagesViewModel vm;
ultimate double backside;
const _InputWidget({required this.vm, required this.backside, Key? key})
: tremendous(key: key);
@override
Widget construct(BuildContext context) {
return Container();
}
}
This empty widget accepts an occasion of MessagesViewModel
, which you’ll be utilizing in a second.
Exchange the construct methodology with this:
Widget construct(BuildContext context) {
return Rework.translate(
offset: Offset(0.0, -1 * backside),
little one: SafeArea(
backside: backside < 10,
little one: TextField(
minLines: 1,
maxLines: 3,
focusNode: vm.focusNode,
controller: vm.textController,
autofocus: false,
ornament: InputDecoration(
crammed: true,
fillColor: Theme.of(context).canvasColor,
hintText: 'Enter a message',
contentPadding: const EdgeInsets.symmetric(
horizontal: 20,
vertical: 5,
),
suffixIcon: IconButton(
onPressed: vm.sendMessage,
icon: const Icon(Icons.ship),
),
),
),
),
);
}
The construct methodology returns a Rework widget with a SafeArea; this ensures the textual content area at all times sticks to the underside whatever the visibility of the keyboard. Discover that you simply’re passing the focusNode
and textController
from the view mannequin to the textual content area. Moreover, the suffixIcon
, a ship icon, invokes the sendMessage()
of the view mannequin.
Subsequent, add two new occasion variables to MessagesViewModel
like so:
ultimate scrollController = ScrollController();
bool loading = true;
You may replace the scroll place of the ListView with scrollController
when a brand new message arrives. You may use loading
to find out the state of the messages display. Due to this fact, declare _scrollToBottom()
above dispose()
like so:
void _scrollToBottom() {
if (_messages.isEmpty) return;
WidgetsBinding.occasion.addPostFrameCallback((_) {
scrollController.jumpTo(scrollController.place.maxScrollExtent);
});
}
This scrolls to the underside of the ListView after the app has up to date it.
Likewise, declare _fetchPreviousMessages()
under _onNewMessage()
. It will fetch the message historical past when a person opens the messages display.
void _fetchPreviousMessages(String userId) async {
ultimate messages = await repo.fetchMessages(userId);
_messages.addAll(messages);
loading = false;
notifyListeners();
_scrollToBottom();
}
Equally, name _scrollToBottom()
in eachsendMessage()
and _updateOrAddMessage
after the decision to notifyListeners();
:
void _updateOrAddMessage(Message message) {
...
notifyListeners();
_scrollToBottom();
}
void sendMessage() async {
...
notifyListeners();
_scrollToBottom();
...
}
Now, name _fetchPreviousMessages()
because the final assertion in _setUpClient()
:
void _setUpClient() async {
...
_fetchPreviousMessages(channel);
}
Including the Messages View
Such as you did for _InputWidget
in messages_screen.darkish
, create one other stateless widget that accepts a MessagesViewModel
like this:
class _BodyWidget extends StatelessWidget {
ultimate MessagesViewModel vm;
ultimate double backside;
const _BodyWidget({required this.vm, required this.backside, Key? key})
: tremendous(key: key);
@override
Widget construct(BuildContext context) {
// 1
if (vm.loading) {
return const Middle(
little one: CircularProgressIndicator.adaptive(),
);
}
ultimate messages = vm.messages;
// 2
if (messages.isEmpty) {
return const Middle(little one: Textual content('You haven't despatched any messages but'));
}
// 3
return ListView.builder(
itemCount: messages.size,
controller: vm.scrollController,
padding: EdgeInsets.solely(backside: backside),
itemBuilder: (_, i) {
return Textual content(
messages[i].information.textual content ?? '',
key: ValueKey(messages[i].information.clientId),
);
});
}
}
- Show a progress indicator if the message historical past is loading.
- Show an error textual content if there aren’t any messages to show.
- Show a ListView of the messages. Within the interim, every message shall be a Textual content.
Lastly, import 'package deal:supplier/supplier.dart'
, '../widespread/get_it.dart'
and '../widespread/common_scaffold.dart'
. Then substitute the construct operate in MessagesScreen widget with:
Widget construct(BuildContext context) {
ultimate backside = MediaQuery.of(context).viewInsets.backside;
return ChangeNotifierProvider<MessagesViewModel>(
create: (_) => MessagesViewModel(channel, getIt()),
little one: Shopper<MessagesViewModel>(
builder: (ctx, vm, _) {
return CommonScaffold(
title: title,
physique: GestureDetector(
onTap: vm.focusNode.unfocus,
little one: _BodyWidget(vm: vm, backside: backside),
),
bottomNavigationBar: _InputWidget(vm: vm, backside: backside),
);
},
),
);
}
It will render _BodyWidget within the physique of the scaffold and _InputWidget as the underside navigation bar. Discover the strategy equipped to onTap
of the GestureDetector; when the person faucets exterior the keyboard, it will dismiss it.
Run the app for each accounts, and it is best to have an analogous expertise:
The left is the client account, and the correct is the admin account.
Constructing the Message Widget
You are at the moment rendering every message in a Textual content widget; on this part, you may garnish the UI to make it extra informative.
Begin by making a message_widget.dart contained in the messaging
package deal. Create a stateless widget that accepts a Message
object:
import 'package deal:flutter/materials.dart';
import 'message_response.dart';
class MessageWidget extends StatelessWidget {
ultimate Message message;
const MessageWidget({required this.message, Key? key}) : tremendous(key: key);
@override
Widget construct(BuildContext context) {
return Container();
}
}
Import '../auth/auth_view_model.dart'
and '../widespread/get_it.dart'
. Design-wise, the widget ought to be 75% of the display width, and messages despatched by the at the moment logged-in person ought to float to the left and in any other case to the correct. Due to this fact, substitute the construct operate with this:
Widget construct(BuildContext context) {
ultimate isSender = message.from.id == getIt<AuthViewModel>().auth.person?.id;
return Align(
alignment: isSender ? Alignment.topRight : Alignment.topLeft,
little one: ConstrainedBox(
constraints: BoxConstraints(
maxWidth: MediaQuery.of(context).measurement.width * 0.75,
),
little one: Container(),
),
);
}
Subsequent, add borders, background shade and a little one
to the empty Container:
Widget construct(BuildContext context) {
...
const radius = Radius.round(10);
return Align(
...
little one: ConstrainedBox(
...
little one: Container(
padding: const EdgeInsets.all(10),
margin: const EdgeInsets.all(5),
ornament: BoxDecoration(
shade: isSender ? Colours.black87 : Colours.gray[50],
border: Border.all(
shade: isSender ? Colours.clear : Colours.gray[300]!),
borderRadius: BorderRadius.solely(
topLeft: radius,
topRight: radius,
bottomLeft: isSender ? radius : Radius.zero,
bottomRight: isSender ? Radius.zero : radius,
),
),
little one: Column(),
),
),
);
}
Keep in mind how a message
has totally different states? This must mirror on the UI. For every state, show a distinct widget.
-
sending
: a progress indicator. -
despatched
: a double examine icon if the present person despatched the message. -
failed
: an error icon.
Import '../widespread/extensions.dart'
and create a way under construct()
that switches on these states and returns the suitable widget:
Widget _getStatus(Message message, bool isSender, BuildContext context) {
swap (message.standing) {
case MessageStatus.sending:
return const SizedBox.sq.(
dimension: 10,
little one: CircularProgressIndicator(
strokeWidth: 2,
),
);
case MessageStatus.despatched:
return Row(
youngsters: [
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,
measurement: 10,
shade: Colours.redAccent,
);
}
}
context.getFormattedTime()
returns a time or date relying on the date of the message.
Now, add properties to the Column widget in construct()
:
Widget construct(BuildContext context) {
...
ultimate msgData = message.information;
return Align(
...
little one: ConstrainedBox(
...
little one: Container(
...
little one: Column(
crossAxisAlignment: CrossAxisAlignment.begin,
youngsters: [
Text(
msgData.text!,
style: TextStyle(
color: isSender ? Colors.white : Colors.black,
),
),
const SizedBox(height: 5),
_getStatus(message, isSender, context),
],
),
),
),
);
}
Lastly, return to messages_screen.dart and import 'message_widget.dart'
. Then in _BodyWidget
, replace the ListView within the construct()
with:
Widget construct(BuildContext context) {
...
return ListView.builder(
...
itemBuilder: (_, i) {
ultimate message = messages[i];
return MessageWidget(
message: message,
key: ValueKey(message.information.clientId),
);
},
);
}
Run on each units:
Supporting Photographs
Along with texts, you may add the performance to ship photos. The shopper will choose photos from their photograph gallery, and you may add these photos to the again finish. Moreover, you may additionally show photos from the again finish. A message can comprise solely textual content, solely photos or each. You may use image_picker to pick out photos from the host system.
Return to the MessageWidget and add these under the opposite variables in construct():
ultimate photos = msgData.photos ?? msgData.localImages;
ultimate hasText = !msgData.textual content.isNullOrBlank();
ultimate hasImages = photos != null && photos.isNotEmpty;
msgData.photos
are URLs of the pictures already uploaded. You may use Image.network()
to show such photos. msgData.localImages
are file handles for photos that exist on the host system; you may show them with Image.file()
.
Subsequent, import 'dart:io'
and 'package deal:image_picker/image_picker.dart'
. Afterwards, substitute the Textual content widget in construct()
with:
if (hasText)
Textual content(
msgData.textual content!,
model:
TextStyle(shade: isSender ? Colours.white : Colours.black),
),
if (hasImages && hasText) const SizedBox(peak: 15),
if (hasImages)
GridView.depend(
crossAxisCount: photos.size > 1 ? 2 : 1,
crossAxisSpacing: 5,
mainAxisSpacing: 5,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
childAspectRatio: 1,
youngsters: photos
.map<Widget>(
(e) => ClipRRect(
borderRadius: BorderRadius.round(10),
little one: e is XFile
? Picture.file(File(e.path), match: BoxFit.cowl)
: Picture.community('$e', match: BoxFit.cowl)),
)
.toList(),
),
You are displaying the pictures in a non-scrolling GridView.
Equally, open messages_view_model.dart and import 'dart:io'
and 'package deal:image_picker/image_picker.dart'
. Then, add these under the occasion variables in MessagesViewModel;
ultimate _picker = ImagePicker();
ultimate _images = <XFile>[];
Record<XFile> get photos => _images;
Subsequent, add two strategies within the view mannequin:
void pickImages() async
void removeImage(int index)
Whilst you’ll name pickImages()
so as to add photos, you may invoke removeImage()
to take away a picture.
Since you may ship the pictures alongside the textual content in sendMessage()
, replace it like so:
void sendMessage() async {
...
if (textual content.isEmpty && _images.isEmpty) return;
...
ultimate message = Message(
...
information: MessageData(
...
localImages: _images.map((e) => e).toList(),
),
...
);
_images.clear();
...
}
The final step right here is to clear _images
in onDispose()
:
void dispose() {
...
_images.clear();
tremendous.dispose();
}
Displaying Photographs
It’s important to present the person the pictures they selected and in addition enable them to take away them. So, head over to messages_screen.dart and import 'dart:io'
and 'package deal:image_picker/image_picker.dart'
. Afterward, create a stateless widget under _InputWidget
. This widget will render a single picture.
class _ImageWidget extends StatelessWidget {
ultimate XFile file;
ultimate VoidCallback onRemove;
ultimate double measurement;
const _ImageWidget({
Key? key,
required this.onRemove,
required this.file,
required this.measurement,
}) : tremendous(key: key);
@override
Widget construct(BuildContext context) {
return Container();
}
}
Because the photos it will show are native recordsdata from the picture picker, you need not deal with picture URLs such as you did for MessageWidget. Exchange the construct()
of _ImageWidget with:
Widget construct(BuildContext context) {
ultimate imageSize = measurement - 15;
return Padding(
padding: const EdgeInsets.solely(left: 5, proper: 10),
little one: SizedBox(
peak: measurement,
width: measurement,
little one: Stack(
clipBehavior: Clip.none,
youngsters: [
Positioned(
top: 15,
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Image.file(
File(file.path),
width: imageSize,
height: imageSize,
fit: BoxFit.cover,
),
),
),
Positioned(
top: -10,
right: -10,
child: IconButton(
onPressed: onRemove,
icon: const Icon(Icons.cancel),
),
)
],
),
),
);
}
It will show a picture with spherical edges, with an “x” icon on the top-right.
Subsequent, declare a variable inside construct()
of _InputWidget
, above the return assertion.
Widget construct(BuildContext context) {
ultimate imageSize = MediaQuery.of(context).measurement.width * 0.21;
...
}
Nonetheless, in _InputWidget
, wrap the TextField
in a Column
. You may show a horizontal listing of photos above the textual content area like so:
Widget construct(BuildContext context) {
...
return Rework.translate(
...
little one: SafeArea(
...
little one: Column(
mainAxisSize: MainAxisSize.min,
youngsters: [
SizedBox(
height: vm.images.isEmpty ? 0 : imageSize,
child: ListView.builder(
itemCount: vm.images.length,
scrollDirection: Axis.horizontal,
itemBuilder: (ctx, i) {
final file = vm.images[i];
return _ImageWidget(
onRemove: () => vm.removeImage(i),
file: file,
measurement: imageSize,
);
},
),
),
TextField(
...
),
],
),
),
);
}
Add a suffix icon that’ll set off the picture picker:
TextField(
...
prefixIcon: IconButton(
onPressed: vm.pickImages,
icon: const Icon(Icons.add),
),
)
Run the app on each units and ship a picture from any of them. You will note one thing like this:
That is all. Nice job on finishing this tutorial!
The place to Go From Right here
The ultimate listing contained in the cellular listing accommodates the total code used on this tutorial, and you will discover it within the zipped file you downloaded earlier. You’ll be able to nonetheless obtain it by clicking Obtain Supplies on the prime or backside of this tutorial.
On this tutorial, you deployed a Golang service on Cloud Run and realized methods to use Pusher to implement real-time chat. To make enhancements to the reminiscence footprint and efficiency of the app, one suggestion is to paginate the chat, letting messages load in pages moderately than loading unexpectedly. You’ll be able to enhance the app’s performance by including help for resending messages that didn’t ship. You would additionally use AnimatedList as an alternative of ListView to enhance the granularity of the doorway of the message widgets. After taking part in round, keep in mind to delete the project from GCP, so it does not incur any fees.
We hope you loved this tutorial. When you have any questions or feedback, please be part of the discussion board dialogue under!