Flutter + Dart = …

Why Flutter?

Good morning! Making positive impacts on end users has always been a developers’ goal. The more users developers can reach the larger the impacts developers can make. It is quite common that when I saw an awesome application that I want to have, it turns out that it is only available on iOS. However, building applications that are cross-platform is costly. Fortunately, Google has released Flutter 1.0 — a cross-platform framework written in Dart — officially on December 4th, 2018. Flutters’ target is not just Android or iOS, but wherever users exist including mobile, web, and desktop.

Overview

Today, we’re going to build a simple real-time messaging application for Android and iOS using Flutter and Firebase (Firebase Auth, Firebase Database, Firebase Storage). Here is the finished app:

Android
iOS

Let’s get started

Friendly Chat is a pretty popular Google’s sample application. To set up Flutter environment and build beautiful UI, you can start here. After finishing the tutorial, you should have an application with a similar UI to the finished one. Now, we are going to integrate Firebase into our app.

Integrate Firebase

  1. First of all, you need to add Firebase your Friendly Chat App on Firebase Console as guided here.
  2. After that, we can add all Firebase dependencies that Friendly Chat needs in pubspec.yaml file:
dependencies:
flutter:
sdk: flutter
firebase_core: ^0.4.0+6
firebase_database: ^3.0.3
firebase_auth: ^0.11.1+7
firebase_storage: ^3.0.2

Note: These dependencies are the latest Firebase dependencies at the time of this writing. Latest Firebase dependencies might require migrating to AndroidX.

Push messages to Firebase Database

DatabaseFirebase is a class acts as the main access point to the database while DatabaseReference is a class references a specific portion of the database. In this case, the DatabaseReference object references the messaging portion of the database.

...
import
'package:firebase_database/firebase_database.dart';
...
final FirebaseDatabase _database = FirebaseDatabase.instance;
final DatabaseReference _messageDatabaseReference = FirebaseDatabase.instance.reference().child("messages");

From the tutorial of Google CodeLabs, we already had _handleSubmitted callback when clicking the send button. Now, we only need to modify the function to push the message to the database every time we send a message. To upload a message to the database, we use the previous reference to push a JSON structure just like this:

void _handleSubmitted(String text) {
_textController.clear();
setState(() {
_isComposing = false;
});
final ChatMessage message = _createMessageFromText(text);
_messageDatabaseReference.push().set(message.toMap());
}
ChatMessage _createMessageFromText(String text) => ChatMessage(
text: text,
username: _name,
animationController: AnimationController(
duration: Duration(milliseconds: 180),
vsync: this,
),
);

Firebase helps use to write simple code that can interact with a scalable database. Moreover, Firebase provides you an easy-to-use UI to check your messages in the database. By now, you might have noticed that your database is not updated when you send new messages. This is all due to the default database rules. For more information on database rules, check this. However, for testing purpose only we will use this rule:

{
"rules": {
"messages": {
".read": "auth != null",
".write": "auth != null",
}
}
}

Only authenticated users can now read and write the messages node. Thus, the next step is to implement Authentication.

Implement Firebase Authentication

To avoid reinventing the wheel, you can follow David Cheah’s instructions to create a login page and authenticate users with Firebase and Flutter. If you don’t want to read the whole post, you can just simply have a look at David’s repo, which is simple and very easy to understand. Another option is having a look at my repo, which I have followed David’s post.

Save images to Firebase Storage

Why should we bother using Firebase Storage when we already had Firebase Database? The reason is performance. We probably don’t want to store images in the Firebase Real-time Database as it will prolong load time. Firebase Storage is designed for rich-content documents such as general files, images, and videos. What is the difference then? Firebase Storage can handle uploading and downloading files in a robust fashion. When users lose their network connection in the middle of the process, Firebase Storage can continue downloading or uploading where it was before.

Similar to the real-time database, we need an entry point:

...
import
'package:firebase_storage/firebase_storage.dart';
...
final FirebaseStorage _firebaseStorage = FirebaseStorage.instance;
final StorageReference _photoStorageReference = _firebaseStorage.ref().child("chat_photos");

To send images, there is a convenient Dart package designed by Googlers called image_picker. Thus, we should add the dependency to pubspec.yaml:

image_picker: ^0.6.0+9

Let’s discuss the flow of saving and displaying images. First of all, the users either choose from gallery or camera a photo. Luckily, with image_picker, we don’t have to manage that. The package would simply return a File after a user chooses an image (we’ll see how simple that is shortly). With that file, we will send directly to Firebase Storage, where we can access it through a URL. In addition, we also have to push the URL to the Firebase Database, so the Widget would know how to display the image through the URL (instead of the text of a message).

...
import 'package:image_picker/image_picker.dart';
...
Widget _buildTextComposer() {
return IconTheme(
data: IconThemeData(color: Theme.of(context).accentColor),
child: Container(
margin: const EdgeInsets.symmetric(horizontal: 8.0),
child: Row(
children: <Widget>[
Flexible(
child: TextField(
controller: _textController,
onChanged: (String text) {
setState(() {
_isComposing = text.length > 0;
});
},
onSubmitted: _handleSubmitted,
decoration: InputDecoration.collapsed(
hintText: "Send a message",
),
),
),
Container(
margin: const EdgeInsets.symmetric(horizontal: 4.0),
child: Row(
children: <Widget>[
IconButton(
icon: Icon(Icons.camera_alt),
onPressed: _sendImageFromCamera,
),
IconButton(
icon: Icon(Icons.image),
onPressed: _sendImageFromGallery,
),
Theme.of(context).platform == TargetPlatform.iOS
? CupertinoButton(
child: Text("Send"),
onPressed: _isComposing
? () => _handleSubmitted(
_textController.text,
)
: null,
)
: IconButton(
icon: Icon(Icons.send),
onPressed: _isComposing
? () => _handleSubmitted(
_textController.text,
)
: null,
),
],
))
],
),
));
}
void _onMessageAdded(Event event) {
final text = event.snapshot.value["text"];
final imageUrl = event.snapshot.value["imageUrl"];
ChatMessage message = imageUrl == null
? _createMessageFromText(text)
: _createMessageFromImage(imageUrl);
setState(() {
_messages.insert(0, message);
});
message.animationController.forward();
}
void _handleSubmitted(String text) {
_textController.clear();
setState(() {
_isComposing = false;
});
final ChatMessage message = _createMessageFromText(text);
_messageDatabaseReference.push().set(message.toMap());
}
void _sendImageFromCamera() async {
_sendImage(ImageSource.camera);
}
void _sendImageFromGallery() async {
_sendImage(ImageSource.gallery);
}
ChatMessage _createMessageFromText(String text) => ChatMessage(
text: text,
username: _name,
animationController: AnimationController(
duration: Duration(milliseconds: 180),
vsync: this,
),
);
ChatMessage _createMessageFromImage(String imageUrl) => ChatMessage(
imageUrl: imageUrl,
username: _name,
animationController: AnimationController(
duration: Duration(milliseconds: 90),
vsync: this,
),
);

Now, we only have to implement _sendImage to upload the image to Firebase Storage. In this simple application, I will name the file using uuid:

...import 'package:uuid/uuid.dart';
...
void _sendImage(ImageSource imageSource) async {
File image = await ImagePicker.pickImage(source: imageSource);
final String fileName = Uuid().v4();
StorageReference photoRef = _photoStorageReference.child(fileName);
final StorageUploadTask uploadTask = photoRef.putFile(image);
final StorageTaskSnapshot downloadUrl = await uploadTask.onComplete;
final ChatMessage message = _createMessageFromImage(
await downloadUrl.ref.getDownloadURL(),
);
_messageDatabaseReference.push().set(message.toMap());
}

Of course, similar to Firebase Database, Firebase Storage also has security rules. Initially, you just need to set read and write permissions true. For this simple messaging application, we only need to modify a little bit in order to only allow read and write permissions for authenticated users with files that are smaller than 5 MB.

service firebase.storage {
match /b/{bucket}/o {
match /chat_photos/{imageId} {
allow read: if request.auth != null;
allow write: if request.auth != null && request.resource.size < 5 * 1024 * 1024
}
}
}

You can get the complete source code right here:

Thank you for your time!

Reference:

To improve on coding productivity try changing your coding environment:

--

--