Flutter 是一个开放源代码 SDK,用于创建面向 iOS 和 Android 的高性能、高保真移动应用。Flutter 框架让您可以轻松构建在应用中反应流畅的界面,同时可减少同步和更新应用视图所需的代码量。
凭借丰富的 Material Design、Cupertino (iOS) 微件及行为,使用 Flutter 构建精美的应用非常容易上手。用户将爱上您的应用的自然外观,因为 Flutter 可实现平台特定的滚动、导航模式、字体等功能。您也会感受到 Flutter 的强大和高效,它具有功能反应式框架,并且可以极为迅速地在设备和仿真器上进行热重载。
您将采用 Dart 编写您的 Flutter 应用。如果您已了解 Java、JavaScript、C# 或 Swift,那么应该很熟悉 Dart 语法。Dart 是针对您的应用运行所需的特定移动平台使用标准 Android 和 iOS 工具链编译的。您可获得 Dart 语言的所有优势,包括既熟悉又简洁的语法、一级函数、async/await、丰富的标准内容库等。
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.
To start a new Flutter project in the IntelliJ editor:
flutter
directory, without the bin
subdirectory. For example, /Users/obiwan/flutter
.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.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!
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.)
To deploy your Flutter app to a simulator or emulator, you'll need to perform these steps:
If your Flutter app starts successfully, you should see a screen like this on your development machine:
To deploy your Flutter app to a device, you'll need to perform these steps:
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()
函数;此微件成为您的微件树的根。
// 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
以管理状态。
// 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 框架的多个主要概念:
FriendlychatApp
或 ChatScreen
调用 build()
函数。 @override
是一个 Dart 注解,其指示您的代码替换框架的抽象StatelessWidget
类的build()
函数。Scaffold
和AppBar
)是 Material Design 应用特有的微件。其他微件(如Text
)则是通用微件,可在任意应用中使用。来自 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
类:
// 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")
),
);
}
}
现在,ChatScreenState
的build
()
函数应包含以前在微件树的ChatScreen
部分中的所有微件。当框架调用build()
函数以刷新界面时,它可以使用其子微件树重新构建ChatScreenState
。
现在,您的应用有能力管理状态,您可以使用输入字段和发送按钮扩建 ChatScreenState
类。
要管理与文本字段的交互,使用一个TextEditingController
对象会很有帮助。您将用它来读取输入字段的内容,并在发送文本消息后清除字段。向ChatScreenState
类定义添加一行以创建此对象。
// Add the following code in the ChatScreenState class definition.
class ChatScreenState extends State<ChatScreen> {
final TextEditingController _textController = new TextEditingController(); //new
以下代码段演示如何定义一个名为_buildTextComposer()
的专用函数,其返回一个Container
微件和一个已配置的TextField
微件。
// 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
微件并按如下方式配置它以管理用户交互:
TextField
构造函数提供一个 TextEditingController
。此控制器还可用于清除字段或读取字段的值。onSubmitted
参数提供一个专用回调函数_handleSubmitted()
。目前,此函数将只清除字段,稍后,我们将向代码添加更多内容以发送消息。按如下方式定义此函数:// Add the following code in the ChatScreenState class definition.
void _handleSubmitted(String text) {
_textController.clear();
}
现在,指示应用如何显示文本输入用户控件。在您的ChatScreenState
类的build()
函数中,向body
属性附加一个名为 _buildTextComposer
的专用函数。_buildTextComposer
函数返回一个封装文本输入字段的微件。
// 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"按钮。由于我们想要显示与输入字段相邻的按钮,因此,我将使用一个Row
微件作为父微件。
然后,将TextField
微件封装在一个Flexible
微件中。这将指示Row
自动调整文本字段的大小以使用该按钮未使用的剩余空间。
// 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
向其传递消息的内容。
// 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
对象。这将为按钮(以及此部分微件树中的任何其他图标)添加当前主题背景的强调色。
// 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.build
或State.build
函数返回的微件的父微件。这表示_buildTextComposer()
函数可以从其封装State
对象访问BuildContext
对象;您不需要显式地将此上下文传递到该函数。
如果您热重载应用 (),您应会看到如下屏幕。
iOS | Android |
您可以通过 IntelliJ IDE 调试模拟器/仿真器或设备上运行的 Flutter 应用。借助 IntelliJ 编辑器,您可以:
IntelliJ 编辑器在您的应用运行时显示系统日志,并提供一个 Debugger 界面处理断点和控制执行流。
要使用断点调试您的 Flutter 应用:
在 IntelliJ 编辑器中启动 Debugger 界面,当它到达断点时暂停应用的执行。然后,您可以使用 Debugger 界面中的控件来确定错误的原因。
可通过在 Friendlychat 应用中的 build()
函数上设置断点来练习使用调试程序,然后运行和调试应用。您可以检查堆栈框架以查看您的应用的函数调用历史记录。
准备好基本的应用框架和屏幕后,现在,您准备定义将显示聊天消息的区域。
在此部分中,您将创建一个显示用户聊天消息的微件。可使用组合来完成此操作,只需创建和合并多个较小的微件即可。从表示单个聊天消息的微件开始,将该微件嵌套在一个父级可滚动列表中,并将可滚动列表嵌套在基本的应用框架中。
首先,我们需要一个表示单个聊天消息的微件。按如下方式定义一个名为ChatMessage
的StatelessWidget
。它的build()
函数将返回一个Row
微件(其显示一个简单的图形头像来表示发送消息的用户)和一个包含发信人姓名及消息文本的Column
微件。
// 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 中所示。
// 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
微件定义中,添加一个名为_messages
的List
成员以表示每条聊天消息。每个列表项都是一个ChatMessage
实例。您需要将消息列表初始化为一个空List
。
// 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()
函数以实现此行为。
// 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
构造函数,因为默认构造函数不会自动检测其�
��参数的突变。
// 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
属性包含传入的消息列表以及输入字段和发送按钮。我们将使用以下布局微件:
Column
,垂直布置其直接子微件。Column
可以占用多个子微件,包括滚动列表和输入字段的行。Flexible
,可作为ListView
的父微件。这将指示框架允许已接收的消息列表进行扩展以填充Column
高度,而TextField
则保持固定大小。Divider
,在用于显示消息的界面和用于撰写消息的输入字段之间绘制一条水平线。Container
,可作为文本合成器的父微件,其可用于定义背景图片、内边距、外边距和其他常见的布局细节。使用decoration
创建一个定义背景色的新BoxDecoration
对象。在此情况下,我们使用由默认主题背景的cardColor
对象定义的ThemeData
。这将使用于撰写消息的界面与消息列表有不同的背景。将参数传递到ListView.builder
构造函数以自定义列表内容和外观:
padding
,用于定义消息文本周围的空间reverse
,可使ListView
从屏幕底部开始itemCount
,可指定列表中的消息数itemBuilder
,用于定义在[index]
中构建每个微件的函数。由于我们不需要当前的构建上下文,因此,我们可以忽略IndexedWidgetBuilder
的第一个参数。给参数 _
(下划线)命名通常表明不会使用该参数。如果您热重载应用 (),您应看到一个类似下面这样的屏幕。
iOS | Android |
现在,尝试使用您刚刚构建的用于撰写和显示的界面发送几条消息!
iOS | Android |
您可以向微件添加动画效果,以使应用的用户体验更流畅和直观。在此部分中,我们将介绍如何向聊天消息列表添加基本动画效果。
当用户发送新消息时,我们将为消息添加动画,使其从列表底部垂直缓出,而不是将其简单地显示在消息列表中。
Flutter 中的动画是以包含键入值和状态(如前进、后退、已完成和已关闭)的Animation
对象形式封装的。您可以为微件附加动画对象,或侦听动画对象的变化。根据对动画对象属性进行的更改,框架可以修改微件的显示方式,并重新构建微件树。
使用AnimationController
类可指定如何运行动画。您可以通过AnimationController
类定义动画的重要特性,如持续时间和播放方向(正向或反向)。
创建AnimationController
对象后,您需要向其传递一个vsync
参数。vsync
可防止处于屏幕之外的动画消耗不必要的资源。要使用ChatScreenState
作为vsync
,请在ChatScreenState
类定义中添加一个TickerProviderStateMixin
mixin。
// 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
类定义中,添加一个成员变量以存储动画控制器。
// 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
实例,并指定将新消息添加到聊天列表时该动画应向前播放。
// 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
}
修改ChatMessage
对象的build()
函数以返回一个SizeTransition
微件,其封装我们以前定义的Container
子微件。SizeTransition
类提供了一个动画效果,其子项的宽度和高度将与给定的大小系数值相乘。
CurvedAnimation
对象与SizeTransition
类结合使用可生成一个缓出动画效果。缓出动画效果可使消息在动画开始时快速滑入,然后慢下来,直至停止。
// 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
对象时,框架将调用此函数。
// 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
要查看动画效果,请重新启动您的应用 () 并输入几条消息。使用重新启动而不是热重载,因为这样可清除任何没有动画控制器的现有消息。
如果您想进行更多动画实验,可尝试以下几个想法:
_handleSubmitted()
函数中指定的duration
值加速或放慢动画效果。Curves
类中定义的常量指定不同的动画曲线。Container
封装在一个FadeTransition
微件中(而不是SizeTransition
)来创建淡入动画效果。在这个可选步骤中,您将为您的应用提供一些复杂的细节,如仅当有要发送的文本时才启用"Send"按钮,以及为 iOS 和 Android 添加本机外观自定义。
目前,即使输入字段中没有文本,也会启用"Send"按钮。您可能需要按钮的外观根据字段是否包含要发送的文本而发生变化。
定义 _isComposing
,一个专用成员变量,当用户在输入字段中进行输入时该变量为 true。
// 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
。
// 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。
// 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 按钮的行为和视觉外观。
_isComposing
为 true
,且按钮的颜色被设置为 Theme.of(context).accentColor
。当用户按发送按钮时,系统将调用 _handleSubmitted()
。_isComposing
为false
,且微件的onPressed
属性被设置为null
,从而将停用发送按钮。框架会自动将发送按钮的颜色更改为Theme.of(context).disabledColor
。为给您的应用界面提供自然的外观,您可以向FriendlychatApp
类的build()
函数添加一个主题背景和一些简单的逻辑。在此步骤中,您将定义一个平台主题背景,其应用一组不同的主要颜色和强调色。您还可以自定义"Send"按钮,以便在 iOS 上使用CupertinoButton
,在 Android 上则使用 Material Design IconButton
。
iOS | Android |
首先,定义一个名为kIOSTheme
的新ThemeData
对象,其采用 iOS 颜色(浅灰色,强调色为橙色),然后定义另一个ThemeData
对象kDefaultTheme
,其采用 Android 颜色(紫色,强调色为橙色)。
// 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
属性和条件运算符构建一个用于选择主题背景的表达式。
// 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)。
// 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
属性和条件运算符构建用于选择按钮的表达式。
// 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 上的边框,在前面的代码段中应用与应用栏相同的逻辑。
// 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 集成。