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

What would you like to learn from this codelab?

I'm new to the topic, and I want a good overview. I know something about this topic, but I want a refresher. I'm looking for example code to use in my project. I'm looking for an explanation of something specific.

Create a simple templated Flutter app. You modify this starter app to create the finished app.

Launch Android Studio.

  1. 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.
  2. Select Flutter Application as the project type, and click Next.
  3. Verify that the Flutter SDK path specifies the SDK's location. (Select Install SDKif the text field is blank.)
  4. Enter FriendlyChat as the project name, and click Next.
  5. Use the default package name suggested by Android Studio, and click Next.
  6. Click Finish.
  7. Wait for Android Studio to install the SDK and create the project.

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:

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

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:

  1. Inside main(), place the cursor in front of the M in MaterialApp.
  2. Right-click, and select Refactor > Extract > Extract Flutter widget.

  1. Enter FriendlyChatApp into the ExtractWidget dialog, and click the Refactor button. The MaterialApp code is placed in a new stateless widget called FriendlyChatApp, and main() is updated to call that class when it calls the runApp() function.
  2. Select the block of text after home:. Start with Scaffold( and end with the Scaffold's closing parenthesis, ). Do not include the ending comma.
  3. Start typing ChatScreen, and select ChatScreen() from the popup. (Choose the ChatScreen 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:

  1. Under the FriendlyChatApp class, around line 27, start typing stless. The editor asks if you want to create a Stateless widget. Press Return to accept. The boilerplate code appears, and the cursor is positioned for you to enter the name of your stateless widget.
  2. Enter ChatScreen.

Update the ChatScreen widget:

  1. Inside the ChatScreen widget, select Container, and start typing Scaffold. Select Scaffold from the popup.
  2. The cursor should be positioned inside the parentheses. Press Return to start a new line.
  3. Start typing appBar, and select appBar: from the popup.
  4. After appBar:, start typing AppBar, and select the AppBar class from the popup.
  5. Within the parentheses, start typing title, and select title: from the popup.
  6. After title:, start typing Text, and select the Text class.
  7. The boilerplate code for Text contains the word data. Delete the first comma after data. Select data, 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:

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:

  1. Select ChatScreen in the line class ChatScreen extends StatelessWidget.
  2. Press Option+Return (macOS) or Alt+Enter (Linux and Windows) to bring up a menu.
  3. 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

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

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:

  1. Select TextField in _buildTextComposer.
  2. Press Option+Return (macOS) or Alt+Enter (Linux and Windows) to bring up a menu, and select Wrap with widget. A new widget is added that wraps the TextField. The placeholder name is selected, and the IDE waits for you to enter a new placeholder name.
  3. Start typing Row, and select Row from the list that appears. A popup appears containing the definition for the Row's constructor. The child property has a red border, and the analyzer tells you that you are missing the required children property.
  4. Hover over child and a popup appears. In the popup, it asks if you want to change the property to children. Select that option.
  5. 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 the children: text. The editor also provides the closing right bracket. 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.
  6. 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:

  1. Select Row.
  2. Press Option+Return (macOS) or Alt+Enter (Linux and Windows) to bring up a menu, and select Wrap with widget. A new widget is added that wraps the TextField. The placeholder name is selected, and the IDE waits for you to enter a new placeholder name.
  3. Start typing Flexible, and select Flexible from the list that appears. A popup appears containing the definition for the Row'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

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.

  1. Position the cursor at the end of the Flexible widget's closing right bracket and comma, and press Return to start a new line.
  2. 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.
  3. 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

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 IconThem:.

  1. Select Container at the top of the _buildTextComposer() function.
  2. Press Option+Return (macOS) or Alt+Enter (Linux and Windows) to bring up a menu, and select Wrap with widget. A new widget is added that wraps the Container. The placeholder name is selected, and the IDE waits for you to enter a new placeholder name.
  3. Start typing Row, and select IconTheme from the list. The child property is surrounded by a red box, and the analyzer tells you that the data property is required.
  4. Add the data property:
return IconTheme(
  data: IconThemeData(color: Theme.of(context).accentColor), // NEW
  child: Container(                                    

Observations

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:

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:

  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 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.
  3. If you weren't running in debug mode, stop the app.
  4. 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:

  1. Position the cursor after the FriendlyChatApp class and start to type stless. (The order of the classes doesn't matter, but this order makes it easier to compare your code to the solution.)
  2. Enter ChatMessage for the class name.

Add a Row to the build() method for ChatMessage:

  1. Position the cursor inside the parentheses in return Container(), and press Return to start a new line.
  2. Add a margin property:
margin: EdgeInsets.symmetric(vertical: 10.0),
  1. The Container's child will be a Row. The Row'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),
          ),
        ],
      ),
    ],
  ),
);
  1. Add a text variable and a constructor to the top of ChatMessage:
class ChatMessage extends StatelessWidget {
  ChatMessage({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

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.

  1. Add a FocusNode to ChatScreenState:
class _ChatScreenState extends State<ChatScreen> {
  final List<ChatMessage> _messages = [];
  final _textController = TextEditingController();
  final FocusNode _focusNode = FocusNode();    // NEW
  1. Add the focusNode property to the TextField in _buildTextComposer():
child: TextField(
  controller: _textController,
  onSubmitted: _handleSubmitted,
  decoration: InputDecoration.collapsed(hintText: 'Send a message'),
  focusNode: _focusNode,  // NEW
),
  1. In _handleSubmitted(), after the call to setState(), request focus on the TextField:
    setState(() {
      _messages.insert(0, message);
    });
    _focusNode.requestFocus();  // NEW

Observations

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

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({this.text, this.animationController}); // MODIFIED
  final String text;
  final AnimationController animationController;      // NEW
  ...

Add an animation controller to the _handleSubmitted() method:

void _handleSubmitted(String text) {
  _textController.clear();
  ChatMessage 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

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:

  1. In the build() method for ChatMessage, select the first Container instance.
  2. Press Option+Return (macOS) or Alt+Enter (Linux and Windows) to bring up a menu, and select Wrap with widget.
  3. Enter SizeTransition. A red box appears around the child: property. This indicates that a required property is missing from the widget class. Hover over SizeTransition, and a tooltip points out that sizeFactor is required and offers to create it. Choose that option, and the property appears with a null value.
  4. Replace null with an instance CurvedAnimation. This adds the boilerplate code for two properties: parent (required) and curve.
  5. For the parent property, replace null with the animationController.
  6. For the curve property, replace null with Curves.easeOut, one of the constants from the Curves class.
  7. Add a line after sizeFactor (but at the same level), and enter an axisAlignment property to the SizeTransition, 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

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 (ChatMessage 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:

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();
  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.length > 0;   // 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

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:

  1. In the build() method for ChatMessage, select the Column widget inside the Row for the Container.
  2. Press Option+Return (macOS) or Alt+Enter (Linux and Windows) to bring up a menu.
  3. 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

Modify the FriendlyChatApp class to vary the theme using the theme property of your app's MaterialApp widget:

  1. Import the foundation package at the top of the file:
import 'package:flutter/foundation.dart';  // NEW
import 'package:flutter/material.dart';
  1. 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).

  1. In the build() method of ChatScreenState, find the following line of code:
      appBar: AppBar(title: Text('FriendlyChat')),
  1. Place the cursor between the two right parentheses ())), type a comma, and press Return to start a new line.
  2. Add the following two lines:
elevation:
   Theme.of(context).platform == TargetPlatform.iOS ? 0.0 : 4.0, // NEW
  1. Right-click in the code pane, and select Reformat code with dartfmt.

Observations

Customize the send icon for Android and for iOS.

  1. 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';
  1. In ChatScreenState's _buildTextComposer()method, modify the line that assigns an IconButton as the child of the Container. Change the assignment to be conditional on the platform. For iOS, use a CupertinoButton; otherwise, stay with an IconButton:
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:

  1. In _ChatScreenState's build() method, select the Column that appears after body:.
  2. Press Option+Return (macOS) or Alt+Enter (Linux and Windows) to bring up a menu, and select Wrap with Container.
  3. After the end of that Column, but before the end of the Container, 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

What's next

Try one of the other Flutter codelabs.

Continue learning about Flutter:

For more information about keyboard shortcuts:

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/friendlychat-steps.git

The sample code is in the steps folder. Only the modified files are included, so the pubspec.yaml file in step-0 is used for all the following steps. You can also drop the code from the main.dart file from any of these steps into a DartPad instance and run them from there.