Lab 4: Chatter
Due Date: September 27
Objectives
- Teach students how to save data in their app
- Learn to separate models from data access
- Create a custom widget
- Interact with external service providers

Due Date: September 27
Objectives
In this lab, we are going to build a simple chat application that will save chat texts and display such texts to the screen automatically in a custom text widget. Here is a screenshot of our app to give you a little idea of where we are going:
At this point, we should be familiar with creating a Flutter app from scratch using VS Code. Do so, cleaning out the main.dart file so that it is similar to the one we had in the last lab, but with the following notes:
clear all the starter code except for main() and the stateless widget MyApp
remove the debug banner
change the color to a darker shade of blue with primaryColor: Color(0xFF0D47A1)
set the title to strings.appTitle; this will lead to an error so create a strings.dart file like we did at the start of the previous lab and then import the file at the top of the page
set home to home: MessageList(); this will lead to an error, so create a folder called views, within that create message_list.dart and add the following for a barebones class and then import this file to main.dart:
class MessageList extends StatefulWidget {
MessageList({Key? key}) : super(key: key);
// necessary to have createState() for a stateful widget
@override
MessageListState createState() => MessageListState();
}
class MessageListState extends State<MessageList> {
// need to have a build(), so will add the AppBar now...
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text(strings.appTitle),
),
);
}
}
Note that there are two missing import statements here; you know this by now, so add them in.
Create a directory in lib called models and within it add the file message.dart. Here we want to create a simple class that knows the structure of a message (simply, some text and a datetime). The following should do nicely to start:
class Message {
final String text;
final DateTime date;
Message(this.text, this.date);
}
This Message model should also know how to create itself from JSON and how to convert itself into JSON. This model should not know how to save itself from a database yet, just what it is and how it moves between a message object and a JSON representation. We can add this to our model with the following methods:
Message.fromJson(Map<dynamic, dynamic> json)
: date = DateTime.parse(json['date'] as String),
text = json['text'] as String;
Map<dynamic, dynamic> toJson() => <dynamic, dynamic>{
'date': date.toString(),
'text': text,
};
The first method creates an object from JSON by invoking the constructor and parsing the data to make the message object. The second provides a JSON map from the current text and date attributes. Again, this model knows how to convert itself to and from different formats, but does not know anything about data persistence. We need another object to help us with that.
Data persistence is different from the transformations we were doing earlier, so the first thing we need to do is figure out a platform to save app data. As discussed in class, there are a lot of benefits to using Google's Firebase for this purpose and we will not rehash all those here. What we will do now is create a Data Access Object class that will help handle access to our message data. In the lib directory, add a new folder called data and within that add a file called message_dao.dart. Within that file, add the following starter code:
class MessageDao {
final DatabaseReference _messagesRef =
FirebaseDatabase.instance.reference().child('messages');
}
All we are doing right now is creating a DatabaseReference -- a particular location in your Firebase Database and can be used for reading or writing data to that location -- using the Firebase library for Fluttr. Oh wait, we have red showing up, probably because we haven't set up that library as a dependency.
Go to pubspec.yaml and add the following dependencies:
dependencies:
flutter:
sdk: flutter
firebase_core: 1.3.0
firebase_database: 7.1.1
intl: 0.17.0
cupertino_icons: ^1.0.2
Some of these we will use later, but let's add them all in down and save the file and then run flutter pub get to actually get them installed. (VS Code will install them for you after saving, so you can skip that latter command if you use that IDE.) Now to get rid of that error, go back to message_dao.dart and import the library with import 'package:firebase_database/firebase_database.dart';
Now we need to add to this class a method that will use this DatabaseReference to save our data to our Firebase collection. The following will do:
void saveMessage(Message message) {
_messagesRef.push().set(message.toJson());
}
Of course, right now you are getting a red line because you haven't imported the Message model yet, so do that right now. All this method is doing is using the built-in method push() to set up a child location to save the data to and then setting that location's value to the value of the message as JSON. We will need more from our data access object, but this will work for now.
We elaborated on the advantages of Firebase in class, but I want to go through the steps we need to set up Firebase and then add it to our application for both Android and iOS. Start by logging into Google services and then going to Firebase. If you are logged in, there should be an option to "Go to console"; click on that option.
After clicking on the "Add Project" card in the upper left of the console, you will have to add Google Analytics to your project. Not essential, but a good idea to add analytics so you have data on app usage.
Now that this is in place, time to set up our database. To do that, go to 'Realtime Database' (may have to click all build options to see this screen):
Once you click on this, you have the option to create a database and choose a location (your location will be different given you are in Qatar).
The next step is to choose the mode -- be sure for now to set to 'test'
This should give you a database as seen below. Nothing yet to add; we'll do that shortly with our app.
Once this is set up, we will start to add Android connectivity to this project. In the upper left corner, select the Android icon:
This will take us to the following screen:
To add in an identifier, you can use 'com.YOUR-ANDREWID-HERE.chatter' (where you replace your Andrew ID into the appropriate spot) or whatever you want to call the project. It doesn't matter if you don't have 'YOUR-ANDREWID-HERE.com' registered.
Once your app is registered, you will get a file to download, as seen below.
This google-services.json file should be put into the android/app directory. After that, open the file android/app/build.gradle and add apply plugin: 'com.google.gms.google-services' to the end where the plugins are listed (probably around line 26 or so). In the main build.gradle file located in the parent android directory, add classpath 'com.google.gms:google-services:4.3.8' to the dependencies list (around line 11). Finally do a search-replace for "com.example.<project_name, whatever you called it>" and replace it with the identifier you created earlier when you registered the app with Firebase.
Note that we could do the same for iOS, but since it is more painful and most students seem to be using Android, I am going to just summarize. To add iOS connectivity, you have to register the iOS app at Firebase (click on the iOS icon rather than Android), download the file given (a plist-type file) and add it to iOS/Runner. You then have to get Cocoapods running (which has several gotchas, depending on your configuration) and run pod install to get the appropriate libraries into the project. That should allow you to interact with Firebase using the iOS version of the plugin.
Now that we have a connection to Firebase, we want some way to write message data to it. The first step is to have an interactive text field that we can write our texts in. As mentioned in class, before we can have an interactive text field, we need to add a TextEditingController that will interact with listeners to notify them when the text has changed. We can do this by adding TextEditingController _messageController = TextEditingController(); at the beginning of our MessageListState class.
With the controller in place, let's add a widget that will create the interactive text field. The code below adds this to the body as a Padding widget that has in it two things: a list of messages and the message input field. We will loop back to the message list in a bit, but the code below gives us this basic widget:
body: Padding(
padding: EdgeInsets.all(16.0),
child: Column(
children: [
// MESSAGE LIST WILL GO HERE LATER
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Flexible(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12.0),
child: TextField(
keyboardType: TextInputType.text,
controller: _messageController,
onChanged: (text) => setState(() {}),
onSubmitted: (input) {
// ACTUALLY SEND THE TEXT NOW
},
decoration:
const InputDecoration(hintText: 'Enter new message'),
),
),
),
// ADD A DYNAMIC SUBMIT BUTTON HERE
],
),
],
),
),
Of course, this won't work without some method that actually sends a message. We could use the message DAO and its saveMessage() method, but we need a few other things. The first is that we want to make sure that there is a message to send before we try to send anything. In class, we said that we didn't want a blank message -- it had to have at least one letter or number in it. (A message with a bunch of blank spaces is not empty, but pretty useless.) To handle that, we wrote a method bool _canSendMessage() { } in class; it's your turn to do that here and fill this method out properly.
Now that we have a _canSendMessage() method, we need to use it as part of sending an actual message and save it to our database. The following code will help create that method:
void _sendMessage() {
if (_canSendMessage()) {
final message = Message(_messageController.text, DateTime.now());
widget.messageDao.saveMessage(message);
setState(() {});
}
}
The one problem here is that this will work, once we send off the message to Firebase, we need to clear it out. Luckily, our message controller has something built-in to help us: _messageController.clear();. We can add that to our the above method, and then call the _sendMessage() method in the appropriate place in our widget, and life is good. Test it out and see that it works. (You will need to go to Firebase directly to see if the message with the appropriate datetime stamp is present in your database. Do that now and be sure it is working before moving onto the next steps; better to debug an issue now than later when we add more code and it gets messier.)
We can actually submit now hitting the enter/return button on the keyboard, but some users would prefer to have a submit button of some sort that they can see and know if they hit it, the message will be sent. To make this, let's use the Cupertino Icons available on Flutter by importing this library: import 'package:flutter/cupertino.dart'; Now we'd like the button to be dynamic -- if there is no text to submit, the button will be an outline, but if there is any text, then the button is filled in and the user knows it's okay to submit. Again we can use our _canSendMessage() to help us with that. The code below should do the trick:
IconButton(
icon: Icon(_canSendMessage()
? CupertinoIcons.arrow_right_circle_fill
: CupertinoIcons.arrow_right_circle),
onPressed: () {
_sendMessage();
})
Add this to the appropriate place to the earlier widget (after the interactive text field) and try this functionality out.
Now that we can send messages, we also need to be able to display them. Of course, when we set up the DAO class, we had a method for sending data, but not retrieving data. We can add such a method with the following:
Query getMessageQuery() {
return _messagesRef;
}
Next we remember that the list of messages could be long and we'd want it scrollable and we don't want it to start at the top of the list of messages, but at the bottom. To do this, we'll add a ScrollController to our MessageListState right after our TextEditingController with the line: ScrollController _scrollController = ScrollController();. Now we need a method that uses that controller to push us to the bottom. The following method will do that:
void _scrollToBottom() {
if (_scrollController.hasClients) {
_scrollController.jumpTo(_scrollController.position.maxScrollExtent);
}
}
Of course, something has to call that method for it to be useful. Inside the build() method of MessageListState, before we return the Scaffold, add the following to call this method: WidgetsBinding.instance!.addPostFrameCallback((_) => _scrollToBottom());
We are going to put these messages inside a custom widget that will make a box with curved edges for the message text and then display the datetime below on the right in a shade of dark grey. Inside the views directory, create a new folder called widgets and add a new file called message_list_widget.dart. By now you are getting the hang of widgets, but may not be familiar with every option; we are giving you the code for this widget, but please read through it and understand how it is working.
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
class MessageWidget extends StatelessWidget {
final String message;
final DateTime date;
MessageWidget(this.message, this.date);
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(left: 1, top: 5, right: 1, bottom: 2),
child: Column(
children: [
Container(
decoration: BoxDecoration(
boxShadow: [
BoxShadow(
color: Colors.grey.shade400,
blurRadius: 2.0,
offset: Offset(0, 1.0))
],
borderRadius: BorderRadius.circular(20.0),
color: Colors.white),
child: MaterialButton(
disabledTextColor: Colors.black87,
padding: EdgeInsets.only(left: 18),
onPressed: null,
child: Wrap(
children: <Widget>[
Container(
child: Row(
children: [
Text(message),
],
)),
],
))),
Padding(
padding: const EdgeInsets.only(top: 4),
child: Align(
alignment: Alignment.topRight,
child: Text(
DateFormat('yyyy-MM-dd, kk:mma').format(date).toString(),
style: TextStyle(color: Colors.grey.shade600),
)),
),
],
));
}
}
When you create this, be sure to add this to message_list.dart so it can be used there.
With our custom widget and our scroll controller, we can now build a widget on the message_list.dart file that will retrieve messages and build a message list. To do this, we will use FirebaseAnimatedList -- take a few minutes to familiarize yourself with this class. Then add this into the file with import 'package:firebase_database/ui/firebase_animated_list.dart';
Now we just need to add a widget that (a) uses our scroll controller, (b) get data from our DAO using the getMessageQuery() method we created, and (c) puts the contents into one of our custom list widgets. We can do this with the following method added to MessageListState:
Widget _getMessageList() {
return Expanded(
child: FirebaseAnimatedList(
controller: _scrollController,
query: widget.messageDao.getMessageQuery(),
itemBuilder: (context, snapshot, animation, index) {
final json = snapshot.value as Map<dynamic, dynamic>;
final message = Message.fromJson(json);
return MessageWidget(message.text, message.date);
},
),
);
}
Now just find a to call this method in the build() method (gave you a big hint already) and we are good to go.