In this codelab we want to show how to implement Firebase into a Flutter app.
We will build a chat app where users can log in / sign in with Firebase, interact with Firestore, upload images to Firebase Storage, push notifications and analytics.
We will show how to setup up the app with Firebase and how to create a Firebase project.
You can jump down to the Firebase section by following this link.
Have a Google account that you will use for login to Firebase.
See the official prerequisites on the Firebase documentation
You can download the project from GitHub.
Be able to run Flutter on either a simulator or physical device.
You can follow the steps in the Flutter website: Getting Started
When you have Flutter setup on your computer then you are ready to create the Flutter project.
With this simple Flutter command you will create a sample / "skeleton" app and will be able to run right after creation.
flutter create gdg_flutter_firebase_chat
You can run the app from the terminal or run it in an IDE such as Android Studio, Intellij IDEA or VS Code (these editors has plugins for Flutter and dart support).
In the terminal navigate to the newly created project, gdg_flutter_firebase_chat
, and run the flutter run
command.
cd gdg_flutter_firebase_chat
flutter run
Now that we can run our Flutter code on an emulator or device, we will try to build something for ourself.
We will now remove all the code in the main.dart
file and fill in with this code below.
import 'package:flutter/material.dart';
void main() {
runApp(
MaterialApp(
title: "GDG Firebase chat",
home: Scaffold(
appBar: AppBar(
title: Text("GDG Firebase chat"),
),
),
),
);
}
Let us delete the test
folder as we will not be needing that in this codelab.
You can do it via the terminal, folder or IDE.
In the terminal you can use this command:
rm -r test/
Now we will create a new folder in the lib
folder. Let us name the folder helpers
, and the purpose of this folder is to have files with code that will be used throughout the project. Such an example could be constants like colours.
You can do it via the terminal, folder or IDE.
In the terminal you can use this command:
cd lib
mkdir helpers
While we are at it we will make a new dart file in that folder, app_constants.dart
.
Your structure should look like this.
In app_constants.dart
we will create a class called AppConstants
which will have static
fields that we can access throughout the app.
import 'package:flutter/material.dart';
class AppConstants {
static const String APP_PRIMARY_COLOR = "#EB342E";
static const String APP_BACKGROUND_COLOR = "#F6F8F9";
static const String APP_BACKGROUND_COLOR_WHITE = "#FFFFFF";
static const String APP_PRIMARY_COLOR_LIGHT = "#9f9f9f";
static const String APP_PRIMARY_COLOR_BLACK = "#000000";
static const String APP_PRIMARY_FONT_COLOR_WHITE = "#FFFFFF";
static const String APP_PRIMARY_COLOR_ACTION = "#BC2923";
static const String APP_PRIMARY_ROOM_COLOR = "#707070";
static const String APP_PRIMARY_COLOR_GREEN = "#009099";
static const String APP_BACKGROUND_COLOR_GRAY = "#D0D0D0";
static Color hexToColor(String code) {
return Color(int.parse(code.substring(1, 7), radix: 16) + 0xFF000000);
}
}
Now we will update the theme of the app with the colors we want. We have our colors in the AppConstants
class.
We update the theme
field in the MaterialApp(...)
in our main.dart
file.
Remember to import app_constants in the import section in the top of the file.
import 'package:flutter/material.dart';
import 'package:gdg_flutter_firebase_chat/helpers/app_constants.dart';
void main() {
runApp(
MaterialApp(
title: "GDG Firebase chat",
theme: ThemeData(
primaryColor: AppConstants.hexToColor(AppConstants.APP_PRIMARY_COLOR),
backgroundColor:
AppConstants.hexToColor(AppConstants.APP_BACKGROUND_COLOR),
primaryColorLight:
AppConstants.hexToColor(AppConstants.APP_PRIMARY_COLOR_LIGHT),
accentColor: Colors.black,
accentIconTheme: IconThemeData(color: Colors.black),
dividerColor: Colors.black12,
textTheme: TextTheme(
caption: TextStyle(color: Colors.white),
),
),
home: Scaffold(
appBar: AppBar(
title: Text("GDG Firebase chat"),
),
),
),
);
}
Let us now add assets to our project. We will first create a folder called assets
and then create a sub folder within the assets
folder, named images
.
The end result should look like this:
You can download the user_placeholder.jpg here and then put it in your images
folder.
In the pubspec.yaml
we need to specify where the assets are located.
flutter:
...
assets:
- assets/images/
Now we will add an app drawer to the app.
We will create a method _appDrawer()
that will return an AppDrawer
Widget.
_appDrawer() {
return Drawer(
child: Column(
children: <Widget>[
DrawerHeader(
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[
CircleAvatar(
radius: 30.0,
backgroundImage:
AssetImage('assets/images/user_placeholder.jpg'),
backgroundColor: Colors.transparent,
),
Text(
'Sumith Damodaran',
style: TextStyle(color: Colors.black),
),
Text(
'PM @ Sitecore',
style: TextStyle(color: Colors.black),
)
],
),
),
Spacer(),
ListTile(
leading: Icon(Icons.home),
title: Text('Home'),
onTap: () {}, // Handle tap of the app drawer item
),
Divider(),
ListTile(
leading: Icon(Icons.people),
title: Text('Attendants'),
onTap: () {}, // Handle tap of the app drawer item
),
Spacer(flex: 8),
],
),
);
}
We will add the _appDrawer()
to the drawer
field in our Scaffold
(...):
home: Scaffold(
drawer: _appDrawer(),
appBar: AppBar(
title: Text("GDG Firebase chat"),
),
),
Run the app and see that the changes should look like this:
Now we will create the chat screen. but first let us create another folder in the lib
folder called screens
. This folder will have files that specific to screens. In this case we will create a file called chat_screen.dart
.
The ChatScreen
class will be a StatefulWidget as we will be inputting text from the keyboard.
import 'package:flutter/material.dart';
class ChatScreen extends StatefulWidget {
@override
_ChatScreenState createState() => _ChatScreenState();
}
class _ChatScreenState extends State<ChatScreen> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("Chats")),
);
}
}
In main.dart
we will change the home
parameter in MaterialApp(...)
.
home: ChatScreen(),
Now run and check that you see the chat screen.
We want our design to look more or less like the design.
Design:
We know the screen will have a text field so we will initialize a TextEditingController
as a field in the class _ChatScreenState
.
bool _isComposing = false; // Being used later to determine when TextEditingController is used to compose a message.
final TextEditingController _textMessageController = TextEditingController();
Then we will create a method _buildMessageComposer()
where our input UI will be build.
_buildMessageComposer() {
return Container(
padding: EdgeInsets.symmetric(horizontal: 8.0),
height: 70.0,
color: Colors.white,
child: Row(
children: <Widget>[
RawMaterialButton(
onPressed: () {},
child: Icon(
Icons.camera_alt,
color: Colors.white,
size: 25.0,
),
shape: CircleBorder(),
elevation: 2.0,
fillColor: Theme.of(context).primaryColor,
padding: EdgeInsets.all(15.0),
),
Expanded(
child: TextField(
controller: _textMessageController,
textCapitalization: TextCapitalization.sentences,
onChanged: (value) {},
decoration: InputDecoration(
border: OutlineInputBorder(
borderRadius: BorderRadius.all(
Radius.circular(10.0),
),
),
hintText: 'Type your message...',
filled: true,
hintStyle: TextStyle(color: Colors.grey[400]),
),
),
),
IconButton(
icon: Icon(Icons.send),
iconSize: 25.0,
color: Theme.of(context).primaryColor,
onPressed: () {},
),
],
),
);
}
We will then have the Scaffoldbody
parameter to be the _buildMessageComposer()
.
body: _buildMessageComposer(),
Now run the app.
The screen should now look like this:
Before we start creating the UI for the messages, we should create some model classes, user
and message
.
Let's create a new folder in the lib
folder, models
. This will have all the model files we need.
Now create two files in the models
folder: user.dart
and message.dart
.
user.dart
:
class User {
final int id;
final String name, profileImageUrl, email, bio;
User({
this.id,
this.name,
this.profileImageUrl,
this.email,
this.bio,
});
}
message.dart
import 'package:gdg_flutter_firebase_chat/models/user.dart';
class Message {
final User sender;
final String
time; // Would usually be type DateTime or Firebase Timestamp in production apps
final String text;
final bool isLiked;
final bool unread;
Message({
this.sender,
this.time,
this.text,
this.isLiked,
this.unread,
});
}
// EXAMPLE MESSAGES IN CHAT SCREEN
List<Message> messages = [
Message(
sender: martin,
time: '5:30 PM',
text: 'Hey, how\'s it going? What did you do today?',
isLiked: true,
unread: true,
),
Message(
sender: currentUser,
time: '4:30 PM',
text: 'Just walked my doge. She was super duper cute. The best pupper!!',
isLiked: false,
unread: true,
),
Message(
sender: martin,
time: '3:45 PM',
text: 'How\'s the doggo?',
isLiked: false,
unread: true,
),
Message(
sender: martin,
time: '3:15 PM',
text: 'All the food',
isLiked: true,
unread: true,
),
Message(
sender: currentUser,
time: '2:30 PM',
text: 'Nice! What kind of food did you eat?',
isLiked: false,
unread: true,
),
Message(
sender: martin,
time: '2:00 PM',
text: 'I ate so much food today.',
isLiked: false,
unread: true,
),
];
In our chat_screen.dart
we create a new method _buildMessage()
for our messages.
isMe
will be used later so when know how the styling of a message should be. With this we know what messages have been sent from who.
_buildMessage(Message message, bool isMe) {
final Widget msg = Padding(
padding: EdgeInsets.all(8.0),
child: Container(
margin: isMe
? EdgeInsets.only(
top: 8.0,
bottom: 8.0,
left: 80.0,
)
: EdgeInsets.only(
top: 8.0,
bottom: 8.0,
),
padding: EdgeInsets.symmetric(horizontal: 25.0, vertical: 15.0),
width: MediaQuery.of(context).size.width * 0.75,
decoration: BoxDecoration(
color: isMe
? AppConstants.hexToColor(AppConstants.APP_PRIMARY_COLOR_ACTION)
: AppConstants.hexToColor(
AppConstants.APP_BACKGROUND_COLOR_WHITE),
borderRadius: isMe
? BorderRadius.only(
topLeft: Radius.circular(15.0),
topRight: Radius.circular(15.0),
bottomLeft: Radius.circular(15.0),
)
: BorderRadius.only(
topLeft: Radius.circular(15.0),
topRight: Radius.circular(15.0),
bottomRight: Radius.circular(15.0),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
message.text,
style: TextStyle(
color: isMe ? Colors.white60 : Colors.blueGrey,
fontSize: 12.0,
fontWeight: FontWeight.w600,
),
),
SizedBox(height: 8.0),
Row(
mainAxisAlignment:
isMe ? MainAxisAlignment.end : MainAxisAlignment.start,
children: <Widget>[
Text(
message.time,
style: TextStyle(
color: isMe ? Colors.white60 : Colors.grey,
fontSize: 12.0,
fontWeight: FontWeight.w600,
),
),
],
),
],
),
),
);
return Row(
children: <Widget>[msg],
);
}
Remember to import the necessary packages, files and classes.
import 'package:gdg_flutter_firebase_chat/helpers/app_constants.dart';
import 'package:gdg_flutter_firebase_chat/models/message.dart';
Let's create a list of messages as a field in the class _ChatScreenState
.
final List<Message> _messages = messages; // messages is the dummy data list in message.dart
Our build()
method should now also iterate over the list of messages.
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("Chats")),
body: ListView.builder(
reverse: true,
padding: EdgeInsets.only(top: 15.0),
itemCount: _messages.length,
itemBuilder: (BuildContext context, int index) {
final Message message = _messages[index];
final bool isMe = message.sender.id == currentUser.id;
return _buildMessage(message, isMe);
},
),
);
}
Run and see how it looks.
To make the list fit the screen with the text field where we compose messages, then we need to "expand" the view of the list.
@override
Widget build(BuildContext context) {
return Container(
color: Colors.white,
child: SafeArea(
top: false,
child: Scaffold(
appBar: AppBar(title: Text("Chats")),
body: GestureDetector(
onTap: () => FocusScope.of(context).unfocus(), // Will hide the keyboard when the user touches the messages list view.
child: Column(
children: <Widget>[
Expanded( // Expand widget to fill the available space
child: Container(
child: ListView.builder(
reverse: true,
padding: EdgeInsets.only(top: 15.0),
itemCount: _messages.length,
itemBuilder: (BuildContext context, int index) {
final Message message = _messages[index];
final bool isMe = message.sender.id == currentUser.id;
return _buildMessage(message, isMe);
},
),
),
),
_buildMessageComposer(),
],
),
),
),
),
);
}
We will create a method to handle when a message has been submitted. This method will be called: _handleSubmitted
and it will take in a String
as a parameter so it can create a new message and add it to our list _messages
.
void _handleSubmitted(String text) {
_textMessageController.clear();
setState(() {
_isComposing = false;
});
Message message = Message(
sender: currentUser,
time: '6:30 PM',
text: text,
isLiked: true,
unread: true,
);
setState(() {
_messages.insert(0, message);
});
}
Run the app and see that you can add new messages to the chat.
If you don't already have a Firebase account, then you can create one here. We will not be needing a paid plan for this codelab.
The next step for us to do is configure one or more apps to our Firebase project. We will do this by registering the app's bundle ID with Firebase and then generate config files to put in the our project.
You can find a detailed description on how to add Firebase to your Flutter app with Android configuration.
You can find a detailed description on how to add Firebase to your Flutter app with iOS configuration.
Follow along by cloning or downloading this repo.
You can use git
by getting the right branch FirebaseInitial
git clone https://github.com/sumithpdd/gdg_flutter_firebase_chat.git -b FirebaseInitial
Navigate to the newly cloned folder.
cd gdg_flutter_firebase_chat
Then run the flutter pub get
command to get all dependencies set up for the project.
flutter pub get
In the lib
folder create another folder services
. In here we will create a dart file called auth_service.dart
.
We will make a service class that handle auth, which in this case is login, logout, and signup.
class AuthService {
final FirebaseAuth _firebaseAuth = FirebaseAuth.instance;
Stream<FirebaseUser> get user => _firebaseAuth.onAuthStateChanged;
Future<void> signup(String name, String email, String password) async {
try {
AuthResult authResult = await _firebaseAuth.createUserWithEmailAndPassword(
email: email, password: password);
} on PlatformException catch (error) {
throw (error);
}
}
Future<void> login(String email, String password) async {
try {
await _firebaseAuth.signInWithEmailAndPassword(
email: email, password: password);
} on PlatformException catch (error) {
throw (error);
}
}
Future<void> logout() {
Future.wait([_firebaseAuth.signOut()]);
}
}
We would need to get a hold of the Firebase instance, so for now let us put some of our collections, storage referrences and instances in the constants.dart
file in the helpers
folder
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_storage/firebase_storage.dart';
final Firestore _db = Firestore.instance;
final usersRef = _db.collection('users');
final chatsRef = _db.collection('chats');
final FirebaseStorage _storage = FirebaseStorage.instance;
final storageRef =_storage.ref();
Then we need to update the signup
method so the file should look like this.
import 'package:firebase_auth/firebase_auth.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/services.dart';
import 'package:gdg_flutter_firebase_chat/helpers/constants.dart';
class AuthService {
final FirebaseAuth _firebaseAuth = FirebaseAuth.instance;
final FirebaseMessaging _firebaseMessaging = FirebaseMessaging();
Stream<FirebaseUser> get user => _firebaseAuth.onAuthStateChanged;
Future<void> signup(String name, String email, String password) async {
try {
AuthResult authResult = await _firebaseAuth.createUserWithEmailAndPassword(
email: email, password: password);
if (authResult.user != null) {
String token = await _firebaseMessaging.getToken();
usersRef.document(authResult.user.uid).setData({
'name': name,
'email': email,
'profileImageUrl': '',
'bio': '',
'token': token,
});
print('Signup complete');
}
} on PlatformException catch (error) {
throw (error);
}
}
Future<void> login(String email, String password) async {
try {
await _firebaseAuth.signInWithEmailAndPassword(
email: email, password: password);
print('login complete');
} on PlatformException catch (error) {
throw (error);
}
}
Future<void> logout() {
Future.wait([_firebaseAuth.signOut()]);
}
}
Now let us modify the User
class.
class User {
final String id;
final String name;
final String profileImageUrl, email, bio, token;
// {} named parameters
User(
{this.id,
this.name,
this.profileImageUrl,
this.email,
this.bio,
this.token});
factory User.fromDoc(DocumentSnapshot doc) {
return User(
id: doc.documentID,
name: doc['name'],
profileImageUrl: doc['profileImageUrl'],
email: doc['email'],
bio: doc['bio'],
token: doc['token']);
}
}
For our app we won't have it in production so we would either put the firestore rule set in "Test Mode" meaning read and write access for anybody in 30 days. Or just have it all enabled for all users.
// Allow read/write access to all users under any conditions
// Warning: **NEVER** use this rule set in production; it allows
// anyone to overwrite your entire database.
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow read, write: if true;
}
}
}
As we have changed the User
constructor parameters we need to fix this:
// YOU - current user
final User currentUser = User(
id: '0',
name: 'Current User',
profileImageUrl: 'assets/images/greg.jpg',
);
final User sumith = User(
id: '1',
name: 'sumith',
profileImageUrl: 'assets/images/greg.jpg',
);
final User martin = User(
id: '2',
name: 'martin',
profileImageUrl: 'assets/images/james.jpg',
);
final User laura = User(
id: '3',
name: 'laura',
profileImageUrl: 'assets/images/john.jpg',
);
final User bilal = User(
id: '4',
name: 'bilal',
profileImageUrl: 'assets/images/olivia.jpg',
);
final User sam = User(
id: '5',
name: 'Sam',
profileImageUrl: 'assets/images/sam.jpg',
);
final User sophia = User(
id: '6',
name: 'Sophia',
profileImageUrl: 'assets/images/sophia.jpg',
);
final User steven = User(
id: '7',
name: 'Steven',
profileImageUrl: 'assets/images/steven.jpg',
);
To check if authentication is working, We will create a new screen for Login and Signup.
Create a login_screen.dart
in the screens
folder.
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:gdg_flutter_firebase_chat/helpers/app_constants.dart';
import 'package:gdg_flutter_firebase_chat/services/auth_service.dart';
class LoginScreen extends StatefulWidget {
@override
_LoginScreenState createState() => _LoginScreenState();
}
class _LoginScreenState extends State<LoginScreen> {
final _loginFormKey = GlobalKey<FormState>();
final _signupFormKey = GlobalKey<FormState>();
String _name, _email, _password;
int _selectedIndex = 0;
_buildLoginForm() {
return Form(
key: _loginFormKey,
child: Column(children: <Widget>[
_buildEmailTF(),
_buildPasswordTF(),
]),
);
}
_buildSignupForm() {
return Form(
key: _signupFormKey,
child: Column(children: <Widget>[
_buildNameTF(),
_buildEmailTF(),
_buildPasswordTF(),
]),
);
}
_buildNameTF() {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 30.0, vertical: 10.0),
child: TextFormField(
decoration: const InputDecoration(labelText: 'Name'),
validator: (input) =>
input.trim().isEmpty ? 'Please enter a vaild name' : null,
onSaved: (input) => _name = input.trim(),
),
);
}
_buildEmailTF() {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 30.0, vertical: 10.0),
child: TextFormField(
decoration: const InputDecoration(labelText: 'Email'),
validator: (input) =>
!input.contains('@') ? 'Please enter a vaild email' : null,
onSaved: (input) => _email = input,
),
);
}
_buildPasswordTF() {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 30.0, vertical: 10.0),
child: TextFormField(
decoration: const InputDecoration(labelText: 'Password'),
validator: (input) =>
input.length < 6 ? 'Password must be atleast 6 characters' : null,
onSaved: (input) => _password = input,
obscureText: true,
),
);
}
_submit() async {
try {
if (_selectedIndex == 0 && _loginFormKey.currentState.validate()) {
_loginFormKey.currentState.save();
await authservice.login(_email, _password);
} else if (_selectedIndex == 1 &&
_signupFormKey.currentState.validate()) {
_signupFormKey.currentState.save();
await authservice.signup(_name, _email, _password);
}
} on PlatformException catch (error) {
_showErrorDialog(error.message);
}
}
_showErrorDialog(String errorMessage) {
showDialog(
context: context,
builder: (_) {
return AlertDialog(
title: Text('Error'),
content: Text(errorMessage),
actions: <Widget>[
FlatButton(
onPressed: () => Navigator.pop(context), child: Text('OK'))
],
);
},
);
}
final AuthService authservice = AuthService();
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text('Welcome!',
style: TextStyle(fontSize: 30.0, fontWeight: FontWeight.w600)),
const SizedBox(height: 10.0),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
Container(
width: 150.0,
child: FlatButton(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10.0),
),
color: _selectedIndex == 0 ? AppConstants.hexToColor(AppConstants.APP_PRIMARY_COLOR) : Colors.grey[300],
child: Text(
'Login',
style: TextStyle(
fontSize: 20.0,
color:
_selectedIndex == 0 ? Colors.white : AppConstants.hexToColor(AppConstants.APP_PRIMARY_COLOR)),
),
onPressed: () => setState(() => _selectedIndex = 0),
),
),
Container(
width: 150.0,
child: FlatButton(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10.0),
),
color: _selectedIndex == 1 ? AppConstants.hexToColor(AppConstants.APP_PRIMARY_COLOR) : Colors.grey[300],
child: Text(
'Sign Up',
style: TextStyle(
fontSize: 20.0,
color:
_selectedIndex == 1 ? Colors.white : AppConstants.hexToColor(AppConstants.APP_PRIMARY_COLOR)),
),
onPressed: () => setState(() => _selectedIndex = 1),
),
)
],
),
_selectedIndex == 0 ? _buildLoginForm() : _buildSignupForm(),
const SizedBox(height: 20.0),
Container(
width: 180,
child: FlatButton(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10.0)),
color: AppConstants.hexToColor(AppConstants.APP_PRIMARY_COLOR),
onPressed: _submit,
child: Text(
'Submit',
style: TextStyle(
color: Colors.white,
fontSize: 20.0,
),
),
))
],
),
),
);
}
}
After login or signup we need to redirect to the screen with attendees.
So let us create a new file in the screens
folder called attendees_screen.dart
import 'package:flutter/material.dart';
import 'package:gdg_flutter_firebase_chat/models/user.dart';
import 'package:gdg_flutter_firebase_chat/models/user_data.dart';
import 'package:gdg_flutter_firebase_chat/services/auth_service.dart';
import 'package:gdg_flutter_firebase_chat/services/database_service.dart';
import 'package:gdg_flutter_firebase_chat/widgets/all_attendees_widget.dart';
import 'package:provider/provider.dart';
class AttendeesScreen extends StatefulWidget {
static final String id ='attendees_screen';
@override
_AttendeesScreenState createState() => _AttendeesScreenState();
}
class _AttendeesScreenState extends State<AttendeesScreen> {
List<User> _users = [];
@override
void initState() {
super.initState();
_setupAttendees();
}
_setupAttendees() async {
String currentUserId =Provider.of<UserData>(context, listen: false).currentUserId;
List<User> users = await Provider.of<DataBaseService>(context, listen: false)
.getAllUsers(currentUserId);
if (mounted) {
setState(() {
_users = users;
});
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Theme.of(context).primaryColor,
appBar: AppBar(
leading: IconButton(
icon: Icon(Icons.menu),
iconSize: 30.0,
color: Colors.white,
onPressed: () {},
),
title: Text(
'Attendees',
style: TextStyle(
fontSize: 28.0,
fontWeight: FontWeight.bold,
),
),
elevation: 0.0,
actions: <Widget>[
IconButton(
icon: Icon(Icons.exit_to_app),
onPressed: Provider.of<AuthService>(context, listen: false).logout,
),
],
),
body: Column(
children: <Widget>[
Expanded(
child: Container(
decoration: BoxDecoration(
color: Theme.of(context).accentColor,
borderRadius: BorderRadius.only(
topLeft: Radius.circular(30.0),
topRight: Radius.circular(30.0),
),
),
child: Column(
children: <Widget>[
AllAttendees(users:_users),
],
),
),
),
],
),
);
}
}
To store current user data we will create a model file user_data.dart
under models
import 'package:flutter/material.dart';
class UserData extends ChangeNotifier {
String currentUserId;
}
We use this class to pass data between screens, for that we will use the provider package. A mixture between dependency injection (DI) and state management, built with widgets for widgets.
Add this to your package's pubspec.yaml
file:
dependencies:
provider: ^4.0.5
Remember to get the dependencies by running this command
flutter pub get
The attendees are all the users that have registered. We update our database service and will create a new function to get all users.
Firstly create a new file for the services
folder called database_service.dart
.
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:gdg_flutter_firebase_chat/helpers/constants.dart';
import 'package:gdg_flutter_firebase_chat/models/user.dart';
class DataBaseService {
Future<User> getUser(String userId) async {
DocumentSnapshot userDoc = await usersRef.document(userId).get();
return User.fromDoc(userDoc);
}
Future<List<User>> getAllUsers(String currentUserId) async {
QuerySnapshot userSnapshot = await usersRef.getDocuments();
List<User> users = [];
userSnapshot.documents.forEach((doc) {
User user = User.fromDoc(doc);
if (user.id != currentUserId) users.add(user);
});
return users;
}
}
Create a widgets
folder in the lib
folder, afterwards we will create a file in the newly created folder. Name the file: all_attendees_widget.dart
import 'package:flutter/material.dart';
import 'package:gdg_flutter_firebase_chat/models/user.dart';
import 'package:gdg_flutter_firebase_chat/screens/chat_screen.dart';
import 'package:cached_network_image/cached_network_image.dart';
class AllAttendees extends StatelessWidget {
final List<User> users;
const AllAttendees({this.users});
@override
Widget build(BuildContext context) {
return Expanded(
child: Container(
decoration: BoxDecoration(
color: Colors.white,
),
child: Padding(
padding: const EdgeInsets.all(8.0),
child: ListView.builder(
itemCount: users.length,
itemBuilder: (BuildContext context, int index) {
final User user = users[index];
return GestureDetector(
onTap: () => Navigator.push(
context,
MaterialPageRoute(
builder: (_) => ChatScreen(
// user: chat.sender,
),
),
),
child: Container(
margin: EdgeInsets.only(top: 5.0, bottom: 5.0, right: 5.0),
padding:
EdgeInsets.symmetric(horizontal: 10.0, vertical: 10.0),
decoration: BoxDecoration(
color: Color(0xFFFFEFEE),
borderRadius: BorderRadius.all( Radius.circular(20.0)
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Row(
children: <Widget>[
CircleAvatar(
radius: 35.0,
backgroundImage: user.profileImageUrl.isEmpty
? AssetImage('assets/images/user_placeholder.jpg')
: CachedNetworkImageProvider(user.profileImageUrl),
),
SizedBox(width: 10.0),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
user.name,
style: TextStyle(
color: Colors.grey,
fontSize: 15.0,
fontWeight: FontWeight.bold,
),
),
SizedBox(height: 5.0),
Text(
user.bio,
style: TextStyle(
color: Colors.grey,
fontSize: 15.0,
),
),
],
),
],
),
],
),
),
);
},
),
),
),
);
}
}
Profile images will come from the network, We will use the Flutter library to load and cache network images. We will be using cached_network_image package.
Add this to your package's pubspec.yaml
file:
dependencies:
cached_network_image: ^2.1.0+1
Remember to get the dependencies by running this command
flutter pub get
We got all our references now we need to navigate from main to attendees screen if we are logged in. Quickly update the login screen and give it an id. This will be used for routes when navigating between screens.
class LoginScreen extends StatefulWidget {
static final String id ='login_screen';
...
We will refactor main.dart, and check if the user is logged in, we will create a new class that extends StatelessWidget.
class MyApp extends StatelessWidget {
Widget _getScreenId() {
return StreamBuilder<FirebaseUser>(
stream: FirebaseAuth.instance.onAuthStateChanged,
builder: (BuildContext context, snapshot) {
if (snapshot.hasData) {
Provider.of<UserData>(context).currentUserId = snapshot.data.uid;
return AttendeesScreen();
} else {
return LoginScreen();
}
},
);
}
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: "GDG Firebase chat",
debugShowCheckedModeBanner: false,
theme: ThemeData(
primaryColor: AppConstants.hexToColor(AppConstants.APP_PRIMARY_COLOR),
backgroundColor:
AppConstants.hexToColor(AppConstants.APP_BACKGROUND_COLOR),
primaryColorLight:
AppConstants.hexToColor(AppConstants.APP_PRIMARY_COLOR_LIGHT),
accentColor: Colors.black,
accentIconTheme: IconThemeData(color: Colors.black),
dividerColor: Colors.black12,
textTheme: TextTheme(
caption: TextStyle(color: Colors.white),
),
),
home: _getScreenId(),
routes: {
LoginScreen.id: (context) => LoginScreen(),
AttendeesScreen.id: (context) => AttendeesScreen(),
},
);
}
}
Then let us modify runApp()
to call MyApp()
, we will also define all the providers for the app.
runApp(MultiProvider(providers: [
ChangeNotifierProvider(
create: (_) => UserData(),
),
Provider<AuthService>(
create: (_) => AuthService(),
),
Provider<DataBaseService>(
create: (_) => DataBaseService(),
),
], child: MyApp()));
}
Here we will look at Firebase Storage, we have images for the attendees profileImages we can store in firebase storage. For that we will create a "edit profile" screen.
Let us refactor some code by moving the AppDrawer into its own widget so it can be reused throughout the project.
Create a new file in the widgets
folder called app_drawer_widget.dart
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:gdg_flutter_firebase_chat/helpers/app_constants.dart';
import 'package:gdg_flutter_firebase_chat/models/user.dart';
import 'package:gdg_flutter_firebase_chat/models/user_data.dart';
import 'package:gdg_flutter_firebase_chat/screens/edit_profile_screen.dart';
import 'package:gdg_flutter_firebase_chat/services/database_service.dart';
import 'package:provider/provider.dart';
class AppDrawer extends StatefulWidget {
@override
_AppDrawerState createState() => _AppDrawerState();
}
class _AppDrawerState extends State<AppDrawer> {
_buildActivity(BuildContext context, String userId) {
return FutureBuilder(
future: Provider.of<DataBaseService>(context, listen: false)
.getUser(userId),
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (!snapshot.hasData) {
return SizedBox.shrink();
}
User user = snapshot.data;
return DrawerHeader(
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[
CircleAvatar(
radius: 20.0,
backgroundImage: user.profileImageUrl.isEmpty
? AssetImage('assets/images/user_placeholder.jpg')
: CachedNetworkImageProvider(user.profileImageUrl),
backgroundColor: Colors.transparent,
),
Text(
user.name,
style: TextStyle(color: Colors.black),
),
Text(
user.bio,
style: TextStyle(color: Colors.black),
),
IconButton(
icon: Icon(Icons.edit),
tooltip: 'Edit Profile',
onPressed: () => Navigator.push(
context,
MaterialPageRoute(
builder: (_) => EditProfileScreen(
user: user,
),
),
),
color:
AppConstants.hexToColor(AppConstants.APP_PRIMARY_COLOR),
),
],
),
);
});
}
@override
Widget build(BuildContext context) {
String currentUserId = Provider.of<UserData>(context).currentUserId;
return Drawer(
child: Column(
children: <Widget>[
currentUserId.isNotEmpty
? _buildActivity(context, currentUserId)
: SizedBox.shrink(),
Spacer(),
ListTile(
leading: Icon(Icons.home),
title: Text('Home'),
onTap: () {},
),
Divider(),
ListTile(
leading: Icon(Icons.people),
title: Text('Attendants'),
onTap: () {},
),
Spacer(flex: 8),
],
),
);
}
}
Update attendees_screen.dart
to use the newly made widget.
drawer: AppDrawer(),
While we are at it let us delete this.
leading: IconButton(
icon: Icon(Icons.menu),
iconSize: 30.0,
color: Colors.white,
onPressed: () {},
),
Create a new screen edit_profile_screen.dart
in the screens
folder.
import 'dart:io';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:gdg_flutter_firebase_chat/helpers/app_constants.dart';
import 'package:gdg_flutter_firebase_chat/models/user.dart';
import 'package:gdg_flutter_firebase_chat/services/storage_service.dart';
import 'package:gdg_flutter_firebase_chat/services/database_service.dart';
import 'package:image_picker/image_picker.dart';
class EditProfileScreen extends StatefulWidget {
static final String id = 'edit_profile_screen';
final User user;
EditProfileScreen({this.user});
@override
_EditProfileScreenState createState() => _EditProfileScreenState();
}
class _EditProfileScreenState extends State<EditProfileScreen> {
final _formKey = GlobalKey<FormState>();
File _profileImage;
String _name = '';
String _bio = '';
bool _isLoading = false;
@override
void initState() {
super.initState();
_name = widget.user.name;
_bio = widget.user.bio;
}
_handleImageFromGallery() async {
File imageFile = await ImagePicker.pickImage(source: ImageSource.gallery);
if (imageFile != null) {
setState(() {
_profileImage = imageFile;
});
}
}
_displayProfileImage() {
// No new profile image
if (_profileImage == null) {
// No existing profile image
if (widget.user.profileImageUrl.isEmpty) {
// Display placeholder
return AssetImage('assets/images/user_placeholder.jpg');
} else {
// User profile image exists
return CachedNetworkImageProvider(widget.user.profileImageUrl);
}
} else {
// New profile image
return FileImage(_profileImage);
}
}
_submit() async {
if (_formKey.currentState.validate() && !_isLoading) {
_formKey.currentState.save();
setState(() {
_isLoading = true;
});
// Update user in database
String _profileImageUrl = '';
if (_profileImage == null) {
_profileImageUrl = widget.user.profileImageUrl;
} else {
_profileImageUrl = await StorageService.uploadUserProfileImage(
widget.user.profileImageUrl,
_profileImage,
);
}
User user = User(
id: widget.user.id,
name: _name,
profileImageUrl: _profileImageUrl,
bio: _bio,
);
// Database update
DataBaseService.updateUser(user);
Navigator.pop(context);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
appBar: AppBar(
backgroundColor: Colors.white,
title: Text(
'Edit Profile',
style: TextStyle(color: Colors.black),
),
),
body: GestureDetector(
onTap: () => FocusScope.of(context).unfocus(),
child: ListView(
children: <Widget>[
_isLoading
? LinearProgressIndicator(
backgroundColor: AppConstants.hexToColor(AppConstants.APP_PRIMARY_COLOR),
valueColor: AlwaysStoppedAnimation(AppConstants.hexToColor(AppConstants.APP_PRIMARY_COLOR)),
)
: SizedBox.shrink(),
Padding(
padding: const EdgeInsets.all(30.0),
child: Form(
key: _formKey,
child: Column(
children: <Widget>[
CircleAvatar(
radius: 60.0,
backgroundColor: Colors.grey,
backgroundImage: _displayProfileImage(),
),
FlatButton(
onPressed: _handleImageFromGallery,
child: Text(
'Change Profile Image',
style: TextStyle(
color: Theme.of(context).accentColor,
fontSize: 16.0),
),
),
TextFormField(
initialValue: _name,
style: TextStyle(fontSize: 18.0),
decoration: InputDecoration(
icon: Icon(
Icons.person,
size: 30.0,
),
labelText: 'Name',
),
validator: (input) => input.trim().length < 1
? 'Please enter a valid name'
: null,
onSaved: (input) => _name = input,
),
TextFormField(
initialValue: _bio,
style: TextStyle(fontSize: 18.0),
decoration: InputDecoration(
icon: Icon(
Icons.book,
size: 30.0,
),
labelText: 'Bio',
),
validator: (input) => input.trim().length > 150
? 'Please enter a bio less than 150 characters'
: null,
onSaved: (input) => _bio = input,
),
Container(
margin: EdgeInsets.all(40.0),
height: 40.0,
width: 250.0,
child: FlatButton(
onPressed: _submit,
color: AppConstants.hexToColor(AppConstants.APP_PRIMARY_COLOR),
textColor: Colors.white,
child: Text(
'Save Profile',
style: TextStyle(fontSize: 18.0),
),
),
),
],
),
),
),
],
),
),
);
}
}
We will need to update the database_service.dart
file and add a new function.
static void updateUser(User user) {
usersRef.document(user.id).updateData({
'name': user.name,
'profileImageUrl': user.profileImageUrl,
'bio': user.bio,
});
}
For storing a profile image we will need a new service, storage_service.dart
in the service
folder. Here we will create functions to handle the upload of user profile image.
import 'dart:io';
import 'package:firebase_storage/firebase_storage.dart';
import 'package:flutter_image_compress/flutter_image_compress.dart';
import 'package:gdg_flutter_firebase_chat/helpers/constants.dart';
import 'package:path_provider/path_provider.dart';
import 'package:uuid/uuid.dart';
class StorageService {
static Future<String> uploadUserProfileImage(
String url, File imageFile) async {
String photoId = Uuid().v4();
File image = await compressImage(photoId, imageFile);
if (url.isNotEmpty) {
// Updating user profile image
RegExp exp = RegExp(r'userProfile_(.*).jpg');
photoId = exp.firstMatch(url)[1];
}
StorageUploadTask uploadTask = storageRef
.child('images/users/userProfile_$photoId.jpg')
.putFile(image);
StorageTaskSnapshot storageSnap = await uploadTask.onComplete;
String downloadUrl = await storageSnap.ref.getDownloadURL();
return downloadUrl;
}
static Future<File> compressImage(String photoId, File image) async {
final tempDir = await getTemporaryDirectory();
final path = tempDir.path;
File compressedImageFile = await FlutterImageCompress.compressAndGetFile(
image.absolute.path,
'$path/img_$photoId.jpg',
quality: 70,
);
return compressedImageFile;
}
}
We used few new packages in the new code lets add them to pubspec.yaml
and run flutter pub get
dependencies:
flutter_image_compress: ^0.6.5+1
path_provider: ^1.6.5
uuid: ^2.0.4
image_picker: ^0.6.3+4
You can install packages from the command line:
flutter pub get
Now let us finish the chat application and save the chat messages to the database (Firestore)
We will update the DatabaseService
Future<List<Message>> getChatMessages(String senderId, String receiverId) async {
List<Message> messages = [];
QuerySnapshot messagesSenderQuerySnapshot = await chatsRef
.where('senderId', isEqualTo: senderId)
.where('toUserId', isEqualTo: receiverId)
.orderBy('timestamp', descending: true)
.getDocuments();
messagesSenderQuerySnapshot.documents.forEach((doc) {
print(doc.documentID);
messages.add(Message.fromDoc(doc));
});
QuerySnapshot messagestoQuerySnapshot = await chatsRef
.where('senderId', isEqualTo: receiverId)
.where('toUserId', isEqualTo: senderId)
.orderBy('timestamp', descending: true)
.getDocuments();
messagestoQuerySnapshot.documents.forEach((doc) {
print(doc.documentID);
messages.add(Message.fromDoc(doc));
});
Comparator<Message> timestampComparator =
(a, b) => b.timestamp.compareTo(a.timestamp);
messages.sort(timestampComparator);
return messages;
}
void sendChatMessage(Message message) {
chatsRef.add({
'senderId': message.senderId,
'toUserId': message.toUserId,
'text': message.text,
'imageUrl': message.imageUrl,
'isLiked': message.isLiked,
'unread': message.unread,
'timestamp': Timestamp.fromDate(DateTime.now()),
});
}
We need to update our Message
model, and introduce "from sender" and "to sender" and new fields, also it will have a function that maps database values into objects.
class Message {
final String id, senderId, toUserId, text, imageUrl;
final bool isLiked;
final bool unread;
final Timestamp timestamp;
Message({
this.id,
this.senderId,
this.toUserId,
this.text,
this.imageUrl,
this.isLiked,
this.unread,
this.timestamp,
});
factory Message.fromDoc(DocumentSnapshot doc) {
return Message(
id: doc.documentID,
senderId: doc['senderId'],
toUserId: doc['toUserId'],
text: doc['text'],
imageUrl: doc['imageUrl'],
isLiked: doc['isLiked'],
unread: doc['unread'],
timestamp: doc['timestamp']);
}
}
We will add dateFormat
to our constants.dart
file
final DateFormat timeFormat = DateFormat('E, h:mm a');
We will use intl
package to use DateFormat
.
Add this to your package's pubspec.yaml
file:
dependencies:
intl: ^0.16.1
Remember to get the dependencies by running this command
flutter pub get
That chat_screen.dart
will need to take 2 parameters, from which user message is coming from and to sender.
final String currentUserId;
final String toUserId;
ChatScreen({this.currentUserId, this.toUserId});
We need to bring messages from our database.
Therefore, delete this as it won't be needed.
final List<Message> _messages = messages;
And instead add this.
DataBaseService _dataBaseService;
List<Message> _messages = [];
@override
void initState() {
super.initState();
_dataBaseService = Provider.of<DataBaseService>(context, listen: false);
_setupMessages();
}
_setupMessages() async {
List<Message> messages = await _dataBaseService.getChatMessages(
widget.currentUserId, widget.toUserId);
setState(() {
_messages = messages;
});
}
Update message time with this.
...
Text('${timeFormat.format(message.timestamp.toDate())}',
...)
In _handleSubmitted(...)
update code to pass the new values and call the database service.
void _handleSubmitted(String text) {
_textMessageController.clear();
setState(() {
_isComposing = false;
});
Message message = Message(
senderId: widget.currentUserId,
toUserId: widget.toUserId,
timestamp: Timestamp.fromDate(DateTime.now()),
text: text,
isLiked: true,
unread: true,
);
setState(() {
_messages.insert(0, message);
});
_dataBaseService.sendChatMessage(message);
}
And finally update the isMe
code to get value from the widget.
final bool isMe = message.senderId == widget.currentUserId;
all_attendees_widget
Chat screen is called through the attendees screen. Therefore, we need to update the all_attendees_widget.dart
.
In the build
method we need to get the current user id.
Widget build(BuildContext context) {
String currentUserId = Provider.of<UserData>(context, listen: false).currentUserId;
...
We then need to pass it to ChatScreen
when we do the navigation.
return GestureDetector(
onTap: () => Navigator.push(
context,
MaterialPageRoute(
builder: (_) => ChatScreen(
currentUserId: currentUserId,
toUserId: user.id,
),
),
),
...
We would like to send pictures in the chat.
So in our storage_service.dart
we will add a new function uploadMessageImage
.
Future<String> uploadMessageImage(File imageFile) async {
String imageId = Uuid().v4();
File image = await compressImage(imageId, imageFile);
String downloadUrl = await _uploadImage(
'images/messages/message_$imageId.jpg',
imageId,
image,
);
return downloadUrl;
}
In chat_screen.dart
we will then update _buildMessage()
.
...
children: <Widget>[
message.imageUrl == null
? _buildText(isMe, message)
: _buildImage(context, message),
SizedBox(height: 8.0),
...
We will also add new methods before the build.
_buildText(bool isMe,Message message) {
return Text(
message.text,
style: TextStyle(
color: isMe ? Colors.white60 : Colors.blueGrey,
fontSize: 12.0,
fontWeight: FontWeight.w600,
),
);
}
_buildImage(BuildContext context,Message message) {
final size = MediaQuery.of(context).size;
return Container(
height: size.height * 0.2,
width: size.width * 0.6,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20.0),
image: DecorationImage(
fit: BoxFit.cover,
image: CachedNetworkImageProvider(message.imageUrl),
)),
);
}
Add to onPressed
of our camera icon.
...
children: <Widget>[
RawMaterialButton(
onPressed: () async {
File imageFile = await ImagePicker.pickImage(
source: ImageSource.gallery,
);
if (imageFile != null) {
String imageUrl =
await Provider.of<StorageService>(context, listen: false)
.uploadMessageImage(imageFile);
_handleSubmitted(null, imageUrl);
}
},
...
Update _handleSubmitted
to take an additional parameter.
void _handleSubmitted(String text, String imageUrl) {
if ((text != null && text.trim().isNotEmpty) || imageUrl != null) {
if (imageUrl == null) {
//text message
setState(() {
_isComposing = false;
});
}
Message message = Message(
senderId: widget.currentUserId,
toUserId: widget.toUserId,
imageUrl: imageUrl,
timestamp: Timestamp.fromDate(DateTime.now()),
text: text,
isLiked: true,
unread: true,
);
setState(() {
_messages.insert(0, message);
});
_dataBaseService.sendChatMessage(message);
}
}
And update its usages
...
onPressed: _isComposing
? () => _handleSubmitted(
_textMessageController.text,
null,
)
: null,
...
Finally we should add a Provider
for the StorageService
class in main.dart
...
Provider<StorageService>(
create: (_) => StorageService(),
),
...
On the chat_screen.dart
lets add whom we modify the app bar to include profile image and name.
...
appBar: AppBar(
title: Row(
children: <Widget>[
CircleAvatar(
radius: 25.0,
backgroundImage: widget.toUser.profileImageUrl.isEmpty
? // Display placeholder
AssetImage('assets/images/user_placeholder.jpg')
: CachedNetworkImageProvider(
widget.toUser.profileImageUrl)),
SizedBox(width: 10.0),
Text(
widget.toUser.name,
style: TextStyle(
fontSize: 18.0,
fontWeight: FontWeight.bold,
),
),
],
),
elevation: 10.0,
),
...
TODO: Have links and other stuff