Flutter 是一个开放源代码 SDK,用于创建面向 iOS 和 Android 的高性能、高保真移动应用。Flutter 框架让您可以轻松构建在应用中反应流畅的界面,同时可减少同步和更新应用视图所需的代码量。

凭借丰富的 Material Design、Cupertino (iOS) 微件及行为,使用 Flutter 构建精美的应用非常容易上手。用户将爱上您的应用的自然外观,因为 Flutter 可实现平台特定的滚动、导航模式、字体等功能。您也会感受到 Flutter 的强大和高效,它具有功能反应式框架,并且可以极为迅速地在设备和仿真器上进行热重载。

您将采用 Dart 编写您的 Flutter 应用。如果您已了解 Java、JavaScript、C# 或 Swift,那么应该很熟悉 Dart 语法。Dart 是针对您的应用运行所需的特定移动平台使用标准 AndroidiOS 工具链编译的。您可获得 Dart 语言的所有优势,包括既熟悉又简洁的语法一级函数async/await丰富的标准内容库等。

您将学习的内容

您在构建移动应用方面处于何种水平?

从未构建过移动应用 仅构建过适用于移动网络的应用 仅构建过适用于 Android 的应用 仅构建过适用于 iOS 的应用 构建过适用于 Android 和 iOS 的应用 构建过适用于移动网络、Android 和 iOS 的应用

Prerequisites

To start developing mobile apps with Flutter you need:

Flutter's IDE tools are available for Android Studio, IntelliJ IDEA Community (free), and IntelliJ IDEA Ultimate.

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. If an AVD already exists, you can start the emulator directly from the device selector in IntelliJ, as shown in the next step.
  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.

在此部分中,您首先要将默认示例应用修改为一个聊天应用。目标是使用 Flutter 构建一个可扩展的简单聊天应用 Friendlychat,它具有以下功能:

iOS

Android

创建主应用框架

您将添加的第一个元素是一个简单的应用栏,其显示应用的静态标题。随着您继续学习此 Codelab 的后续部分,您将逐步向应用添加更多自适应的有状态界面元素。

main.dart文件位于您的 Flutter 项目中的lib目录下,并包含main()函数,从它开始执行您的应用。

main()runApp()函数定义与默认应用中的相同。runApp()函数将Widget(在运行时,Flutter 框架将扩展该微件并在应用的屏幕上显示它)作为自己的参数。由于应用在界面中使用 Material Design 元素,因此,创建一个新的MaterialApp对象并将其传递到runApp()函数;此微件成为您的微件树的根。

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"),
        ),
      ),
    ),
  );
}

要指定用户在您的应用中看到的默认屏幕,请在MaterialApp定义中设置home参数。home参数引用一个定义此应用的主界面的微件。该微件包含一个Scaffold微件,它将一个简单的AppBar作为其子微件。

如果您运行应用 (),您应会看到类似于如下屏幕。

iOS

Android

构建聊天屏幕

为给交互式组件打下基础,您可以将简单应用分成两个不同的微件子类:一个永远不会发生变化的根级FriendlychatApp微件,以及一个在发送消息和内部状态发生变化时可以重新构建的子级ChatScreen微件。目前,这两个类都可以扩展StatelessWidget。稍后,我们将调整ChatScreen以管理状态。

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")),
    );
  }
}

此步骤介绍 Flutter 框架的多个主要概念:

随着您继续更改和优化应用的界面,您可以快速查看结果,无需完全重启应用。使用 Flutter 的热重载功能将更新的源文件注入运行的 Dart 虚拟机,然后刷新界面。热重载是一个强大的工具,可应用于实验、原型开发和迭代。

如果您在将界面分成单独的类后点击"Hot Reload"按钮 (),您将看不到任何变化:

iOS

Android

在此部分中,您将学习如何构建让用户可以输入和发送文本消息的用户控件。

在设备上,点击文本字段将调出一个软键盘。用户可以通过输入一个非空字符串并按软键盘上的 Return 键发送聊天消息。或者,用户可以通过按输入字段旁的 Send 图形按钮发送他们键入的消息。

目前,用于撰写消息的界面位于聊天屏幕的顶部,但我们在下一步中添加用于显示消息的界面后,它将移至聊天屏幕的底部。

添加一个交互式文本输入字段

Flutter 框架提供一个名为TextField的 Material Design 微件。它是一个有状态微件(一个具有可变状态的微件),具有用于自定义输入字段行为的属性。状态是构建微件时可以同步读取的信息,而该信息在微件的生命周期期间可能会发生变化。向 Friendlychat 添加第一个有状态微件需要进行几个修改。

在 Flutter 中,如果您要以视觉方式呈现微件中有状态的数据,您应将此数据封装在一个State对象中。然后,您可以将State对象与扩展StatefulWidget类的微件进行关联。

以下代码段向您演示如何开始在main.dart文件中开始定义一个类,以添加交互式文本输入字段。首先,您应将ChatScreen类更改为子类StatefulWidget,而不是StatelessWidget。当TextField处理可变文本内容时,状态位于此级别的微件层次结构,因为ChatScreen将具有一个文本控制器对象。您还将定义一个可实现State对象的新ChatScreenState类。

按以下所示替换createState()函数以附加ChatScreenState类。您将使用新类构建有状态的TextField微件。

build()函数上方添加一行以定义ChatScreenState类:

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")
      ),
    );
  }
}

现在,ChatScreenStatebuild()函数应包含以前在微件树的ChatScreen部分中的所有微件。当框架调用build()函数以刷新界面时,它可以使用其子微件树重新构建ChatScreenState

现在,您的应用有能力管理状态,您可以使用输入字段和发送按钮扩建 ChatScreenState 类。

要管理与文本字段的交互,使用一个TextEditingController对象会很有帮助。您将用它来读取输入字段的内容,并在发送文本消息后清除字段。向ChatScreenState类定义添加一行以创建此对象。

main.dart

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

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

以下代码段演示如何定义一个名为_buildTextComposer()的专用函数,其返回一个Container微件和一个已配置的TextField微件。

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"),
    ),
  );
}

Container微件开始,在屏幕边缘和输入字段的每侧之间添加一个水平外边距。此处的单位为逻辑像素,可根据设备的像素比转换为特定数量的物理像素。您可能知道其等效术语,即 iOS 的点数或 Android 的密度无关像素

添加一个TextField微件并按如下方式配置它以管理用户交互:

main.dart

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

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

放置文本合成器微件

现在,指示应用如何显示文本输入用户控件。在您的ChatScreenState类的build()函数中,向body属性附加一个名为 _buildTextComposer的专用函数。_buildTextComposer函数返回一个封装文本输入字段的微件。

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
   );
 }

如果您重新启动应用 (),您应看到一个类似下面这样的屏幕。

iOS

Android

从无状态微件更改为有状态微件需要重新启动应用。如需了解可以热重载的变更类型的详情,请参阅 Flutter IntelliJ 文档

添加一个自适应"Send"按钮

接下来,我们将在文本字段的右侧添加一个"Send"按钮。由于我们想要显示与输入字段相邻的按钮,因此,我将使用一个Row微件作为父微件。

然后,将TextField微件封装在一个Flexible微件中。这将指示Row自动调整文本字段的大小以使用该按钮未使用的剩余空间。

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
  );
}

现在,您可以创建一个显示Send图标的IconButton微件。在icon属性中,使用Icons.send常量创建一个新的Icon实例。此常量表明您的微件使用由 Material 图标内容库提供的以下"Send"图标。

将您的IconButton微件置于另一个Container父微件中;这让您可以自定义按钮的外边距间距,以使按钮在视觉上更适合与输入字段相邻。对于onPressed属性,使用一个匿名函数也会调用_handleSubmitted()函数,并使用_textController向其传递消息的内容。

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
      ]
    ),
  );
}

默认 Material Design 主题背景的按钮颜色为黑色。要给您的应用中的图标添加强调色,您可以应用一个不同的主题背景。

图标的颜色、不透明度和大小继承自IconTheme微件,其使用一个IconThemeData对象定义这些特性。将_buildTextComposer()函数中的所有微件封装在一个IconTheme微件中,并使用其data属性指定当前主题背景的ThemeData对象。这将为按钮(以及此部分微件树中的任何其他图标)添加当前主题背景的强调色。

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
 );
}

BuildContext对象是微件在应用的微件树中的位置的句柄。每个微件都有自己的BuildContext,其成为StatelessWidget.buildState.build函数返回的微件的父微件。这表示_buildTextComposer()函数可以从其封装State对象访问BuildContext对象;您不需要显式地将此上下文传递到该函数。

如果您热重载应用 (),您应会看到如下屏幕。

iOS

Android

使用 IntelliJ 调试您的应用

您可以通过 IntelliJ IDE 调试模拟器/仿真器或设备上运行的 Flutter 应用。借助 IntelliJ 编辑器,您可以:

IntelliJ 编辑器在您的应用运行时显示系统日志,并提供一个 Debugger 界面处理断点和控制执行流。

使用断点

要使用断点调试您的 Flutter 应用:

  1. 打开您要在其中设置断点的源文件。
  2. 查找您要设置断点的行,点击该行,然后在菜单中选择 Run > Toggle Line Breakpoint。或者,您也可以点击边线(位于行编号右侧)来切换断点。
  3. 在菜单中选择 Run > Debug

在 IntelliJ 编辑器中启动 Debugger 界面,当它到达断点时暂停应用的执行。然后,您可以使用 Debugger 界面中的控件来确定错误的原因。

可通过在 Friendlychat 应用中的 build() 函数上设置断点来练习使用调试程序,然后运行和调试应用。您可以检查堆栈框架以查看您的应用的函数调用历史记录。

准备好基本的应用框架和屏幕后,现在,您准备定义将显示聊天消息的区域。

实现消息列表

在此部分中,您将创建一个显示用户聊天消息的微件。可使用组合来完成此操作,只需创建和合并多个较小的微件即可。从表示单个聊天消息的微件开始,将该微件嵌套在一个父级可滚动列表中,并将可滚动列表嵌套在基本的应用框架中。

首先,我们需要一个表示单个聊天消息的微件。按如下方式定义一个名为ChatMessageStatelessWidget。它的build()函数将返回一个Row微件(其显示一个简单的图形头像来表示发送消息的用户)和一个包含发信人姓名及消息文本的Column微件。

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),
              ),
            ],
          ),
        ],
      ),
    );
  }
}

按如下所示定义 _name 变量,将 Your Name 替换为您自己的名字。我们将使用此变量为带有发信人姓名的每条聊天消息添加标签。为简单起见,在此 Codelab 中,您可以对该值进行硬编码,但大多数应用将通过身份验证检索发信人的姓名,如 Firebase for Flutter Codelab 中所示。

main.dart

// Add the following code to main.dart.

const String _name = "Your Name";

要打造个性化的CircleAvatar微件,可使用用户的姓名首字母大写作为其标签,只需将_name变量值的第一个字符传递到一个子Text微件即可。我们将使用CrossAxisAlignment.start作为Row构造函数的crossAxisAlignment参数,以确定头像和消息相对于其父微件的位置。

对于头像,父微件是一个Row微件,其主轴是水平的,因此,CrossAxisAlignment.start可为其提供沿垂直轴的最高位置。对于消息,父微件是一个Column微件,其主轴是垂直的,因此CrossAxisAlignment.start将沿水平轴的最左侧位置对齐文本。

在头像旁,垂直对齐两个Text微件,以在顶部显示发信人的姓名和在下方显示消息文本。要设置发信人姓名的样式,并使其大于消息文本,您需要使用Theme.of(context)来获取相应的ThemeData对象。该对象的textTheme属性让您可以访问subhead之类的文本的 Material Design 逻辑样式,因此,您可以避免对字体大小和其他文本属性进行硬编码。

我们尚未指定此应用的主题背景,因此,Theme.of(context) 将检索默认的 Flutter 主题背景。在稍后的步骤中,您将替换此默认主题背景以针对 Android 和 iOS 采用不同方式设置您的应用的样式。

实现聊天消息列表

下一个优化是获取聊天消息的列表并在界面中显式它。我们需要此列表可滚动,以便用户可以查看聊天历史记录。此列表还会按时间顺序显式消息,最新消息显示在可见列表最下面的行上。

在您的ChatScreenState微件定义中,添加一个名为_messagesList成员以表示每条聊天消息。每个列表项都是一个ChatMessage实例。您需要将消息列表初始化为一个空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();

当前用户从文本字段发送消息时,您的应用应将新消息添加到消息列表。按如下方式修改您的 _handleSubmitted() 函数以实现此行为。

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
 }

您调用setState()以修改_messages,并让框架了解此部分微件树已发生变化,且需要重新构建界面。在setState()中应仅执行同步操作,否则,在操作完成前,此框架会重新构建微件。


一般情况下,在此函数调用之外的某些专用数据发生变化后,可以使用一个空的闭包调用setState()。不过,最好更新setState()的闭包中的数据,因此,随后别忘了调用它。

放置消息列表

现在,您可以准备显示聊天消息列表。我们将从_messages列表获取ChatMessage微件,并将其置于ListView微件中以实现可滚动列表。

ChatScreenState类的build()函数中,添加一个ListView微件以获取消息列表。我们选择ListView.builder构造函数,因为默认构造函数不会自动检测其参数的突变。

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
 );
}

现在,Scaffold微件的body属性包含传入的消息列表以及输入字段和发送按钮。我们将使用以下布局微件:

将参数传递到ListView.builder构造函数以自定义列表内容和外观:

如果您热重载应用 (),您应看到一个类似下面这样的屏幕。

iOS

Android

现在,尝试使用您刚刚构建的用于撰写和显示的界面发送几条消息!

iOS

Android

您可以向微件添加动画效果,以使应用的用户体验更流畅和直观。在此部分中,我们将介绍如何向聊天消息列表添加基本动画效果。

当用户发送新消息时,我们将为消息添加动画,使其从列表底部垂直缓出,而不是将其简单地显示在消息列表中。

Flutter 中的动画是以包含键入值和状态(如前进、后退、已完成已关闭)的Animation对象形式封装的。您可以为微件附加动画对象,或侦听动画对象的变化。根据对动画对象属性进行的更改,框架可以修改微件的显示方式,并重新构建微件树。

指定动画控制器

使用AnimationController类可指定如何运行动画。您可以通过AnimationController类定义动画的重要特性,如持续时间和播放方向(正向或反向)。

创建AnimationController对象后,您需要向其传递一个vsync参数。vsync可防止处于屏幕之外的动画消耗不必要的资源。要使用ChatScreenState作为vsync,请在ChatScreenState类定义中添加一个TickerProviderStateMixin mixin。

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();

ChatMessage 类定义中,添加一个成员变量以存储动画控制器。

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

按如下方式在ChatScreenState类中修改_handleSubmitted()函数。在此函数中,实例化一个AnimationController对象,并将动画的运行时持续时间指定为 700 毫秒。(我们选择这个较长的持续时间以放缓动画效果,因此,您可以看到转换是逐渐完成的;实际上,您可能需要设置一个较短的持续时间,并在运行应用时停用缓慢模式。)

将动画控制器附加到一个新的 ChatMessage 实例,并指定将新消息添加到聊天列表时该动画应向前播放。

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
}

添加 SizeTransition 微件

修改ChatMessage对象的build()函数以返回一个SizeTransition微件,其封装我们以前定义的Container子微件。SizeTransition类提供了一个动画效果,其子项的宽度和高度将与给定的大小系数值相乘。

CurvedAnimation对象与SizeTransition类结合使用可生成一个缓出动画效果。缓出动画效果可使消息在动画开始时快速滑入,然后慢下来,直至停止。

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
     );
   }
 }

丢弃动画

当不再需要动画时,最好丢弃动画控制器以释放资源。以下代码段演示如何通过替换ChatScreenState中的dispose()函数实现此操作。在当前应用中,由于应用只有一个屏幕,因此,框架不会调用dispose()函数。在具有多个屏幕的较复杂的应用中,当不再使用ChatScreenState对象时,框架将调用此函数。

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

要查看动画效果,请重新启动您的应用 () 并输入几条消息。使用重新启动而不是热重载,因为这样可清除任何没有动画控制器的现有消息。

如果您想进行更多动画实验,可尝试以下几个想法:

在这个可选步骤中,您将为您的应用提供一些复杂的细节,如仅当有要发送的文本时才启用"Send"按钮,以及为 iOS 和 Android 添加本机外观自定义。

将"Send"按钮设置为情境感知

目前,即使输入字段中没有文本,也会启用"Send"按钮。您可能需要按钮的外观根据字段是否包含要发送的文本而发生变化。

定义 _isComposing,一个专用成员变量,当用户在输入字段中进行输入时该变量为 true。

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

要在用户与字段交互时收到有关文本变更的通知,请向 TextField 构造函数传递一个onChanged回调。TextField在其值随着字段的当前值发生变化时将调用此函数。在onChanged回调中,调用setState()以便在字段包含一些文本时将_isComposing的值更改为 true。

然后,当 _isComposing 为 false 时,将 onPressed 参数修改为 null

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
                   ),
                 )
               ]
           ),
        )
     );
   }

已清除文本字段时,修改 _handleSubmitted 以将 _isComposing 更新为 false。

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();
}

现在,_isComposing 变量控制 Send 按钮的行为和视觉外观。

针对 iOS 和 Android 进行自定义

为给您的应用界面提供自然的外观,您可以向FriendlychatApp类的build()函数添加一个主题背景和一些简单的逻辑。在此步骤中,您将定义一个平台主题背景,其应用一组不同的主要颜色和强调色。您还可以自定义"Send"按钮,以便在 iOS 上使用CupertinoButton,在 Android 上则使用 Material Design IconButton

iOS

Android

首先,定义一个名为kIOSTheme的新ThemeData对象,其采用 iOS 颜色(浅灰色,强调色为橙色),然后定义另一个ThemeData对象kDefaultTheme,其采用 Android 颜色(紫色,强调色为橙色)。

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],
);

使用应用的MaterialApp微件的theme属性修改FriendlychatApp类,以改变主题背景。使用顶级defaultTargetPlatform属性和条件运算符构建一个用于选择主题背景的表达式。

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(),
    );
  }
}

我们可以将选择的主题背景应用到AppBar微件(您的应用界面顶部的横幅)。elevation属性定义AppBar的 z 坐标。0.0的 z 坐标值没有阴影 (iOS),4.0的值有定义的阴影 (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
   ),

可通过在_buildTextComposer函数中修改其Container父微件来自定义"Send"按钮。使用child属性和条件运算符构建用于选择按钮的表达式。

main.dart

// Add the following code to main.dart.

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

// 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,
       )
   ),

将顶级Column封装在一个Container微件中,以在其上方边缘添加一个浅灰色的边框。此边框有助于在视觉上将 iOS 上的应用栏与应用正文区分开来。要隐藏 Android 上的边框,在前面的代码段中应用与应用栏相同的逻辑。

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                                                           
   );
 }

如果您热重载应用 (),对于 iOS 和 Android,您应该会看到不同的颜色、阴影及图标按钮。

恭喜您!

现在,您已掌握了使用 Flutter 框架构建跨平台移动应用的基础知识。

我们已经阐述的内容

后续计划

继续学习 Flutter:

Flutter for Firebase Codelab 中向 Friendlychat 应用添加 Firebase 功能。

如果您要查看示例作为参考,或在特定部分启动 Codelab,我们建议下载此示例。要获取 Codelab 的示例代码副本,请从您的终端运行以下命令:

 git clone https://github.com/flutter/friendlychat-steps.git

示例代码位于 offline_steps 文件夹中。我们已针对每个步骤为您创建快照,每个目录一个快照。每个步骤都以前面的步骤为基础。

Flutter for Firebase Codelab 从 full_steps 文件夹开始,介绍如何将您的应用与 Firebase 集成。