Flutter is Google's UI toolkit for building beautiful, natively compiled applications for mobile, web, and desktop from a single codebase. In this codelab, you'll create a simple chat application for Android, iOS, and (optionally) the web.
This codelab provides a deeper dive into Flutter than Write Your First Flutter App, part 1 and part 2. If you want a gentler introduction to Flutter, start with those.
What you learn
- How to write a Flutter app that looks natural on both Android and iOS
- How to use the Android Studio IDE, using many shortcuts supported by the Flutter plugin for Android Studio and IntelliJ
- How to debug your Flutter app
- How to run your Flutter app on an emulator, a simulator, and a device
What would you like to learn from this codelab?
You need two pieces of software to complete this codelab: the Flutter SDK ( download) and an editor ( configure). This codelab assumes that you use Android Studio, but you can use your preferred editor.
You can run this codelab using any of the following devices:
- A physical device (Android or iOS) connected to your computer and set to developer mode
- The Android emulator
- The iOS simulator
- The Chrome browser
- Windows, macOS or Linux desktop (if you enable Flutter's desktop support)
If you are running on Android, you must do some setup in Android Studio. If you are running on iOS, you must also have Xcode installed on a Mac. For more information, see Set up an editor.
Create a simple templated Flutter app. You modify this starter app to create the finished app.
Launch Android Studio.
- If you do not have open projects, then select Start a new Flutter app from the welcome page. Otherwise, select File > New > New Flutter Project.
- Select Flutter Application as the project type, and click Next.
- Verify that the Flutter SDK path specifies the SDK's location. (Select Install SDKif the text field is blank.)
- Enter
friendly_chat
as the project name, and click Next. - Use the default package name suggested by Android Studio, and click Next.
- Click Finish.
- Wait for Android Studio to install the SDK and create the project.
Alternatively, create a Flutter app on the command line.
$ flutter create friendly_chat $ cd friendly_chat $ dart migrate --apply-changes $ flutter run
Problems?
See the Test drive page for more information about creating a simple templated app. Or, use the code at the following links to get back on track.
In this section, you begin modifying the default sample app, to make it a chat app. The goal is to use Flutter to build FriendlyChat, a simple, extensible chat app with these features:
- The app displays text messages in real time.
- Users can enter a text string message, and send it either by pressing the Return key or the Send button.
- The UI runs on Android and iOS devices, as well as the web.
Try the finished app on DartPad!
Create the main app scaffold
The first element you add is a simple app bar that shows a static title for the app. As you progress through subsequent sections of this codelab, you incrementally add more responsive and stateful UI elements to the app.
The main.dart
file is located under the lib
directory in your Flutter project, and contains the main()
function that starts the execution of your app.
Replace all of the code in
main.dart
with the following:
import 'package:flutter/material.dart';
void main() {
runApp(
MaterialApp(
title: 'FriendlyChat',
home: Scaffold(
appBar: AppBar(
title: Text('FriendlyChat'),
),
),
),
);
}
Observations
- Any Dart program, whether it's a command-line app, an AngularDart app, or a Flutter app, starts with a
main()
function. - The
main()
andrunApp()
function definitions are the same as in the automatically generated app. - The
runApp()
function takes as its argument aWidget
, which the Flutter framework expands and displays to the screen at run time. - This chat app uses Material Design elements in the UI, so a
MaterialApp
object is created and passed to therunApp()
function. TheMaterialApp
widget becomes the root of your app's widget tree. - The
home
argument specifies the default screen that users see in your app. In this case, it consists of aScaffold
widget that has a simpleAppBar
as its child widget. This is typical for a Material app.
Run the app by clicking the Run icon
in the editor . The first time you run an app, it can take a while. The app is faster in later steps.
You should see something like the following:
Pixel 3XL | iPhone 11 |
Build the chat screen
To lay the groundwork for interactive components, you break the simple app into two different subclasses of widget: a root-level FriendlyChatApp
widget that never changes and a child ChatScreen
widget that rebuilds when messages are sent and internal state changes. For now, both these classes can extend StatelessWidget
. Later, you modify ChatScreen
to be a stateful widget. That way, you can change its state as needed.
Create the
FriendlyChatApp
widget:
- Inside
main()
, place the cursor in front of theM
inMaterialApp
. - Right-click, and select Refactor > Extract > Extract Flutter widget.
- Enter
FriendlyChatApp
into the ExtractWidget dialog, and click the Refactor button. TheMaterialApp
code is placed in a new stateless widget calledFriendlyChatApp
, andmain()
is updated to call that class when it calls therunApp()
function. - Select the block of text after
home:
. Start withScaffold(
and end with the Scaffold's closing parenthesis,)
. Do not include the ending comma. - Start typing
ChatScreen,
and select ChatScreen() from the popup. (Choose theChatScreen
entry that is marked with an equal sign inside the yellow circle. This gives you a class with empty parentheses, rather than a constant.)
Create a stateless widget,
ChatScreen
:
- Under the
FriendlyChatApp
class, around line 27, start typingstless
. The editor asks if you want to create aStateless
widget. Press Return to accept. The boilerplate code appears, and the cursor is positioned for you to enter the name of your stateless widget. - Enter
ChatScreen
.
Update the
ChatScreen
widget:
- Inside the
ChatScreen
widget, select Container, and start typingScaffold
. Select Scaffold from the popup. - The cursor should be positioned inside the parentheses. Press Return to start a new line.
- Start typing
appBar,
and select appBar: from the popup. - After
appBar:
, start typingAppBar,
and select the AppBar class from the popup. - Within the parentheses, start typing
title,
and select title: from the popup. - After
title:
, start typingText,
and select the Text class. - The boilerplate code for
Text
contains the worddata
. Delete the first comma afterdata
. Selectdata,
and replace it with'FriendlyChat'
. (Dart supports single or double quotation marks, but prefers single quotation marks unless the text already contains a single quotation mark.)
Look in the upper, right corner of the code pane. If you see a green checkmark, then your code passed analysis. Congratulations!
Observations
This step introduces several key concepts of the Flutter framework:
- You describe the part of the user interface represented by a widget in its
build()
method. The framework calls thebuild()
methods forFriendlyChatApp
andChatScreen
when inserting these widgets into the widget hierarchy and when their dependencies change. @override
is a Dart annotation that indicates that the tagged method overrides a superclass's method.- Some widgets, like
Scaffold
andAppBar
, are specific to Material Design apps. Other widgets, likeText
, are generic and can be used in any app. Widgets from different libraries in the Flutter framework are compatible and can work together in a single app. - Simplifying the
main()
method enables hot reload becausehot reload doesn't rerunmain()
.
Click the hot reload
button to see the changes almost instantly. After dividing the UI into separate classes and modifying the root widget, you should see no visible change in the UI.
Problems?
If your app is not running correctly, look for typos. If needed, use the code at the following link to get back on track.
In this section, you learn how to build a user control that enables the user to enter and send chat messages.
On a device, clicking the text field brings up a soft keyboard. Users can send chat messages by typing a non-empty string and pressing the Return key on the soft keyboard. Alternatively, users can send their typed messages by pressing the graphical Send button next to the input field.
For now, the UI for composing messages is at the top of the chat screen, but after you add the UI for displaying messages in the next step, you move it to the bottom of the chat screen.
Add an interactive text input field
The Flutter framework provides a Material Design widget called TextField
. It's a StatefulWidget (a widget that has mutable state) with properties for customizing the behavior of the input field. State
is information that can be read synchronously when the widget is built and might change during the lifetime of the widget. Adding the first stateful widget to the FriendlyChat app requires making a few modifications.
Change the
ChatScreen
class to be stateful:
- Select
ChatScreen
in the lineclass ChatScreen extends StatelessWidget
. - Press
Option
+
Return
(macOS) orAlt
+
Enter
(Linux and Windows) to bring up a menu. - From the menu, select Convert to StatefulWidget. The class is automatically updated with the boilerplate code for a stateful widget including a new
_ChatScreenState
class for managing state.
To manage interactions with the text field, you use a TextEditingController
object for reading the contents of the input field and for clearing the field after the chat message is sent.
Add a
TextEditingController
to _ChatScreenState.
Add the following as the first line in the _ChatScreenState
class:
final _textController = TextEditingController();
Now that your app has the ability to manage state, you can build out the _ChatScreenState
class with an input field and a Send button.
Add a
_buildTextComposer
function to _ChatScreenState
:
Widget _buildTextComposer() {
return Container(
margin: EdgeInsets.symmetric(horizontal: 8.0),
child: TextField(
controller: _textController,
onSubmitted: _handleSubmitted,
decoration: InputDecoration.collapsed(
hintText: 'Send a message'),
),
);
}
Observations
- In Flutter, stateful data for a widget is encapsulated in a
State
object. TheState
object is then associated with a widget that extends theStatefulWidget
class. - The code above defines a private method called
_buildTextComposer()
that returns aContainer
widget with a configuredTextField
widget. - The
Container
widget adds a horizontal margin between the edge of the screen and each side of the input field. - The units passed to
EdgeInsets.symmetric
are logical pixels that get translated into a specific number of physical pixels, depending on a device's pixel ratio. You might be familiar with the equivalent term for Android (density-independent pixels) or for iOS (points). - The
onSubmitted
property provides a private callback method,_handleSubmitted()
. At first, this method just clears the field, but later you extend it to send the chat message. - The
TextField
with theTextEditingController
gives you control over the text field. This controller will clear the field and read its value.
Add the
_handleSubmitted
function to _ChatScreenState
for clearing the text controller:
void _handleSubmitted(String text) {
_textController.clear();
}
Add a text composer widget
Update the
build()
method for _ChatScreenState.
After the appBar: AppBar(...)
line, add a body:
property:
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('FriendlyChat')),
body: _buildTextComposer(), // NEW
);
}
Observations
- The
_buildTextComposer
method returns a widget that encapsulates the text input field. - Adding
_buildTextComposer
to thebody
property causes the app to display the text input user control.
Hot reload the app. You should see a screen that looks like the following:
Pixel 3XL | iPhone 11 |
Add a responsive Send button
Next, you add a Send button to the right of the text field. This involves adding a bit more structure to the layout.
In the
_buildTextComposer
function, wrap the TextField
inside a Row:
- Select
TextField
in_buildTextComposer
. - Press
Option
+
Return
(macOS) orAlt
+
Enter
(Linux and Windows) to bring up a menu, and select Wrap with widget. A new widget is added that wraps theTextField
. The placeholder name is selected, and the IDE waits for you to enter a new placeholder name. - Start typing
Row,
and select Row from the list that appears. A popup appears containing the definition for theRow
's constructor. Thechild
property has a red border, and the analyzer tells you that you are missing the requiredchildren
property. - Hover over
child
and a popup appears. In the popup, it asks if you want to change the property tochildren
. Select that option. - The
children
property takes a list, rather than a single widget. (Right now, there is only one item in the list, but you will add another soon.) Convert the widget to a list of one by typing a left bracket ([
) after thechildren:
text. The editor also provides the closing right parenthesis. Delete the closing bracket. Several lines down, just before the right parenthesis that closes the row, type the right bracket followed by a comma (],
). The analyzer should now show a green checkmark. - The code is now correct, but is not well formatted. Right-click in the code pane, and select Reformat Code with dartfmt.
Wrap the
TextField
inside a Flexible
:
- Select
Row
. - Press
Option
+
Return
(macOS) orAlt
+
Enter
(Linux and Windows) to bring up a menu, and select Wrap with widget. A new widget is added that wraps theTextField
. The placeholder name is selected, and the IDE waits for you to enter a new placeholder name. - Start typing
Flexible,
and select Flexible from the list that appears. A popup appears containing the definition for theRow
's constructor.
Widget _buildTextComposer() {
return Container(
margin: EdgeInsets.symmetric(horizontal: 8.0),
child: Row( // NEW
children: [ // NEW
Flexible( // NEW
child: TextField(
controller: _textController,
onSubmitted: _handleSubmitted,
decoration: InputDecoration.collapsed(
hintText: 'Send a message'),
),
), // NEW
], // NEW
), // NEW
);
}
Observations
- Using a
Row
allows you to place the Send button adjacent to the input field. - Wrapping the
TextField
in aFlexible
widget tells theRow
to automatically size the text field to use the remaining space that isn't used by the button. - Adding the comma after the right bracket tells the formatter how to format the code.
Next, you add a Send button. This is a Material app, so use the corresponding Material icon :
Add the Send button to the
Row
.
The Send button becomes the second item in the Row
's list.
- Position the cursor at the end of the
Flexible
widget's closing right bracket and comma, and press Return to start a new line. - Start typing
Container,
and select Container from the popup. The cursor is positioned inside the container's parentheses. Press Return to start a new line. - Add the following lines of code to the container:
margin: EdgeInsets.symmetric(horizontal: 4.0),
child: IconButton(
icon: const Icon(Icons.send),
onPressed: () => _handleSubmitted(_textController.text)),
Observations
- The
IconButton
displays the Send button. - The
icon
property specifies theIcons.send
constant from the Material library to create a newIcon
instance. - Placing the
IconButton
inside aContainer
widget lets you customize the margin spacing of the button so that it visually fits better next to your input field. - The
onPressed
property uses an anonymous function to invoke the_handleSubmitted()
method and passes the contents of the message using the_textController
. - In Dart, the arrow syntax (
=> expression
) is sometimes used in declaring functions. This is shorthand for{ return expression; }
and is only used for one-line functions. For an overview of Dart function support, including anonymous and nested functions, see the Dart Language Tour.
Hot reload the app to see the Send button:
Pixel 3XL | iPhone 11 |
The color of the button is black, which comes from the default Material Design theme. To give the icons in your app an accent color, pass the color argument to IconButton
, or apply a different theme.
In
_buildTextComposer()
, wrap the Container
in an IconTheme:
.
- Select
Container
at the top of the_buildTextComposer()
function. - Press
Option
+
Return
(macOS) orAlt
+
Enter
(Linux and Windows) to bring up a menu, and select Wrap with widget. A new widget is added that wraps theContainer
. The placeholder name is selected, and the IDE waits for you to enter a new placeholder name. - Start typing
IconTheme,
and select IconTheme from the list. Thechild
property is surrounded by a red box, and the analyzer tells you that thedata
property is required. - Add the
data
property:
return IconTheme(
data: IconThemeData(color: Theme.of(context).accentColor), // NEW
child: Container(
Observations
- Icons inherit their color, opacity, and size from an
IconTheme
widget, which uses anIconThemeData
object to define these characteristics. - The
IconTheme
'sdata
property specifies theThemeData
object for the current theme. This gives the button (and any other icons in this part of the widget tree) the accent color of the current theme. - A
BuildContext
object is a handle to the location of a widget in your app's widget tree. Each widget has its ownBuildContext
, which becomes the parent of the widget returned by theStatelessWidget.build
orState.build
function. This means that_buildTextComposer()
can access theBuildContext
object from its encapsulatingState
object. You don't need to pass the context to the method explicitly.
Hot reload the app. The Send button should now be blue:
Pixel 3XL | iPhone 11 |
Problems?
If your app is not running correctly, look for typos. If needed, use the code at the following link to get back on track.
You've found something special!
There are a couple of ways to debug your app. You can either use your IDE directly to set breakpoints, or you can use Dart DevTools (not to be confused with Chrome DevTools). This codelab demonstrates how to set breakpoints using Android Studio and IntelliJ. If you are using another editor, like VS Code, use DevTools for debugging. For a gentle introduction to Dart DevTools, see Step 2.5 of Write your first Flutter app on the web.
The Android Studio and IntelliJ IDEs enable you to debug Flutter apps running on an emulator, a simulator, or a device. With these editors, you can:
- Select a device or simulator to debug your app on.
- View the console messages.
- Set breakpoints in your code.
- Examine variables and evaluate expressions at runtime.
The Android Studio and IntelliJ editors show the system log while your app is running, and provides a Debugger UI to work with breakpoints and control the execution flow.
Work with breakpoints
Debug your Flutter app using breakpoints:
- Open the source file in which you want to set a breakpoint.
- Locate the line where you want to set a breakpoint, click it, and then select Run > Toggle Line Breakpoint. Alternatively, you can click in the gutter (to the right of the line number) to toggle a breakpoint.
- If you weren't running in debug mode, stop the app.
- Restart the app using Run > Debug, or by clicking the Run debug button in the UI.
The editor launches the Debugger UI and pauses the execution of your app when it reaches the breakpoint. You can then use the controls in the Debugger UI to identify the cause of the error.
Practice using the debugger by setting breakpoints on the build()
methods in your FriendlyChat app, and then run and debug the app. You can inspect the stack frames to see the history of method calls by your app.
With the basic app scaffolding and screen in place, now you're ready to define the area where chat messages are displayed.
Implement a chat message list
In this section, you create a widget that displays chat messages using composition (creating and combining multiple smaller widgets). You start with a widget that represents a single chat message. Then, you nest that widget in a parent scrollable list. Finally, you nest the scrollable list in the basic app scaffold.
Add the
ChatMessage
stateless widget:
- Position the cursor after the
FriendlyChatApp
class and start to typestless
. (The order of the classes doesn't matter, but this order makes it easier to compare your code to the solution.) - Enter
ChatMessage
for the class name.
Add a
Row
to the build()
method for ChatMessage
:
- Position the cursor inside the parentheses in
return Container()
, and press Return to start a new line. - Add a
margin
property:
margin: EdgeInsets.symmetric(vertical: 10.0),
- The
Container'
s child will be aRow
. TheRow
's list contains two widgets: an avatar and a column of text.
return Container(
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
margin: const EdgeInsets.only(right: 16.0),
child: CircleAvatar(child: Text(_name[0])),
),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(_name, style: Theme.of(context).textTheme.headline4),
Container(
margin: EdgeInsets.only(top: 5.0),
child: Text(text),
),
],
),
],
),
);
- Add a
text
variable and a constructor to the top ofChatMessage
:
class ChatMessage extends StatelessWidget {
ChatMessage({required this.text}); // NEW
final String text; // NEW
At this point, the analyzer should only complain about _name
being undefined. You fix that next.
Define the
_name
variable.
Define the _name
variable as shown, replacing Your Name
with your own name. You use this variable to label each chat message with the sender's name. In this codelab, you hard-code the value for simplicity, but most apps retrieve the sender's name through authentication. After the main()
function, add the following line:
String _name = 'Your Name';
Observations
- The
build()
method forChatMessage
returns aRow
that displays a simple graphical avatar to represent the user who sent the chat message, aColumn
widget containing the sender's name, and the text of the message. - The
CircleAvatar
is personalized by labeling it with the user's first initial by passing the first character of the_name
variable's value to a childText
widget. - The
crossAxisAlignment
parameter specifiesCrossAxisAlignment.start
in theRow
constructor to position the avatar and messages relative to their parent widgets. For the avatar, the parent is aRow
widget whose main axis is horizontal, soCrossAxisAlignment.start
gives it the highest position along the vertical axis. For messages, the parent is aColumn
widget whose main axis is vertical, soCrossAxisAlignment.start
aligns the text at the furthest left position along the horizontal axis. - Next to the avatar, two
Text
widgets are vertically aligned to display the sender's name on top and the text of the message below. Theme.of(context)
provides the default FlutterThemeData
object for the app. In a later step, you override this default theme to style your app differently for Android and iOS.- The
ThemeData
'stextTheme
property gives you access to Material Design logical styles for text likeheadline4
, so you can avoid hard-coding font sizes and other text attributes. In this example, the sender's name is styled to make it larger than the message text.
Hot reload the app.
Type messages into the text field. Press the Send button to clear the message. Type a long message into the text field to see what happens when the text field overflows. Later, in step 9, you wrap the column in an Expanded
widget to make the Text
widget wrap.
Implement a chat message list in the UI
The next refinement is to get the list of chat messages and show it in the UI. You want this list to be scrollable so that users can view the message history. The list should also present the messages in chronological order, with the most recent message displayed at the bottom-most row of the visible list.
Add a
_messages
list to _ChatScreenState
.
In the _ChatScreenState
definition, add a List
member called _messages
to represent each chat message:
class _ChatScreenState extends State<ChatScreen> {
final List<ChatMessage> _messages = []; // NEW
final _textController = TextEditingController();
Modify the
_handleSubmitted()
method in _ChatScreenState.
When the user sends a chat message from the text field, the app should add the new message to the message list. Modify the _handleSubmitted()
method to implement this behavior:
void _handleSubmitted(String text) {
_textController.clear();
ChatMessage message = ChatMessage( //NEW
text: text, //NEW
); //NEW
setState(() { //NEW
_messages.insert(0, message); //NEW
}); //NEW
}
Put the focus back on the text field after content submission.
- Add a
FocusNode
to_ChatScreenState
:
class _ChatScreenState extends State<ChatScreen> {
final List<ChatMessage> _messages = [];
final _textController = TextEditingController();
final FocusNode _focusNode = FocusNode(); // NEW
- Add the
focusNode
property to theTextField
in_buildTextComposer()
:
child: TextField(
controller: _textController,
onSubmitted: _handleSubmitted,
decoration: InputDecoration.collapsed(hintText: 'Send a message'),
focusNode: _focusNode, // NEW
),
- In
_handleSubmitted()
, after the call tosetState()
, request focus on theTextField
:
setState(() {
_messages.insert(0, message);
});
_focusNode.requestFocus(); // NEW
Observations
- Each item in the list is a
ChatMessage
instance. - The list is initialized to be empty.
- Calling
setState()
to modify_messages
lets the framework know that this part of the widget tree changed, and it needs to rebuild the UI. Only synchronous operations should be performed insetState()
because otherwise the framework could rebuild the widgets before the operation finishes. - In general, it is possible to call
setState()
with an empty closure after some private data changed outside of this method call. However, updating data insidesetState
's closure is preferred, so you don't forget to call it afterward.
Hot reload the app.
Enter text into the text field and press Return
. The text field once again has the focus.
Place the message list
You're now ready to display the list of chat messages. Get the ChatMessage
widgets from the _messages
list, and put them in a ListView
widget, for a scrollable list.
In the
build()
method for _ChatScreenState
, add a ListView
inside a Column
:
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text ('FriendlyChat')),
body: Column( // MODIFIED
children: [ // NEW
Flexible( // NEW
child: ListView.builder( // NEW
padding: EdgeInsets.all(8.0), // NEW
reverse: true, // NEW
itemBuilder: (_, int index) => _messages[index], // NEW
itemCount: _messages.length, // NEW
), // NEW
), // NEW
Divider(height: 1.0), // NEW
Container( // NEW
decoration: BoxDecoration(
color: Theme.of(context).cardColor), // NEW
child: _buildTextComposer(), // MODIFIED
), // NEW
], // NEW
), // NEW
);
}
Observations
- The
ListView.builder
factory method builds a list on demand by providing a function that is called once per item in the list. The function returns a new widget at each call. The builder also automatically detects mutations of itschildren
parameter and initiates a rebuild. - The parameters passed to the
ListView.builder
constructor customize the list contents and appearance: padding
creates whitespace around the message text.itemCount
specifies the number of messages in the list.itemBuilder
provides the function that builds each widget in[index]
. Because you don't need the current build context, you can ignore the first argument ofIndexedWidgetBuilder
. Naming the argument with an underscore (_) and nothing else is a convention indicating that the argument won't be used.- The
body
property of theScaffold
widget now contains the list of incoming messages as well as the input field and Send button. The layout uses the following widgets: Column
: Lays out its direct children vertically. TheColumn
widget takes a list of child widgets (same as aRow
) that becomes a scrolling list and a row for an input field.Flexible
, as a parent ofListView
: Tells the framework to let the list of received messages expand to fill theColumn
height whileTextField
remains a fixed size.Divider
: Draws a horizontal line between the UI for displaying messages and the text input field for composing messages.Container
, as a parent of the text composer: Defines background images, padding, margins, and other common layout details.decoration
: Creates a newBoxDecoration
object that defines the background color. In this case you're using thecardColor
defined by theThemeData
object of the default theme. This gives the UI for composing messages a different background from the messages list.
Hot reload the app. You should see a screen that looks as follows:
Pixel 3XL | iPhone 11 |
Try sending a few chat messages using the UIs for composing and displaying that you just built!
Pixel 3XL | iPhone 11 |
Problems?
If your app is not running correctly, look for typos. If needed, use the code at the following link to get back on track.
You can add animation to your widgets to make the user experience of your app more fluid and intuitive. In this section, you learn how to add a basic animation effect to your chat message list.
When the user sends a new chat message, instead of simply displaying it in the message list, you animate the message to vertically ease up from the bottom of the screen.
Animations in Flutter are encapsulated as Animation
objects that contain a typed value and a status (such as forward, reverse, completed, and dismissed). You can attach an animation object to a widget or listen for changes to the animation object. Based on changes to the animation object's properties, the framework can modify the way your widget appears and rebuild the widget tree.
Specify an animation controller
Use the AnimationController
class to specify how the animation should run. The AnimationController
lets you define important characteristics of the animation, such as its duration and playback direction (forward or reverse).
Update the
_ChatScreenState
class definition to include a TickerProviderStateMixin
:
class _ChatScreenState extends State<ChatScreen> with TickerProviderStateMixin { // MODIFIED
final List<ChatMessage> _messages = [];
final _textController = TextEditingController();
final FocusNode _focusNode = FocusNode();
...
In the
ChatMessage
class definition, add a variable to store the animation controller:
class ChatMessage extends StatelessWidget {
ChatMessage({required this.text, required this.animationController}); // MODIFIED
final String text;
final AnimationController animationController; // NEW
...
Add an animation controller to the
_handleSubmitted()
method:
void _handleSubmitted(String text) {
_textController.clear();
var message = ChatMessage(
text: text,
animationController: AnimationController( // NEW
duration: const Duration(milliseconds: 700), // NEW
vsync: this, // NEW
), // NEW
); // NEW
setState(() {
_messages.insert(0, message);
});
_focusNode.requestFocus();
message.animationController.forward(); // NEW
}
Observations
- The
AnimationController
specifies the animation's runtime duration to be 700 milliseconds. (This longer duration slows the animation effect so you can see the transition happen more gradually. In practice, you probably want to set a shorter duration when running your app.) - The animation controller is attached to a new
ChatMessage
instance, and specifies that the animation should play forward whenever a message is added to the chat list. - When creating an
AnimationController
, you must pass it avsync
argument. Thevsync
is the source of heartbeats (theTicker
) that drives the animation forward. This example uses_ChatScreenState
as thevsync
, so it adds aTickerProviderStateMixin
mixin to the_ChatScreenState
class definition. - In Dart, a mixin allows a class body to be reused in multiple class hierarchies. For more information, see Adding features to a class: mixins, a section in the Dart Language Tour.
Add a SizeTransition widget
Adding a SizeTransition
widget to the animation has the effect of animating a ClipRect
that increasingly exposes the text as it slides in.
Add a
SizeTransition
widget to the build()
method for ChatMessage
:
- In the
build()
method forChatMessage
, select the firstContainer
instance. - Press
Option
+
Return
(macOS) orAlt
+
Enter
(Linux and Windows) to bring up a menu, and select Wrap with widget. - Enter
SizeTransition
. A red box appears around thechild:
property. This indicates that a required property is missing from the widget class. Hover overSizeTransition,
and a tooltip points out thatsizeFactor
is required and offers to create it. Choose that option, and the property appears with anull
value. - Replace
null
with an instanceCurvedAnimation
. This adds the boilerplate code for two properties:parent
(required) andcurve
. - For the
parent
property, replacenull
with theanimationController
. - For the
curve
property, replacenull
withCurves.easeOut
, one of the constants from theCurves
class. - Add a line after
sizeFactor
(but at the same level), and enter anaxisAlignment
property to theSizeTransition
, with a value of 0.0.
@override
Widget build(BuildContext context) {
return SizeTransition( // NEW
sizeFactor: // NEW
CurvedAnimation(parent: animationController, curve: Curves.easeOut), // NEW
axisAlignment: 0.0, // NEW
child: Container( // MODIFIED
...
Observations
- The
CurvedAnimation
object, in conjunction with theSizeTransition
class, produces an ease-out animation effect. The ease-out effect causes the message to slide up quickly at the beginning of the animation and slow down until it comes to a stop. - The
SizeTransition
widget behaves as an animatingClipRect
that exposes more of the text as it slides in.
Dispose of the animation
It's good practice to dispose of your animation controllers to free up your resources when they are no longer needed.
Add the
dispose()
method to _ChatScreenState.
Add the following method to the bottom of _ChatScreenState
:
@override
void dispose() {
for (var message in _messages){
message.animationController.dispose();
}
super.dispose();
}
The code is now correct, but is not well formatted. Right-click in the code pane, and select Reformat Code with dartfmt.
Hot reload the app (or hot restart, if the running app contains chat messages), and enter a few messages to observe the animation effect.
If you want to experiment further with animations, then here are a few ideas to try:
- Speed up or slow down the animation by modifying the
duration
value specified in the_handleSubmitted()
method. - Specify different animation curves by using the constants defined in the
Curves
class. - Create a fade-in animation effect by wrapping the
Container
in aFadeTransition
widget instead of aSizeTransition
.
Problems?
If your app is not running correctly, look for typos. If needed, use the code at the following link to get back on track.
In this optional step, you give your app a few sophisticated details, like making the Send button enabled only when there's text to send, wrapping longer messages, and adding native-looking customizations for Android and for iOS.
Make the send button context-aware
Currently, the Send button appears enabled, even when there is no text in the input field. You might want the button's appearance to change depending on whether the field contains text to send.
Define
_isComposing
, a private variable that is true whenever the user types in the input field:
class _ChatScreenState extends State<ChatScreen> with TickerProviderStateMixin {
final List<ChatMessage> _messages = [];
final _textController = TextEditingController();
final FocusNode _focusNode = FocusNode();
bool _isComposing = false; // NEW
Add an
onChanged()
callback method to _ChatScreenState.
In the _buildTextComposer()
method, add the onChanged
property to the TextField
, and update the onSubmitted
property:
Flexible(
child: TextField(
controller: _textController,
onChanged: (String text) { // NEW
setState(() { // NEW
_isComposing = text.isNotEmpty; // NEW
}); // NEW
}, // NEW
onSubmitted: _isComposing ? _handleSubmitted : null, // MODIFIED
decoration:
InputDecoration.collapsed(hintText: 'Send a message'),
focusNode: _focusNode,
),
),
Update the
onPressed()
callback method in _ChatScreenState.
While still in the _buildTextComposer()
method, update the onPressed
property for the IconButton
:
Container(
margin: EdgeInsets.symmetric(horizontal: 4.0),
child: IconButton(
icon: const Icon(Icons.send),
onPressed: _isComposing // MODIFIED
? () => _handleSubmitted(_textController.text) // MODIFIED
: null, // MODIFIED
)
...
)
Modify
_handleSubmitted
to set _isComposing
to false when the text field is cleared:
void _handleSubmitted(String text) {
_textController.clear();
setState(() { // NEW
_isComposing = false; // NEW
}); // NEW
ChatMessage message = ChatMessage(
...
Observations
- The
onChanged
callback notifies theTextField
that the user edited its text.TextField
calls this method whenever its value changes from the current value of the field. - The
onChanged
callback callssetState()
to change the value of_isComposing
to true when the field contains some text. - When
_isComposing
is false, theonPressed
property is set tonull
. - The
onSubmitted
property was also modified so that it won't add an empty string to the message list. - The
_isComposing
variable now controls the behavior and the visual appearance of the Send button. - If the user types a string in the text field, then
_isComposing
istrue,
and the button's color is set toTheme.of(context).accentColor
. When the user presses the Send button, the framework invokes_handleSubmitted()
. - If the user types nothing in the text field, then
_isComposing
isfalse,
and the widget'sonPressed
property is set tonull
, disabling the Send button. The framework automatically changes the button's color toTheme.of(context).disabledColor
.
Hot reload your app to try it out!
Wrap long lines
When a user sends a chat message that exceeds the width of the UI for displaying messages, the lines should wrap so the entire message displays. Right now, lines that overflow are truncated, and a visual overflow error displays. A simple way of making sure that the text wraps correctly is to put it inside of an Expanded
widget.
Wrap the
Column
widget with an Expanded
widget:
- In the
build()
method forChatMessage
, select theColumn
widget inside theRow
for theContainer
. - Press
Option
+
Return
(macOS) orAlt
+
Enter
(Linux and Windows) to bring up a menu. - Start typing
Expanded,
and select Expanded from the list of possible objects.
The following code sample shows how the ChatMessage
class looks after making this change:
...
Container(
margin: const EdgeInsets.only(right: 16.0),
child: CircleAvatar(child: Text(_name[0])),
),
Expanded( // NEW
child: Column( // MODIFIED
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(_name, style: Theme.of(context).textTheme.headline4),
Container(
margin: EdgeInsets.only(top: 5.0),
child: Text(text),
),
],
),
), // NEW
...
Observations
The Expanded
widget allows its child widget (like Column
) to impose layout constraints (in this case the Column
's width) on a child widget. Here, it constrains the width of the Text
widget, which is normally determined by its contents.
Customize for Android and iOS
To give your app's UI a natural look and feel, you can add a theme and some simple logic to the build()
method for the FriendlyChatApp
class. In this step, you define a platform theme that applies a different set of primary and accent colors. You also customize the Send button to use a Material Design IconButton
on Android and a CupertinoButton
on iOS.
Add the following code to
main.dart
, after the main()
method:
final ThemeData kIOSTheme = ThemeData(
primarySwatch: Colors.orange,
primaryColor: Colors.grey[100],
primaryColorBrightness: Brightness.light,
);
final ThemeData kDefaultTheme = ThemeData(
primarySwatch: Colors.purple,
accentColor: Colors.orangeAccent[400],
);
Observations
- The
kDefaultTheme ThemeData
object specifies colors for Android (purple with orange accents). - The
kIOSTheme
ThemeData
object specifies colors for iOS (light grey with orange accents).
Modify the
FriendlyChatApp
class to vary the theme using the theme
property of your app's MaterialApp
widget:
- Import the foundation package at the top of the file:
import 'package:flutter/foundation.dart'; // NEW
import 'package:flutter/material.dart';
- Modify the
FriendlyChatApp
class to choose an appropriate theme:
class FriendlyChatApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'FriendlyChat',
theme: defaultTargetPlatform == TargetPlatform.iOS // NEW
? kIOSTheme // NEW
: kDefaultTheme, // NEW
home: ChatScreen(),
);
}
}
Modify the theme of the
AppBar
widget (the banner at the top of your app's UI).
- In the
build()
method of_ChatScreenState
, find the following line of code:
appBar: AppBar(title: Text('FriendlyChat')),
- Place the cursor between the two right parentheses (
))
), type a comma, and press Return to start a new line. - Add the following two lines:
elevation:
Theme.of(context).platform == TargetPlatform.iOS ? 0.0 : 4.0, // NEW
- Right-click in the code pane, and select Reformat code with dartfmt.
Observations
- The top-level
defaultTargetPlatform
property and conditional operators are used to select the theme. - The
elevation
property defines the z-coordinates of theAppBar
. A z-coordinate value of4.0
has a defined shadow (Android), and a value of0.0
has no shadow (iOS). .
Customize the send icon for Android and for iOS.
- Add the following import to the top of
main.dart
:
import 'package:flutter/cupertino.dart'; // NEW
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
- In
_ChatScreenState
's_buildTextComposer()
method, modify the line that assigns anIconButton
as the child of theContainer
. Change the assignment to be conditional on the platform. For iOS, use aCupertinoButton
; otherwise, stay with anIconButton
:
Container(
margin: EdgeInsets.symmetric(horizontal: 4.0),
child: Theme.of(context).platform == TargetPlatform.iOS ? // MODIFIED
CupertinoButton( // NEW
child: Text('Send'), // NEW
onPressed: _isComposing // NEW
? () => _handleSubmitted(_textController.text) // NEW
: null,) : // NEW
IconButton( // MODIFIED
icon: const Icon(Icons.send),
onPressed: _isComposing ?
() => _handleSubmitted(_textController.text) : null,
)
),
Wrap the top-level
Column
in a Container
widget, and give it a light grey border on its upper edge.
This border helps visually distinguish the app bar from the body of the app on iOS. To hide the border on Android, apply the same logic used for the app bar in the previous code sample:
- In
_ChatScreenState
'sbuild()
method, select theColumn
that appears afterbody:
. - Press
Option
+
Return
(macOS) orAlt
+
Enter
(Linux and Windows) to bring up a menu, and select Wrap with Container. - After the end of that
Column
, but before the end of theContainer
, add the code (shown) that conditionally adds the appropriate button depending on the platform.
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('FriendlyChat'),
elevation:
Theme.of(context).platform == TargetPlatform.iOS ? 0.0 : 4.0, // NEW
),
body: Container(
child: Column(
children: [
Flexible(
child: ListView.builder(
padding: EdgeInsets.all(8.0),
reverse: true,
itemBuilder: (_, int index) => _messages[index],
itemCount: _messages.length,
),
),
Divider(height: 1.0),
Container(
decoration: BoxDecoration(color: Theme.of(context).cardColor),
child: _buildTextComposer(),
),
],
),
decoration: Theme.of(context).platform == TargetPlatform.iOS // NEW
? BoxDecoration( // NEW
border: Border( // NEW
top: BorderSide(color: Colors.grey[200]!), // NEW
), // NEW
) // NEW
: null), // MODIFIED
);
}
Hot reload the app. You should see different colors, shadows, and icon buttons for Android and for iOS.
Pixel 3XL | iPhone 11 |
Problems?
If your app is not running correctly, look for typos. If needed, use the code at the following link to get back on track.
Congratulations!
You now know the basics of building cross-platform mobile apps with the Flutter framework.
What we covered
- How to build a Flutter app from the ground up
- How to use some of the shortcuts provided in Android Studio and IntelliJ
- How to run, hot reload, and debug your Flutter app on an emulator, a simulator, and a device
- How to customize your user interface with widgets and animations
- How to customize your user interface for Android and iOS
What's next
Try one of the other Flutter codelabs.
Continue learning about Flutter:
- flutter.dev: The documentation site for the Flutter project
- The Flutter cookbook
- The Flutter API reference documentation
- Additional Flutter sample apps, with source code
For more information about keyboard shortcuts:
- Flutter — IDE Shortcuts for Faster Development by Pooja Bhaumik
- Flutter: My favourite keyboard shortcuts by Andrea Bizzotto
You might want to download the sample code to view the samples as reference or start the codelab at a specific section. To get a copy of the sample code for the codelab, run this command from your terminal:
git clone https://github.com/flutter/codelabs
The sample code for this codelab is in the friendly_chat
folder. Each numbered step folder lines up with how the code looks at the end of the numbered steps of this codelab. You can also drop the code from the lib/main.dart
file from any of these steps into a DartPad instance and run them from there.