In this tutorial, you're going to build a real-time chat web app that supports Google authentication and both text and image messages, and you're going to do it without writing a single line of server-side code. "But how?" you may exclaim. Dart, with its comprehensive core libraries and well-honed syntax, combined with the awesome power of Firebase, an application platform with a real-time database, authentication, file storage, and static hosting. That's how!
Oh, and let's not forget that the app will be structured by everyone's favorite framework, Angular (or in this case, AngularDart). With such a structure in place, it will be easy to maintain and expand the code to create a more full-featured chat app. It will also save you from having to write lots of DOM-manipulation code, and it will divide up code tasks into services and components to maximize reusability.
Code tested with Dart SDK 1.24.2, Angular Dart 4.0.0, and Firebase 4.2.0 JavaScript API.
Credit
This tutorial was modeled after the JavaScript version appearing here: Firebase: Build a Real Time Web Chat App.
What You'll Learn
- Sync data using the Firebase Realtime Database and Firebase Storage.
- Authenticate your users using Firebase Auth.
- Deploy your web app on Firebase static hosting.
- Create a web app using the AngularDart framework.
What You Won't Learn
This is an intermediate tutorial intended for those who are familiar with basic programming concepts and HTML/CSS. We will not spend time on these topics, instead focusing on AngularDart and Firebase.
The Stack
Let's take a quick look at the technologies you'll be using for this project. If you're missing any of them, you'll need to follow the instructions to get them installed.
Dart
Dart is an open-source, scalable, object-oriented programming language, with robust libraries and runtimes, for building web, server, and mobile apps. It was originally developed by Google, but has since become an ECMA standard.
Dart Primers
Check out the Dart Language Tour for a crash course in the Dart language. If you already know JavaScript, Java, PHP, ActionScript, C/C++/C#, or another "curly brace" language, you'll find Dart to be familiar, and you can be productive with Dart within an hour or so.
If you like learning from videos, take a look at Learn Dart in One Video.
Get Dart
Before you can get into the exciting world of writing Dart code, you need to download the Dart SDK and, for web apps, a special browser called Dartium:
- Dart SDK and Dartium
- On Windows? Try the Dart for Windows installer, or use Chocolatey.
- On Mac? Use Homebrew.
- On Linux? Use apt-get or the Dart Debian package.
- Dart-Up is a great option for Windows, Mac, or Linux.
Dart Tools and IDEs
For ideas on what editor you should use to work on a Dart project, or to learn more about Dart's awesome suite of developer tools, check out the Dart Tools page. Note that since you need to import external libraries for this project, you will not be able to use the online DartPad environment.
AngularDart
AngularDart is the Dart version of the popular Angular web application framework. The concepts and semantics are currently nearly identical, but since AngularDart is a separate project, it is not guaranteed to remain entirely in sync with the TypeScript version. If you're wondering why you might want to use AngularDart instead of the more mainstream Angular, here are 7 reasons.
AngularDart Material Design Components
Many of the UI widgets used in this tutorial make use of the excellent angular_components package for AngularDart. These are the AngularDart components that Google uses to build the sophisticated, mission-critical apps that bring in much of Google’s revenue, such as AdWords and AdSense. The availability of these components helps make AngularDart such a great choice for building full-featured web apps quickly and easily.
Other Tutorials
For other great tutorials and code labs to help you get up and running quickly with AngularDart, take a look at the Dart WebDev Code Labs page.
Firebase
Firebase is a powerful back-end platform that provides data storage, file storage, user authentication, static hosting, and more. It's Firebase that makes it possible to create a complex, multi-user app with persistent data using so little front-end code. It has APIs for Android, iOS, and JavaScript, as well as a REST API. For this project, you'll make use of a thin Dart wrapper around the JavaScript API.
The Chat App
The world could always use another chat application, right? Yes! So let's make one. When you're finished, you'll have something that looks like this:
How It Works
Your chat app will be a web application written with Dart and AngularDart, and it will run in a web browser. During development, you'll run it in Dartium, which is a special build of Chromium that includes the Dart virtual machine. Each running client will post a user's chat messages to a Firebase real-time NoSQL database, and all clients will automatically receive any changes to it. Changes in hand, the client will update the display.
Step 1: Get the Starter Code
Since this tutorial isn't about HTML, CSS, or UI, you're going to start off with all of that already done.
Download
Visit the GitHub repository and either clone it or download a ZIP file. You'll see a handy, green Clone or download button on the site to help you with this.
Open Project and Acquire Dependencies
Open the project in your favorite IDE. If you're using WebStorm (recommended), you need only open the pubspec.yaml file and you'll see a Get dependencies link in the upper right of the editor. (The links may ask you to Enable Dart support first.) Alternatively, this command is available from the file's context menu in the Project panel.
With lesser editors, you can get your dependencies using the command line. Be sure to navigate to your project's root first:
pub get
Step 2: Create a Firebase Project and Set Up Your App
Before you can use a Firebase database, you need to log into the Firebase site and create a new project.
Create Project
In the Firebase Console, click on Add project. You can name it whatever you like, but it can help to give it the same name as your app. Watch out, though! Dart project names and file names use lots of underscores, and those aren't allowed in Firebase project names. Use dashes instead.
And don't worry! Firebase has a very generous free tier, so it typically won't cost you anything to develop a new app.
Get Your Web App Credentials
In the Firebase Console for your new project, in the Overview section, click the Add Firebase to your web app button. This will reveal an HTML/JavaScript snippet that looks something like this:
Of course, the values of the properties will be different from those shown here. They will be unique to your project.
Add the Credentials to Your App
Since we're not dealing with JavaScript in this lesson, we can't use the snippet directly, so copy each of these four properties' values into the corresponding empty strings inside your Angular service file:
lib/src/services/database_service.dart
fb.initializeApp(
apiKey: "",
authDomain: "",
databaseURL: "",
storageBucket: ""
);
You won't be needing any of the other values for this tutorial. We'll go over everything in this file and what it means later. For now, just get your Firebase project's values in there.
Enable Google Auth
Your chat users are going to use their Google IDs to log into your app, and Firebase is going to help you make that possible. First, you need to enable Google authentication in your Firebase Console.
Click Authentication in the left-side navigation, then select the SIGN-IN METHOD tab. Edit the Google provider entry, and you should see something like this:
Make sure the Enable switch is turned on, then click SAVE.
Step 3: Install the Firebase Command-Line Interface
In order to host your app on Firebase's servers, you'll need the Firebase command-line interface (CLI). If you're not interested in hosting there, you can skip this step.
In the Firebase Console for your project, go to the Hosting section, click GET STARTED, and follow the instructions to download and install the CLI. Note that it's a NodeJS app, so you'll need to have installed that already.
With NodeJS on your system, you can use npm to install the Firebase tools. Enter this command in a terminal:
npm install -g firebase-tools
Once you've got it installed, run:
firebase --version
Next, authorize the Firebase CLI by running:
firebase login
Now you need to initialize your project for use by the Firebase CLI. From your project's root, run:
firebase init
You will be asked a series of questions. (Questions from Firebase CLI version 3.11.0.)
- Are you ready to proceed? Yes. (Probably.)
- What Firebase CLI features do you want to setup for this folder? You will only need the
Hosting
option to be selected. - Select a default Firebase project for this directory. Select your project's ID. This will be the name you entered in the Firebase Console.
- What do you want to use as your public directory? For Dart projects, you'll typically want to enter
build/web
here. - Configure as a single-page app (rewrite all urls to /index.html)? No. A project that uses the Angular router might need this option, but not this one.
At this point, the CLI will write a few files to your project's build directory, but those will be overwritten by Dart's build process later, anyway.
Step 4: Run the App
Now it's time to make sure you can run this thing. With a properly set-up WebStorm, this is as easy as choosing Debug from the context menu (right-click, Ctrl-click, whatever...) of web/index.html. That will normally run your project in Dartium, via Pub Serve.
Running Without WebStorm
If you need to do things the hard way, navigate to your project's root directory in a terminal and run:
pub serve
By default, Dart's server will run on localhost:8080
.
To launch Dartium, navigate to its directory in your finder or file explorer and double-click the Chromium executable file.
Note: While this tool is referred to as Dartium, the executable on Mac/Linux is named
Chromium
, and on Windows it's namedchrome.exe
. For instance, if you used Dart for Windows to install the Dart SDK and Dartium, the default path to Dartium would be something likeC:\Program Files\dart\chromium\chrome.exe
. Since you'll be running this often, it might be a good idea to create a desktop shortcut for Dartium (on Windows, right-click your Desktop and select New -> Shortcut).
Enter localhost:8080
into the address bar. If it's your first execution of a new project, be patient as Dart runs transformers on your build. Before long, you should see your running project appear.
The header will look a little goofy just now, but you'll use some Angular features to fix that up soon enough. Also, the UI doesn't do anything yet; you'll add the code for that in upcoming steps.
Step 5: Project Tour
Before getting any deeper, let's take a look around the project as it exists now.
Web
In the project's web folder, you'll find the files that function as the app's starting point.
Index
The notable parts of the web/index.html file include a few stylesheet links and a few script tags. You import a CSS flexbox helper, courtesy of the Polymer project, called iron-flex-layout.css, that helps to easily lay out parts of the UI. Also, you pull in an icon font from Google.
You're going to be using a Dart wrapper around the Firebase JavaScript library, so you need to include that JavaScript code here. After that, the standard Dart script files are included.
Within the <body>
, you'll find just one tag: <my-app>
. This represents your chat app's main Angular component.
Main
Dart applications always start with the top-level main()
function, and for your app, that can be found in the web/main.dart file, which gets loaded up when you run the app via index.html. That file imports two important pieces of code: The AngularDart startup code and your application's main component, AppComponent
. All main()
has to do is call Angular's bootstrap()
function, passing it the Dart class name of your app component.
Services
This project has only one service: DatabaseService
. It can be found in lib/src/services/database_service.dart. In Angular, a service is loosely defined as any piece of code that provides something to your app. The database service provides access to Firebase for you, including handling user authentication, interaction with the real-time database, and file uploads. In a more complex app, you might divide up these tasks into multiple services.
You make use of a service through Angular's dependency injection system. We'll go over how to take advantage of that as it comes up in our discussion of view components.
Dart's ability to assign an alias (namespace) to imports comes into play here. The Firebase package has a number of common symbols in it, like Event
, that you might need to differentiate, so they are placed behind the name fb
.
Angular services are typically decorated with @Injectable()
, which allows them to have services injected into them, should the need arise.
The class declares a number of member variables to store references to the various database services, with each of the types prefixed by fb
, since they are part of the Firebase library. In the constructor, Firebase's initializeApp()
function is called with your Firebase credentials from the online console. Once the constructor runs, your app is connected to the Firebase servers and ready to do your bidding.
View Components
The project is comprised of just two view components: AppComponent
and AppHeader
.
App Component
The app's main component files, found in lib, compose a custom Angular component that serves as the default view.
HTML
The component's HTML file contains the view's template. Inside, you'll find a reference to <app-header>
, which is the project's other view component. The remainder of the markup is the chat UI.
Dart
In the Dart file is a class that the Angular framework will instantiate when the component's selector
is encountered. The web/index.html file contains a <my-app>
tag. Since that's not a standard HTML tag, Angular handles it, and it does so by instantiating AppComponent
.
In addition to importing the AppHeader
class, the component needs to have it declared in the directives
list of the @Component
decorator in order to use it (all components are also directives.
The providers
list is where the component can register services with the dependency injector. After importing the DatabaseService
class, you include it in the list, which tells Angular to get ready to inject it into components in need. Services need only be registered once. They do not need to appear in the providers
list of every component that needs them. Now that the database service has been registered here, the injector is aware of it and will be able to provide it to all children of AppComponent
, such as AppHeader
.
The styleUrls
list is where you can import CSS files that the view template may need. It's also possible to use <style>
tags in the template itself, but sometimes it's nice to separate your CSS code into a file.
Inside the AppComponent
class, you can see the dependency injector in action. When Angular encounters the <my-app>
tag, it creates a new instance of this class, and it examines the constructor's parameter list to identify symbols that have been registered with the injector. When it finds one, such as DatabaseService
, the framework checks to see if it's already managing an instance of it. If necessary, Angular instantiates the service, and if it has already done so, it simply provides the existing instance. Now AppComponent
has its very own reference to the database service.
App Header
The header component is basic, with an unremarkable view template and no new concepts in its component class. Remember, even though the header component requires a reference to the database service, it does not need to register the service in its providers
list, since that was done already by AppComponent
.
Step 6: User Sign-In
Time to set up user authentication for your chat app. Fortunately, Firebase makes this just about as easy as it can get.
User Service
Before you can authenticate your users with Google authentication, you need a place to store the user data. Add a new member variable to the database service class:
lib/src/services/database_service.dart
fb.User user;
Now you need to initialize a few values in the database service. Add the following code to DatabaseService
's constructor, after the call to initializeApp()
:
lib/src/services/database_service.dart
_fbGoogleAuthProvider = new fb.GoogleAuthProvider();
_fbAuth = fb.auth();
_fbAuth.onAuthStateChanged.listen(_authChanged);
You create a new instance of GoogleAuthProvider
for use in the sign-in process, get a reference to the Firebase auth object for your project, and set up an event listener to handle changes in your app's auth status. Note that all of these variables are prefixed with an underscore, which is Dart's way of making them private to the current library. Code outside the service should not have access to these references.
You'll need to add that event handler to the service class next:
lib/src/services/database_service.dart
void _authChanged(fb.User fbUser) {
user = fbUser;
}
Whenever the Firebase auth status changes, _authChanged()
will be called. If fbUser
is null
, that means the change event was triggered by a user signing out. Otherwise, user
will now reference the signed-in user's details.
Now you need to give the user a way to sign in and out. Add these methods to the class:
lib/src/services/database_service.dart
Future signIn() async {
try {
await _fbAuth.signInWithPopup(_fbGoogleAuthProvider);
}
catch (error) {
print("$runtimeType::login() -- $error");
}
}
void signOut() {
_fbAuth.signOut();
}
In signIn()
, you use Dart's fabulous async/await
syntax to treat asynchronous code as though it were synchronous. Async functions always return a Future
, though not explicitly in this case. Firebase provides you with a convenient sign-in popup, and you pass it the Google auth provider you created in the service's constructor. The user will be asked to sign in and grant permissions, and if any exceptions are thrown, they'll be caught by the try/catch
block. Shortly, you will tie the signIn()
method to the Sign In button in the UI.
The signOut()
method, which will be called when the user clicks Sign Out, does nothing more than call Firebase's signOut()
function, returning nothing (void
).
Since these methods need to be called from outside the service, specifically by buttons in component templates, they need to be declared public (no underscore prefix).
The App Header
All of the user management UI is sitting awkwardly in your app header's view template. A little Angular magic will bring it to life.
NgIf
You don't want to see the sign-in
and sign-out
blocks on the screen at the same time, so fix that first:
lib/app_header.html
...
<div id="sign-in" *ngIf="dbService.user == null" class="horiz">
...
<div id="sign-out" *ngIf="dbService.user != null" class="horiz">
...
AngularDart makes it easy to control the presence or absence of an element in the DOM by means of the NgIf directive. The AppHeader
class has a public reference to your database service called fbService
that you can access from within the template. With the NgIf attribute directives added to the <div>
elements, the two UI blocks will never be visible at the same time.
Data Binding
Next, you want to display the signed-in user's name and profile image in the header, and you can do that by adding a src
binding to the <img>
tag and an interpolated expression to the user-name
<div>
:
lib/app_header.html
...
<img class="icon" [src]="dbService.user?.photoURL">
<div id="user-name">{{dbService.user?.displayName}}</div>
...
Using the square bracket syntax for [src]
allows you to directly access the src
property on the <img>
DOM element, as opposed to going through the src
attribute. If you don't understand that distinction, read up on it in the docs, as it's an important concept when creating Angular applications. You bind the src
property to the value of user.photoURL
, which is stored in your database service instance. Note the use of the Elvis operator (?.
), used to prevent Angular from choking if it tries to obtain the value of photoURL
when user
is null
.
The Angular interpolation expression, delimited by double curly braces ({{ }}
), binds the value of user.displayName
into the text node of the user-name
<div>
.
UI Events
All that's left is to listen for trigger
events on the two <material-button>
elements. Why not click
, you ask? Well, trigger
covers all the ways a user might activate a UI control.
lib/app_header.html
...
<material-button ... (trigger)="dbService.signIn()">Google Sign In
</material-button>
...
<material-button ... (trigger)="dbService.signOut()">Sign Out
</material-button>
...
With those event handlers in place, your buttons will directly call the signIn()
and signOut()
methods on your database service.
Run It
Your header bar should now be fully functional, and you should be able to sign in and out with your Google credentials. Your name and profile pic (if any) should display in the header bar while you're signed in.
Step 7: Read Messages
What's a chat app without messages, right? In this step, you'll set up your database with a few starter messages and add the code that downloads and displays them.
Import Starter Messages
Back in your Firebase console, visit the Database section using the left-side navigation. From your database's overflow menu (looks like a vertical ellipsis), select Import JSON. Your starter code included a file at data/initial_messages.json. Browse to your project and select that file. This will replace any data currently in your database.
Create a Message Class
Unlike some web languages I could mention, Dart has real classes and types, and you can take advantage of the structure and type safety those features provide. To that end, you will create a class to store message data. Create a folder for your project's data models, then add a new file with the following class:
lib/src/models/message.dart
class Message {
final String name;
final String text;
String photoURL;
String imageURL;
Message(this.name, [this.text, String photoURL, this.imageURL]) {
this.photoURL = photoURL ?? "https://lh3.googleusercontent.com/-XdUIqdMkCWA/AAAAAAAAAAI/AAAAAAAAAAA/4252rscbv5M/photo.jpg";
}
Message.fromMap(Map map) :
this(map['name'], map['text'], map['photoURL'], map['imageURL']);
Map toMap() => {
"name": name,
"text": text,
"photoURL": photoURL,
"imageURL": imageURL
};
}
We won't go over this code in great detail, but there are several things to mention. First, when the app displays a message, it will display either text or an image, but never both. Any message that has an imageURL
value is an image message, and even if there is something in text
, it will be ignored.
Second, the only constructor parameter that's required is name
, representing the name of the sender, but if no photoURL
is passed in, a default is set that shows a generic avatar image.
Lastly, since Firebase is based on JSON data, it can only handle a few primitive types, and Message
isn't one of them. That means you need an easy way to initialize a Message
from a standard Map
and a way to create a Map
from a Message
instance. Map
is a type that the Firebase wrapper can handle, as it's analogous to a plain JavaScript object. The named constructor fromMap()
and the toMap()
method handle these conversions for you. There are more interesting ways to achieve this kind of data serialization in Dart, but this simplistic approach works well enough for a demo app.
Message Service
Before you can display messages, you must retrieve them. This will be the job of the database service.
You will need to add a new import at the top of your service file to include your new Message
class:
lib/src/services/database_service.dart
import '../models/message.dart';
Add a member variable to store incoming messages, perhaps just beneath the user
declaration:
lib/src/services/database_service.dart
List<Message> messages;
In the class constructor, you need to initialize a few more variables. Add this code anywhere after the call to initializeApp()
:
lib/src/services/database_service.dart
_fbDatabase = fb.database();
_fbRefMessages = _fbDatabase.ref("messages");
The first line will create a reference to your Firebase database, and then, for convenience, the second stores a more specific reference to the messages
collection within the database.
When a user logs in, you'll need to grab the existing messages, but you'll limit it to the last 12 so that the user doesn't get overwhelmed by a long chat history. Add this code block to the end of the _authChanged()
method:
lib/src/services/database_service.dart
if (user != null) {
messages = [];
_fbRefMessages.limitToLast(12).onChildAdded.listen(_newMessage);
}
This code sets up an event listener to be called every time a new child is added to your database's messages
collection. To start things off, _newMessage()
will be called up to 12 times, depending on how many messages exist in the database.
Add the _newMessage()
method:
lib/src/services/database_service.dart
void _newMessage(fb.QueryEvent event) {
Message msg = new Message.fromMap(event.snapshot.val());
messages.add(msg);
}
Each time this handler is called, the event
object will contain a snapshot
. A Firebase snapshot is a representation of data in the database at some point in time. For this handler, you can expect each snapshot to be a new chat message, but it must first be converted into a Dart Map
using the call to val()
. Since the Message
class was designed to be instantiated from a Map
, you can send the return value of val()
straight to the named constructor. Once you've got a valid Message
object, you can add it to your List
.
If you'd like to check that your code is working at this point, you could add something like print(msg.text);
to the _newMessage()
function and run the app. You should see 3 messages appear in your debug console.
Message Display
Now that your service is retrieving messages, you need to get them on the screen. That's where the main app component comes in.
It would be best if your users weren't messing around with the chat UI before logging in, so let's make that impossible. Add an Angular binding expression to the chat
element's hidden
attribute:
lib/app_component.html
...
<div id="chat" [hidden]="dbService.user == null" ...>
...
The AppComponent
class has an injected instance of the database service, and you can use this to test the value of user
. If there isn't a user, there should be no chat UI. If you test the app now, you should see that most of the page elements appear only after signing in. Unless you explicitly sign out or clear your browser cache after having signed in, you will be automatically signed in every time you re-run the app.
You should have an empty <div>
in your code with the CSS class msg-container
. That element needs just one child, but it's a doozy. I'm going to leave it here and let you mull it over for a few minutes:
lib/app_component.html
...
<div class="msg-container ...>
<div *ngFor="let msg of dbService.messages"
class="message layout horizontal">
<img [src]="msg.photoURL" class="icon">
<div>
<div class="name">{{msg.name}}</div>
<div *ngIf="msg.imageURL == null">{{msg.text}}</div>
<div *ngIf="msg.imageURL != null">
<a [href]="msg.imageURL" target="_blank">
<img [src]="msg.imageURL" class="message-image">
</a>
</div>
</div>
</div>
</div>
...
Angular's NgFor directive acts like a repeater, looping over the elements of a List
(array) and creating a DOM node for each. NgFor is included as an attribute on the DOM element you'd like to repeat, in this case an element that's set up to display a chat message. It works much like Dart's for...in
loop. For every msg
in dbService.messages
, a new <div>
will be stamped into the DOM, and within that, data bindings display the various parts of the msg
.
The first <img>
tag binds its src
property to the photoURL
field of a Message
instance. Interpolations are used to produce similar bindings for name
and text
. The NgIf directive is used to make sure only text or an image is displayed, but never both.
If it's an image message, the uploaded image is wrapped in an anchor tag so the user can click to look at the full-size image in a new browser tab.
Step 8: Send Messages
In this section, you will add the ability for users to send messages. This will make your chat app 98% more attractive to users.
Message Service
Once again, you'll start by beefing up your database service. Add the sendMessage()
function to DatabaseService
:
lib/src/services/database_service.dart
Future sendMessage({String text, String imageURL}) async {
try {
Message msg = new Message(user.displayName, text, user.photoURL, imageURL);
await _fbRefMessages.push(msg.toMap());
}
catch (error) {
print("$runtimeType::sendMessage() -- $error");
}
}
The function takes two optional, named parameters. If this is a text message, text
should be included and imageURL
will be null
; vice versa for an image message. Next, you create a Message
instance with the passed-in data and a few values from the logged-in user. Dart's async/await
syntax makes the asynchronous call look like a synchronous one, allowing you to use a standard try/catch
block to intercept exceptions. With your database's messages
collection reference, you push()
the new message to Firebase. Of course, you convert the Message
instance to a Map
first so that Firebase knows what to do with it.
Message Input
Now that your service knows how to send a message to Firebase, you need to teach your UI to take advantage of it.
Your app component needs a place to store the user's text input. Add this member variable to the AppComponent
class, maybe just beneath the fbService
declaration:
lib/app_component.dart
String inputText = "";
Using Angular's data-binding feature, this property will stay in sync with what the user types into the message input box.
Add this method to the class, as well:
lib/app_component.dart
void sendTextMessage() {
String messageText = inputText.trim();
if (messageText.isNotEmpty) {
dbService.sendMessage(text: messageText);
inputText = "";
}
}
Whenever the user presses Enter in the input box or activates the Send button, sendTextMessage()
will spring into action. If everything about the user's message is kosher, the text will be sent on to your database service and the user input will be emptied.
Use the NgModel directive to set up a two-way binding on your message input (the <material-input>
element in the file), then add some special Angular event syntax to detect the Enter key. Throw a trigger
handler on the Send button, too:
lib/app_component.html
<material-input [(ngModel)]="inputText" (keyup.enter)="sendTextMessage()">
</material-input>
<material-button (trigger)="sendTextMessage()">Send
</material-button>
If you test your code now, you should discover that you can send text messages! You can watch the database change in real time from the Database section of your Firebase console.
Step 9: Send Images
Your app at this point is amazing, and I wouldn't blame you a bit if you just walked away and called it a day. But if you did that, you'd miss out on the killer feature, the one that'll land you in the history books: Images! To do it, you will use Firebase Storage, a file/blob database service.
Image Service
You need to initialize one more thing in your service's constructor. Add this line at the end:
lib/src/services/database_service.dart
_fbStorage = fb.storage();
Now you can access Firebase's file storage service.
Next, teach your database service to send image messages by adding this new method:
lib/src/services/database_service.dart
Future sendImage(File file) async {
fb.StorageReference fbRefImage =
_fbStorage.ref("${user.uid}/${new DateTime.now()}/${file.name}");
fb.UploadTask task =
fbRefImage.put(file, new fb.UploadMetadata(contentType: file.type));
StreamSubscription sub;
sub = task.onStateChanged.listen((fb.UploadTaskSnapshot snapshot) {
print("Uploading Image -- Transfered ${snapshot.bytesTransferred}/${snapshot.totalBytes}...");
if (snapshot.bytesTransferred == snapshot.totalBytes) {
sub.cancel();
}
}, onError: (fb.FirebaseError error) {
print(error.message);
});
try {
fb.UploadTaskSnapshot snapshot = await task.future;
if (snapshot.state == fb.TaskState.SUCCESS) {
sendMessage(imageURL: snapshot.downloadURL.toString());
}
} catch (error) {
print(error);
}
}
Okay, I admit it. Up until now, most of the functions have been short and relatively simple, but I'm afraid the cakewalk is over. Uploading files takes a few steps.
First, you create a storage reference for the new file. It takes the form of a path, starting with the user's uid
(user ID). Next up is the string form of today's date. Last, the name of the file the user chose in the UI. Later, when you look at the Storage section of your Firebase console, you'll be able to navigate these "folders" you've created and view/delete uploaded files, and they'll be nicely organized by user and date.
Then you call your new reference's put()
method. This starts the file uploading to Firebase's servers, and it returns an UploadTask
instance, which you'll use soon.
The next part is optional, but just for fun, you subscribe to the upload's state_changed
event. In that handler, one of the things you can do is track how many bytes have been transferred. The handler may be called multiple times per upload. Once all the bytes have been uploaded, you cancel the event subscription with sub.cancel()
. If you were to remove this section of code, the sendImage()
method would be quite a bit simpler but still functional.
In the try/catch
block, you wait for the upload task's Future
to complete, and when it does, you can access the uploaded image's downloadURL
. Once you have that, you can use your good old sendMessage()
method from the last section to get your message into the database.
Image Input
Much like you had to do for text messages, you need to wire up a few things to make the user's wishes known to your database service.
This method needs to materialize in your AppComponent
class:
lib/app_component.dart
void sendImageMessage(FileList files) {
if (files.isNotEmpty) {
dbService.sendImage(files.first);
}
}
The sendImageMessage()
function will be called from your user interface. If you've ever used HTML's file input element, you know that it's a bit unsightly. Because of this, you're going to have it in your interface, but we've added some CSS to make it invisible so we can put our own lovely button there instead. The <input>
element in your app's view template looks like this:
<input #mediaCapture type="file" accept="image/*,capture=camera">
It has an ID of mediaCapture
. If you're new to AngularDart, that style of adding an ID may seem confusing. This is a very useful feature called a template reference variable. I suggest you take a moment to familiarize yourself with the docs on those.
Go ahead and add some Angular event handlers to both the file input element and the button below it:
lib/app_component.html
<input ... (change)="sendImageMessage(mediaCapture.files)">
<material-button ... (trigger)="mediaCapture.click()">...
</material-button>
Notice first that the <material-button>
, when activated, simulates a mouse click on the invisible <input>
element, and it does so using the template reference variable, mediaCapture
. Pretty useful trick, eh? The same reference is used in the call to sendImageMessage()
to send the file(s) selected by the user.
Test it! Image messages are now fully functional, or you need to work on your copy/paste skills.
Step 10: A Few Finishing Touches
If you've played with the chat app much to this point, you may have noticed a few rough edges. For one, the message input is in need of a bit of focus management. It would be more convenient if focus returned there automatically after clicking Send or the image upload button. Also, the message container needs to scroll down when new messages arrive.
We're not going to go into how they work, but I've provided a few Angular directives to help out with these issues. With a few keystrokes, you can solve these problems.
Import the Directives
At the top of your app component file, add a few more imports:
lib/app_component.dart
import 'src/directives/vu_scroll_down.dart';
import 'src/directives/vu_hold_focus.dart';
Declare the Directives
Whenever you use a custom directive or component in another component, you must declare it in the @Component
decorator's directives
list. There are already a few directives in the list, includingAppHeader
, so all you need to do is add the two new ones:
lib/app_component.dart
...
directives: const [... VuScrollDown, VuHoldFocus],
...
Use the Directives
These are attribute directives, so they need to be added to the elements they should affect as attributes:
lib/app_component.html
...
<div class="msg-container ..." ... vuScrollDown>
...
<material-input ... label="Message..." ... vuHoldFocus>
...
Hopefully you're not confused by all those dots, but we don't get a lot of horizontal space for code in these articles, so it can be necessary to abbreviate. Essentially, locate those two elements and slap the attribute directives on them. I encourage you to examine the directives' code on your own. Perhaps another post has been devoted to directive construction....
Test the Directives
When you run the app again, input focus shouldn't be as easily stolen, and the messages should scroll to keep new stuff in view.
Step 11: Build and Deploy Your App
Firebase comes with a hosting service that will serve your static assets and/or web app. You deploy your files to Firebase Hosting using the Firebase CLI.
Create a Build
In a perfect world, you could run your Dart code natively in a standard web browser. In this world, you need to compile your code to JavaScript first. Talk about a downgrade!
If you're using WebStorm and enjoying the fruits of hardcore Dart integration, open up your project's pubspec.yaml file and click the Build link in the upper-right corner of the editor window.
If you like to type, you can create a build on the command line (from your project's root):
pub build
Your build will appear in the build folder off your project's root. You can take the contents of build/web and manually deploy them to the server of your choice, or if you followed this tutorial's Firebase CLI setup steps, you can quickly expose your files to an unsuspecting Internet like so:
firebase deploy
As usual, run this on the command line from your project's root folder, where there should be a firebase.json file placed there by your earlier use of firebase init
.
If the deployment succeeds, the CLI will display a URL that you can visit to see the live version of your awesome chat app. It will be something like: https://<PROJECT_ID>.firebaseapp.com
.
Congrats!
If you got all the way through this monster of a tutorial, you are extraordinary. Seriously, we should talk, maybe hang out. As far as I'm concerned, determination is 80% of success.
If you skimmed your way through the whole thing without building the app, forget all that stuff I said about hanging out. Go ask your mom why she wasn't able to raise a winner. Return to the beginning and do it right. I'll be here when you're done.
Go forth and build more killer apps with AngularDart and Firebase. Yay!