Flutter is an open source SDK for creating high-performance, high-fidelity mobile apps for iOS and Android. The Flutter framework makes it easy for you to build user interfaces that react smoothly in your app, while reducing the amount of code required to synchronize and update your app's view.

Flutter makes it easy to get started building beautiful apps, with its rich set of Material Design and Cupertino (iOS) widgets and behaviors. Your users will love your app's natural look and feel, because Flutter implements platform-specific scrolling, navigational patterns, fonts, and more. You'll feel powerful and productive with Flutter's functional-reactive framework and our extremely fast hot reloads on devices and emulators.

You'll write your Flutter apps in Dart. Dart syntax should look familiar if you already know Java, JavaScript, C#, or Swift. Dart is compiled using the standard Android and iOS toolchains for the specific mobile platform where your app needs to run. You get all the benefits of the Dart language, including familiar and terse syntax, first-class functions, async/await, rich standard libraries, and more.

What you'll learn

What is your level of experience with building mobile apps?

Never built mobile apps Built apps for the mobile web only Built apps for Android only Built apps for iOS only Built apps for Android and iOS Built apps for mobile web, Android, and iOS

Prerequisites

To develop mobile apps with Flutter you need:

To build and run Flutter apps on iOS:

To build and run Flutter apps on Android:

Get detailed Flutter setup information

Before proceeding with this codelab, run the flutter doctor command and see that all the checkmarks are showing; this will download any missing SDK files you need and ensure that your codelab machine is set up correctly for Flutter development.

 flutter doctor

The next section of this codelab walks you through the basics of using IntelliJ IDEA to create and run a Flutter app.

Create a Flutter project

To start a new Flutter project in the IntelliJ editor:

  1. Launch the IDE.
  2. Select Create New Project > Flutter. If a project is already open, use File > New > Project... > Flutter.
  3. Enter or browse to your Flutter SDK directory, then click Next. This is the top-level flutter directory, without the bin subdirectory. For example, /Users/obiwan/flutter.
  4. Name your project, for example my_friendlychat. Use all lowercase letters (required) with underscore characters as separators (recommended). The first character must be a letter. The default location is $HOME/IdeaProjects/<project_name>, but you can specify a different directory if you like.
  5. Press Finish to create your Flutter project.

IntelliJ creates a project directory and generates the files for a basic Flutter sample app. In this codelab, you'll work in lib/main.dart. This source file written in the Dart programming language is the main entry point for your Flutter app. You'll revisit this file as you progress through this codelab and add more widgets to your app.

Congratulations, you've created your first Flutter app!

Run your app

Follow these instructions to run the app from the IntelliJ editor. (Alternatively, you can start your app from the terminal, as described in Run your Flutter app on the Flutter website.)

iOS Simulator or Android Emulator

To deploy your Flutter app to a simulator or emulator, you'll need to perform these steps:

  1. If the iOS Simulator is not already running, launch the simulator on your development machine by selecting Flutter Device Selection>Open iOS Simulator.
  2. For the Android Emulator, select Tools > Android > AVD Manager to create a virtual device and start the emulator.
  3. Start your Flutter app in IntelliJ:

If your Flutter app starts successfully, you should see a screen like this on your development machine:

iOS or Android Device

To deploy your Flutter app to a device, you'll need to perform these steps:

  1. Connect the device to your development machine via USB.
  2. Start your Flutter app in IntelliJ:

If your Flutter app starts successfully, you should see a screen like this on the attached device.

In this section, you'll begin modifying the default sample app into a chat app. The goal is to use Flutter to build Friendlychat, a simple, extensible chat app with these features:

iOS

Android

Create the main app scaffold

The first element you'll add is a simple app bar that shows a static title for the app. As you progress through subsequent sections of this codelab, you'll 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.

The main() and runApp() function definitions are the same as in the default app. The runApp() function takes as its argument a Widget which the Flutter framework expands and displays to the screen of the app at run time. Since the app uses Material Design elements in the UI, create a new MaterialApp object and pass it to the runApp() function; this widget becomes the root of your widget tree.

main.dart

// Replace the code in main.dart with the following.

import 'package:flutter/material.dart';

void main() {
 runApp(
   new MaterialApp(
     title: "Friendlychat",
     home: new Scaffold(
       appBar: new AppBar(
         title: new Text("Friendlychat"),
       ),
     ),
   ),
 );
}

To specify the default screen that users see in your app, set the home argument in your MaterialApp definition. The home argument references a widget that defines the main UI for this app. The widget consists of a Scaffold widget that has a simple AppBar as its child widget.

If you run the app (), you should see a single screen that looks like this.

iOS

Android

Build the chat screen

To lay the groundwork for interactive components, you'll break the simple app into two different subclasses of widget: a root-level FriendlychatApp widget that never changes, and a child ChatScreen widget that can rebuild when messages are sent and internal state changes. For now, both these classes can extend StatelessWidget. Later, we'll adapt ChatScreen to manage state.

main.dart

// Replace the code in main.dart with the following.

import 'package:flutter/material.dart';

void main() {
 runApp(new FriendlychatApp());
}

class FriendlychatApp extends StatelessWidget {
 @override
 Widget build(BuildContext context) {
   return new MaterialApp(
     title: "Friendlychat",
     home: new ChatScreen(),
   );
 }
}

class ChatScreen extends StatelessWidget {
 @override
 Widget build(BuildContext context) {
   return new Scaffold(
     appBar: new AppBar(title: new Text("Friendlychat")),
   );
 }
}

The new widgets in the above code snippet share something in common; they are layout widgets. Their role is to perform layout tasks like anchoring, aligning, and distributing other widgets.

As you continue to make changes and refine the UI for your app, you can view the results quickly, without a full application restart. Use Flutter's hot reload feature to inject updated source files into the running Dart Virtual Machine and refresh the UI. Hot reload is a powerful tool for experimentation, prototyping, and iteration.

If you click the Hot Reload button () after dividing the UI into separate classes, you should see no change:

iOS

Android

In this section, you'll learn how to build a user control that enables the user to enter and send text messages.

On a device, clicking on 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 we add the UI for displaying messages in the next step, it will move 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 stateful widget with properties for customizing the behavior of the input field. As the first stateful widget of this codelab, it requires some modifications to manage internal state changes.

In Flutter, if you want to visually present stateful data in a widget, you should encapsulate this data in a State object. You can then associate your State object with a widget that extends the StatefulWidget class.

The following code snippet shows how you might start to define a class in your main.dart file to add the interactive text input field. First you'll change the ChatScreen class to subclass StatefulWidget instead of StatelessWidget. Then, you'll define a new ChatScreenState class that creates the State object.

Override the createState() method as shown to attach the ChatScreenState class. You'll use the new class to build the stateful TextField widget.

Add a line above the build() method to define the ChatScreenState class:

main.dart

// Modify the ChatScreen class definition to extend StatefulWidget.

class ChatScreen extends StatefulWidget {                     //modified
 @override                                                         //new
 State createState() => new ChatScreenState();                     //new
} 

// Add the ChatScreenState class definition in main.dart.

class ChatScreenState extends State<ChatScreen> {                  //new
 @override                                                         //new
 Widget build(BuildContext context) {
   return new Scaffold(
      appBar: new AppBar(
        title: new Text("Friendlychat")
       ),
    );
  }
}

Now the build() method for ChatScreenState should include all the widgets formerly in the ChatScreen part of the widget tree. When the framework calls the build() method to refresh the UI, it can rebuild ChatScreenState with its tree of children widgets.

Now that your app has the ability to manage state, you can build out the ChatScreenState class with an input field and send button.

To manage interactions with the text field, it's helpful to use a TextEditingController object. You'll use it for reading the contents of the input field, and for clearing the field after the text message is sent. Add a line to the ChatScreenState class definition to create this object.

main.dart

// Add the following code in the ChatScreenState class definition.

class ChatScreenState extends State<ChatScreen> {
  final TextEditingController _textController = new TextEditingController(); //new

The following code snippet shows how you can define a private method called _buildTextComposer() that returns a Container widget with a configured TextField widget.

main.dart

// Add the following code in the ChatScreenState class definition.

Widget _buildTextComposer() {
 return new Container(
     margin: const EdgeInsets.symmetric(horizontal: 8.0),
     child: new TextField(
        controller: _textController,
        onSubmitted: _handleSubmitted,
        decoration: new InputDecoration.collapsed(
          hintText: "Send a message"),
        ),
    );
}

Start with a Container widget that adds a horizontal margin between the edge of the screen and each side of the input field. The units here 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 iOS (points) or for Android (density-independent pixels).

Add a TextField widget and configure it as follows to manage user interactions:

main.dart

// Add the following code in the ChatScreenState class definition.

void _handleSubmitted(String text) {
 _textController.clear();
}

Place the text composer widget

Now, tell the app how to display the text input user control. In the build() method of your ChatScreenState class, attach a private method called _buildTextComposer to the body property. The _buildTextComposer method returns a widget that encapsulates the text input field.

main.dart

// Modify the code in the ChatScreenState class definition as follows.

 @override
 Widget build(BuildContext context) {
   return new Scaffold(
       appBar: new AppBar(
           title: new Text("Friendlychat")
       ),
       body: _buildTextComposer() //new
   );
 }

If you restart the app () , you should see a single screen that looks like this.

iOS

Android

The change from stateless to stateful widget requires restarting the app. For details on the types of changes that can be hot reloaded, see the Flutter IntelliJ documentation.

Add a responsive Send button

Next, we'll add a ‘Send' button to the right of the text field. Since we want to display the button adjacent to the input field, we'll use a Row widget as the parent.

Then wrap the TextField widget in a Flexible widget. This tells the Row to automatically size the text field to use the remaining space that isn't used by the button.

main.dart

// Modify the _buildTextComposer method with the code below to arrange the 
// text input field and send button.

Widget _buildTextComposer() {
 return new Container(
     margin: const EdgeInsets.symmetric(horizontal: 8.0),
     child: new Row(                                              //new
         children: <Widget>[                                      //new
           new Flexible(                                          //new
             child: new TextField(
               controller: _textController,
               onSubmitted: _handleSubmitted,
               decoration: new InputDecoration.collapsed(
               hintText: "Send a message"),
             ),
           )                                                       //new
         ]                                                         //new
     )                                                             //new
  );
}

You can now create an IconButton widget that displays the Send icon. In the icon property, use the Icons.send constant to create a new Icon instance. This constant indicates that your widget uses the following ‘Send' icon provided by the material icons library.

Put your IconButton widget inside another Container parent widget; this lets you customize the margin spacing of the button so that it visually fits better next to your input field. For the onPressed property, use an anonymous function to also invoke the _handleSubmitted() method and use _textController to pass it the contents of the message.

main.dart

// Modify the _buildTextComposer method with the code below to define the 
// send button.

Widget _buildTextComposer() {
 return new Container(
   margin: const EdgeInsets.symmetric(horizontal: 8.0),
   child: new Row(
     children: <Widget>[
       new Flexible(
         child: new TextField(
           controller: _textController,
           onSubmitted: _handleSubmitted,
           decoration: new InputDecoration.collapsed(
             hintText: "Send a message"),
         ),
       ),
       new Container(                                          //new
         margin: new EdgeInsets.symmetric(horizontal: 4.0),    //new
         child: new IconButton(                                //new
           icon: new Icon(Icons.send),
           onPressed: () => _handleSubmitted(_textController.text)),//new
       ),                                                     //new
     ]
   ),
 );
}

The color of the button is black, from the default Material Design theme. To give the icons in your app an accent color, you can apply a different theme.

Icons inherit their color, opacity, and size from an IconTheme widget, which uses an IconThemeData object to define these characteristics. Wrap all the widgets in the _buildTextComposer() method in an IconTheme widget, and use its data property to specify the ThemeData object of 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.

main.dart

// Modify the _buildTextComposer method with the code below to give the 
// send button the current theme's accent color.

Widget _buildTextComposer() {
 return new IconTheme(                                             //new
   data: new IconThemeData(color: Theme.of(context).accentColor),  //new
   child: new Container(                                      //modified
     margin: const EdgeInsets.symmetric(horizontal: 8.0),
     child: new Row(
       children: <Widget>[
         new Flexible(
           child: new TextField(
             controller: _textController,
             onSubmitted: _handleSubmitted,
             decoration: new InputDecoration.collapsed(
               hintText: "Send a message"),
           ),
         ),
         new Container(
           margin: new EdgeInsets.symmetric(horizontal: 4.0),
           child: new IconButton(
             icon: new Icon(Icons.send),
             onPressed: () => _handleSubmitted(_textController.text)),
         ),
       ]
     ),
   )                                                              //new
 );
}

The _buildTextComposer() method has access to the BuildContext object from its encapsulating State object; you don't need to pass the context to the method explicitly.

If you hot reload the app (), you should see a screen that looks like this.

iOS

Android

Debug your app with IntelliJ

The IntelliJ IDE enables you to debug Flutter apps running on a simulator/emulator or on a device. With the IntelliJ editor, you can:

The IntelliJ editor shows 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

To debug your Flutter app using breakpoints:

  1. Open the source file in which you want to set a breakpoint.
  2. Locate the line where you want to set a breakpoint, click on it, then select Run > Toggle Line Breakpoint in the menu. Alternatively, you can click in the gutter (to the right of the line number) to toggle a breakpoint.
  3. Select Run > Debug in the menu.

The IntelliJ 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 setState() methods in your Friendlychat app, 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 will be displayed.

Implement a message list

In this section, you'll create a widget that displays users' chat messages. You'll do this using composition, by creating and combining multiple smaller widgets. Start with a widget that represents a single chat message, nest that widget in a parent scrollable list, and nest the scrollable list in the basic app scaffold.

First, we need a widget that represents a single chat message. Define a StatelessWidget called ChatMessage as follows. Its build() method returns a Row widget that displays a simple graphical avatar to represent the user who sent the message, a Column widget containing the sender's name, and the text of the message.

main.dart

// Add the following class definition to main.dart.

class ChatMessage extends StatelessWidget {
 ChatMessage({this.text});
 final String text;
 @override
 Widget build(BuildContext context) {
   return new Container(
     margin: const EdgeInsets.symmetric(vertical: 10.0),
     child: new Row(
       crossAxisAlignment: CrossAxisAlignment.start,
       children: <Widget>[
         new Container(
           margin: const EdgeInsets.only(right: 16.0),
           child: new CircleAvatar(child: new Text(_name[0])),
         ),
         new Column(
           crossAxisAlignment: CrossAxisAlignment.start,
           children: <Widget>[
             new Text(_name, style: Theme.of(context).textTheme.subhead),
             new Container(
               margin: const EdgeInsets.only(top: 5.0),
               child: new Text(text),
             ),
           ],
         ),
       ],
     ),
   );
 }
}

Define the _name variable as shown, replacing Your Name with your own name. We'll 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 will retrieve the sender's name via authentication, as shown in the Firebase for Flutter codelab.

main.dart

// Add the following code to main.dart.

const String _name = "Your Name";

To personalize the CircleAvatar widget, label it with the user's first initial by passing the first character of the _name variable's value to a child Text widget. We'll use CrossAxisAlignment.start as the crossAxisAlignment argument of the Row constructor to position the avatar and messages relative to their parent widgets.

For the avatar, the parent is a Row widget whose main axis is horizontal, so CrossAxisAlignment.start gives it the highest position along the vertical axis. For messages, the parent is a Column widget whose main axis is vertical, so CrossAxisAlignment.start aligns the text at the furthest left position along the horizontal axis.

Next to the avatar, align two Text widgets vertically to display the sender's name on top and the text of the message below. To style the sender's name and make it larger than the message text, you'll need to use Theme.of(context)to obtain an appropriate ThemeData object. Its textTheme property gives you access to Material Design logical styles for text like subhead, so you can avoid hard-coding font sizes and other text attributes.

We haven't specified a theme for this app, so Theme.of(context)retrieves the default Flutter theme. In a later step, you'll override this default theme to style your app differently for Android vs. iOS.

Implement a chat message list

The next refinement is to get the list of chat messages and show it in the UI. We want this list to be scrollable so that users can view the chat 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.

In your ChatScreenState widget definition, add a List member called _messages to represent each chat message. Each list item is a ChatMessage instance. You need to initialize the message list to an empty List.

main.dart

// Add the following code to the ChatScreenState class definition.

class ChatScreenState extends State<ChatScreen> {
  final List<ChatMessage> _messages = <ChatMessage>[];             // new
  final TextEditingController _textController = new TextEditingController();

When the current user sends a message from the text field, your app should add the new message to the message list. Modify your _handleSubmitted() method as follows to implement this behavior.

main.dart

// Modify the code in the _handleSubmitted method definition.

void _handleSubmitted(String text) {
  _textController.clear();
    ChatMessage message = new ChatMessage(                         //new
      text: text,                                                  //new
      );                                                           //new
    setState(() {                                                  //new
      _messages.insert(0, message);                                //new
    });                                                            //new
 }

You call setState()to modify _messages and to let the framework know this part of the widget tree has changed and it needs to rebuild the UI. Only synchronous operations should be performed in setState(), 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 inside setState()'s closure is preferred, so you don't forget to call it afterwards.

Place the message list

You're now ready to display the list of chat messages. We'll get the ChatMessage widgets from the _messages list and put them in a ListView widget, for a scrollable list.

In the build() method of your ChatScreenState class, add a ListView widget for the message list. We choose the ListView.builder constructor because the default constructor doesn't automatically detect mutations of its children argument.

main.dart

// Modify the code in the ChatScreenState class definition as follows.

Widget build(BuildContext context) {
  return new Scaffold(
    appBar: new AppBar(title: new Text("Friendlychat")),
    body: new Column(                                        //modified
      children: <Widget>[                                         //new
      new Flexible(                                               //new
        child: new ListView.builder(                              //new
          padding: new EdgeInsets.all(8.0),                       //new
          reverse: true,                                          //new
          itemBuilder: (_, int index) => _messages[index],        //new
          itemCount: _messages.length,                            //new
        )                                                         //new
      ),                                                          //new
      new Divider(height: 1.0),                                   //new
      new Container(                                              //new
        decoration: new BoxDecoration(
          color: Theme.of(context).cardColor),                   //new
        child: _buildTextComposer(),                         //modified
      ),                                                          //new
     ]                                                            //new
   ),                                                             //new
 );
}

The body property of the Scaffold widget now contains the list of incoming messages as well as the input field and send button. We are using the following layout widgets:

Pass arguments to the ListView.builder constructor to customize the list contents and appearance:

If you hot reload the app (), you should see a single screen that looks like this.

iOS

Android

Now, try sending a few messages using the UIs for composing and displaying that you just built!

iOS

Android

You can add animation effects to your widgets to make the user experience of your app more fluid and intuitive. In this section, we'll go over how to add a basic animation effect to your chat message list.

When the user sends a new message, instead of simply displaying it in the message list, we'll animate the message to ease out vertically from the bottom of the list.

Animations in Flutter are encapsulated as Animation objects that contain a typed value and a status (such as forward, backward, 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 class lets you define important characteristics of the animation, such as its duration and playback direction (forward or reverse).

When the AnimationController object is created, you'll need to pass it a vsync argument. This option keeps the current widget tree in display memory until Flutter's rendering engine completes its refresh cycle. To use your ChatScreenState as the vsync, include a TickerProviderStateMixin mixin in the ChatScreenState class definition.

main.dart

// Modify the code in the ChatScreenState class definition as follows.

class ChatScreenState extends State<ChatScreen> with TickerProviderStateMixin { // modified
  final List<ChatMessage> _messages = <ChatMessage>[];
  final TextEditingController _textController = new TextEditingController();

Modify the _handleSubmitted() method in your ChatScreenState class as follows. In this method, instantiate an AnimationController object and specify the animation's runtime duration to be 700 milliseconds. (We picked this longer duration period to slow down the the animation effect so you can see the transition happen more gradually; in practice, you'll probably want to set a shorter duration period and disable slow mode when running your app.)

Attach the animation controller to a new ChatMessage instance, and specify that the animation should play forward whenever a new message is added to the chat list

main.dart

// Modify the _handleSubmittted method definition as follows.

void _handleSubmitted(String text) {
  _textController.clear();
  ChatMessage message = new ChatMessage(
    text: text,
    animationController: new AnimationController(                  //new
      duration: new Duration(milliseconds: 700),                   //new
      vsync: this,                                                 //new
    ),                                                             //new
  );                                                               //new
  setState(() {
    _messages.insert(0, message);
  });
  message.animationController.forward();                           //new
}

In the ChatMessage class definition, add a member variable to store the animation controller.

main.dart

// Modify the ChatMessage class definition as follows.

class ChatMessage extends StatelessWidget {
  ChatMessage({this.text, this.animationController});         //modified
  final String text;
  final AnimationController animationController;                   //new

Add a SizeTransition widget

Modify the ChatMessageListItem object's build() method to return a SizeTransition widget which wraps the Container child widget we previously defined. The SizeTransition class provides an animation effect where the width or height of its child is multiplied by a given size factor value.

The CurvedAnimation object, in conjunction with the SizeTransition class, produces an ease-out animation effect. The ease-out effect causes the message to slide in quickly at the beginning of the animation and slow down until it comes to a stop.

main.dart

// Modify the build() method for the ChatMessage class as follows.

class ChatMessage extends StatelessWidget {
  ChatMessage({this.text, this.animationController});
  final String text;
  final AnimationController animationController;
  @override
  Widget build(BuildContext context) {
    return new SizeTransition(                                    //new
    sizeFactor: new CurvedAnimation(                              //new
      parent: animationController,                                //new
      curve: Curves.easeOut                                       //new
      ),                                                          //new
      axisAlignment: 0.0,                                         //new
      child: new Container(                                  //modified
         margin: const EdgeInsets.symmetric(vertical: 10.0),
         child: new Row(
           crossAxisAlignment: CrossAxisAlignment.start,
           children: <Widget>[
             new Container(
               margin: const EdgeInsets.only(right: 16.0),
               child: new CircleAvatar(child: new Text(_name[0])),
             ),
             new Column(
               crossAxisAlignment: CrossAxisAlignment.start,
               children: <Widget>[
                 new Text(_name, style: Theme.of(context).textTheme.subhead),
                 new Container(
                   margin: const EdgeInsets.only(top: 5.0),
                   child: new Text(text),
                 ),
               ],
             ),
           ],
         ),
       )                                                           //new
     );
   }
 }

Dispose the animation

It's good practice to dispose of your animation controllers to free up your resources when they are no longer needed. The following code snippet shows how you can implement this operation by overriding the dispose() method in ChatScreenState. In the current app, the framework does not call the dispose()method since the app only has a single screen. In a more complex app with multiple screens, the framework would invoke the method when the ChatScreenState object was no longer in use.

main.dart

// Add the following code to the ChatScreenState class definition.

@override
void dispose() {                                                   //new
  for (ChatMessage message in _messages)                           //new
    message.animationController.dispose();                         //new
  super.dispose();                                                 //new
}                                                                  //new

To see the animation effect, restart your app () and enter a few messages. Using restart rather than hot reload clears any existing messages that do not have an animation controller.

If you want to experiment further with animations, here are a few ideas to try:

In this optional step, you'll give your app a few sophisticated details, like making the Send button enabled only when there's text to send and adding native-looking customizations for iOS and Android.

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 member variable that is true whenever the user is typing in the input field.

main.dart

// Add the following code in the ChatScreenState class definition.

class ChatScreenState extends State<ChatScreen> with TickerProviderStateMixin {
  final TextEditingController _textController = new TextEditingController();
  bool _isComposing = false;                                      //new

To be notified about changes to the text as the user interacts with the field, pass an onChanged callback to the TextField constructor. TextField calls this method whenever its value changes with the current value of the field. In your onChanged callback, call setState() to change the value of _isComposing to true when the field contains some text.

Then modify the onPressed argument to be null when _isComposing is false.

main.dart

// Modify the _buildTextComposer method with the code below
// to add the onChanged() and onPressed() callbacks.

Widget _buildTextComposer() {
   return new IconTheme( 
     data: new IconThemeData(color: Theme.of(context).accentColor),
     child: new Container(
         margin: const EdgeInsets.symmetric(horizontal: 8.0),
         child: new Row(
             children: <Widget>[
               new Flexible(
                   child: new TextField(
                       controller: _textController,
                       onChanged: (String text)  {                //new
                         setState(() {                            //new
                           _isComposing = text.length > 0;        //new
                         });                                      //new
                       },                                         //new
                       onSubmitted: _handleSubmitted,
                       decoration: new InputDecoration.collapsed(
                           hintText: "Send a message"),
                       ),
                   ),
               new Container(
                   margin: new EdgeInsets.symmetric(horizontal: 4.0),
                   child: new IconButton(
                       icon: new Icon(Icons.send),
                       onPressed: _isComposing ?            // modified
                                  () =>  _handleSubmitted(_textController.text) :// modified
                                  null,                     //modified
                   ),
                 )
               ]
           ),
        )
     );
   }

Modify _handleSubmitted to update _isComposing to false when the text field is cleared.

main.dart

// Modify the _handleSubmittted method definition as follows.

void _handleSubmitted(String text) {
  _textController.clear();
  setState(() {                                                    //new
    _isComposing = false;                                          //new
  });                                                              //new
  ChatMessage message = new ChatMessage(
    text: text,
    animationController: new AnimationController(
      duration: new Duration(milliseconds: 700),
      vsync: this,
    ),
  );
  setState(() {
    _messages.insert(0, message);
  });
  message.animationController.forward();
}

The _isComposing variable now controls the behavior and the visual appearance of the Send button.

Customize for iOS and Android

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 CupertinoButton on iOS and a Material Design IconButton on Android.

iOS

Android

First, define a new ThemeData object named kIOSTheme with colors for iOS (light grey with orange accents) and another ThemeData object kDefaultTheme with colors for Android (purple with orange accents).

main.dart

// Add the following code to main.dart.

final ThemeData kIOSTheme = new ThemeData(
  primarySwatch: Colors.orange,
  primaryColor: Colors.grey[100],
  primaryColorBrightness: Brightness.light,
);

final ThemeData kDefaultTheme = new ThemeData(
  primarySwatch: Colors.purple,
  accentColor: Colors.orangeAccent[400],
);

Modify the FriendlychatApp class to vary the theme using the theme property of your app's MaterialApp widget. Use the top-level defaultTargetPlatform property and conditional operators to build an expression for selecting a theme.

main.dart

// Add the following code to main.dart.

import 'package:flutter/foundation.dart';                        //new

// Modify the FriendlychatApp class definition in main.dart.

class FriendlychatApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      title: "Friendlychat",
      theme: defaultTargetPlatform == TargetPlatform.iOS         //new
        ? kIOSTheme                                              //new
        : kDefaultTheme,                                         //new
      home: new ChatScreen(),
    );
  }
}

We can apply the selected theme to the AppBar widget (the banner at the top of your app's UI). The elevation property defines the z-coordinates of the AppBar. A z-coordinate value of 0.0 has no shadow (iOS) and a value of 4.0 has a defined shadow (Android).

main.dart

// Modify the build() method of the ChatScreenState class.

Widget build(BuildContext context) {
  return new Scaffold(
    appBar: new AppBar(
      title: new Text("Friendlychat"),                                 //modified
      elevation:
         Theme.of(context).platform == TargetPlatform.iOS ? 0.0 : 4.0, //new
   ),

Customize the Send icon by modifying its Container parent widget in the _buildTextComposer method. Use the child property and conditional operators to build an expression for selecting a button.

main.dart

// Add the following code to main.dart.

import 'package:flutter/cupertino.dart';

// Modify the _buildTextComposer method.

new Container(
   margin: new EdgeInsets.symmetric(horizontal: 4.0),
   child: Theme.of(context).platform == TargetPlatform.iOS ?  //modified
   new CupertinoButton(                                       //new
     child: new Text("Send"),                                 //new
     onPressed: _isComposing                                  //new
         ? () =>  _handleSubmitted(_textController.text)      //new
         : null,) :                                           //new
   new IconButton(                                            //modified
       icon: new Icon(Icons.send),
       onPressed: _isComposing ?
           () =>  _handleSubmitted(_textController.text) : null,
       )
   ),

Wrap the top-level Column in a Container widget to give it a light grey border on its upper edge. This border will help 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 snippet.

main.dart

// Modify the following lines in main.dart.

Widget build(BuildContext context) {
  return new Scaffold(
    appBar: new AppBar(
      title: new Text("Friendlychat"),
      elevation: Theme.of(context).platform == TargetPlatform.iOS ? 0.0 : 4.0,
      ),
    body: new Container(                                      //modified                            
      child: new Column(                                      //modified                                
        children: <Widget>[                                   
          new Flexible(                                       
            child: new ListView.builder(                    
              padding: new EdgeInsets.all(8.0),           
              reverse: true,                              
              itemBuilder: (_, int index) => _messages[index], 
              itemCount: _messages.length,                 
              ),                                            
            ),                                                   
          new Divider(height: 1.0),                            
            new Container(                                       
              decoration: new BoxDecoration(
                color: Theme.of(context).cardColor), 
                child: _buildTextComposer(),                
                ),                                               
             ],                                                      
           ),                                                         
           decoration: Theme.of(context).platform == TargetPlatform.iOS          //new
               ? new BoxDecoration(                                              //new
                   border:                                                       //new
                       new Border(top: new BorderSide(color: Colors.grey[200]))) //new
               : null),                                                          //new                                                           
   );
 }

If you hot reload the app () , you should see different colors, shadows, and icon buttons for iOS and Android.

Congratulations!

You now know the basics of building cross-platform mobile apps with the Flutter framework.

What we've covered

What's next

Continue learning about Flutter:

Add Firebase features to the Friendlychat app in the Flutter for Firebase codelab.

We recommend downloading the sample if you want 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/friendlychat-steps.git

The sample code is in the offline_steps folder. We have created snapshots for you for each step, one snapshot per directory. Each step builds on the preceding step.

The Flutter for Firebase codelab begins with the full_steps folder, and shows how to integrate your app with Firebase.