不知不觉,进阶的教程已经写了几十篇了,通过前面的学习,大家已经打下了良好的基础,接下来我们就开始进行项目实战吧!
我们现在要写一个叫“谈天说地”的应用程序,这是一个简单、可扩展的聊天应用程序,能实时显示信息,用户可以输入文本信息,也可以通过按返回键或发送图标发送,还可以在iOS和Android设备上运行。
首先我们要在IntelliJ编辑器中启动一个新的Flutter项目:
启动IntelliJ IDEA。
选择Create New Project > Flutter
,如果项目已经打开,则选择File > New > Project... > Flutter
。这是flutter的顶级目录,例如/Users/obiwan/flutter
。
输入或浏览到Flutter SDK目录,然后单击Next。
命名您的项目,例如talk_casually
。推荐使用全小写字母命名,并使用下划线字符作为分隔符,第一个字符必须是一个字母。默认存储位置为$HOME/IdeaProjects/
,但如果有需要,您也可以指定不同的目录。
按Finish以创建Flutter项目
现在我们修改一下默认的示例应用程序,添加的第一个元素是一个简单的app bar
,用于显示应用程序的静态标题。随着这个项目的后续进展,我们将逐步向应用程序添加更多响应和状态的UI元素。
main.dart
文件位于Flutter项目中的lib
目录下,并包含启动执行应用程序的main()
函数。
main()
和runApp()
函数定义与默认应用程序中的相同,runApp()
函数作为参数,它是一个Widget
,Flutter框架在运行时展开并显示在应用程序的屏幕上。由于应用程序在UI中使用质感设计元素,因此创建一个新的MaterialApp
对象并将其传递给runApp()
函数,这个控件是我们应用程序控件树的根。
import 'package:flutter/material.dart';
void main() {
runApp(new MaterialApp(
title: '谈天说地',
home: new Scaffold(
appBar: new AppBar(
title: new Text('谈天说地'),
)
)
));
}
要指定用户在应用程序中看到的默认屏幕,需要在MaterialApp
定义中设置home
参数,home
参数引用了一个定义此应用程序的主UI的控件。该控件由一个Scaffold
控件组成,它具有一个简单的AppBar
作为其子控件。
为了给交互式组件打下基础,我们将简单的应用程序分解为两个不同的控件子类:一个永远不会更改的根级别的TalkcasuallyApp
控件,以及一个可在消息发送和内部状态更改时重建的子类ChatScreen
控件。现在,这两个类都可以扩展StatelessWidget
,之后,我们将调整ChatScreen
来管理状态。
import 'package:flutter/material.dart';
void main() {
runApp(new TalkcasuallyApp());
}
class TalkcasuallyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new MaterialApp(
title: '谈天说地',
home: new ChatScreen(),
);
}
}
class ChatScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text('谈天说地'),
)
);
}
}
上面代码中的新控件有一些共同点:它们是布局控件。他们的角色是执行布局任务,比如锚定、对齐和分发其他控件。
在这里要插一句话。随着您继续进行更改并优化应用程序的UI,您可以快速查看结果,而不需要重新启动完整的应用程序。使用Flutter的热重新加载功能将更新的源文件注入正在运行的Dart虚拟机(Dart Virtual Machine)并刷新UI。热重载是实验、原型设计和迭代的强大工具。需要注意的是,在IntelliJ IDEA中,热重载按钮是一个黄色闪电图标。
Flutter框架提供了一个名为TextField的质感设计控件,它是一个有状态的控件,具有用于自定义输入字段行为的属性。作为该项目的第一个有状态控件,它需要一些修改才能管理内部状态更改。
在Flutter中,如果要在窗口控件中可视化呈现状态数据,则应将此数据封装在State
对象中。然后,您可以将State
对象与扩展StatefulWidget
类的窗口控件相关联。
以下代码片段显示了如何开始在main.dart
文件中定义一个类,用于添加交互式文本输入字段。首先,我们将ChatScreen
类更改为子类StatefulWidget
而不是StatelessWidget
。然后,我们将定义一个创建State对象的ChatScreenState
类。
覆盖createState()
方法,以附加ChatScreenState
类,我们将使用新类来构建有状态的TextField
控件。在build()
方法之上添加一行以定义ChatScreenState
类:
class ChatScreen extends StatefulWidget {
@override
State createState() => new ChatScreenState();
}
class ChatScreenState extends State<ChatScreen> {
@override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text('谈天说地'),
)
);
}
}
现在,ChatScreenState
的build()
方法应该包括以前在控件树的ChatScreen
部分中的所有窗口控件。当框架调用build()
方法来刷新UI时,它可以使用它的子窗口控件树来重建ChatScreenState
。
当你对一个类或方法有疑问时,查看Flutter框架API的源代码定义对我们是很有用,可以更好地了解幕后情况。您可以通过选择一个类或方法名称,然后右键单击并选择“Go to Declaration”选项,从IntelliJ的编辑器面板轻松完成此操作。根据操作系统的不同,您也可以点击键盘上的命令或控制按钮。这是一个很好的习惯呢!
现在,我们的应用程序有能力管理状态,您可以使用输入字段和发送按钮构建ChatScreenState
类。要管理与文本字段的交互,需要使用TextEditingController
对象。您将使用它来读取输入字段的内容,并在发送消息后清除该字段。添加一行代码到ChatScreenState
类定义中以创建此对象。
class ChatScreenState extends State<ChatScreen> {
final TextEditingController _textController = new TextEditingController();
//...
以下代码片段显示了如何定义一个名为_buildTextComposer()
的私有方法,该方法使用已配置的TextField
控件返回Container
控件。
从Container
控件开始,在屏幕的边缘和输入字段的每一边之间增加一个水平边距。这里的单位是根据设备的像素比例将逻辑像素转换为特定数量的物理像素。比如iOS的points
或Android的density-independent pixels
等术语。
添加一个TextField
窗口控件,并按如下方式进行配置,以管理用户交互:
要控制文本字段的内容,我们将向TextField
构造函数提供一个TextEditingController
,该控制器也可用于清除该字段或读取其值。
要在用户提交消息时通知,需要使用onSubmitted
参数提供一个私有回调方法_handleSubmitted()
。现在这种方法只会清除该字段,稍后我们将添加更多的代码来发送消息。
class ChatScreenState extends State<ChatScreen> {
final TextEditingController _textController = new TextEditingController();
void _handleSubmitted(String text) {
_textController.clear();
}
Widget _buildTextComposer() {
return new Container(
margin: const EdgeInsets.symmetric(horizontal: 8.0),
child: new TextField(
controller: _textController,
onSubmitted: _handleSubmitted,
decoration: new InputDecoration.collapsed(hintText: '发送消息'),
)
);
}
//...
现在,告诉应用程序如何显示文本输入控件,在ChatScreenState
类的build()
方法中,将一个名为_buildTextComposer
的私有方法附加到body
属性。_buildTextComposer
方法返回一个封装文本输入字段的控件。
class ChatScreenState extends State<ChatScreen> {
//...
@override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text('谈天说地'),
),
body: _buildTextComposer()
);
}
}
这里要特别注意一点,从无状态到有状态控件的更改需要重新启动应用程序。
接下来,我们将在文本字段的右侧添加一个“发送”按钮,由于我们要显示与输入字段相邻的按钮,因此我们将使用Row
控件作为父项。然后将TextField
控件包装在Flexible
控件中,这将使Row
自动将文本字段的大小用于使用按钮未使用的剩余空间。
class ChatScreenState extends State<ChatScreen> {
//...
Widget _buildTextComposer() {
return new Container(
margin: const EdgeInsets.symmetric(horizontal: 8.0),
child: new Row(
children: [
new Flexible(
child: new TextField(
controller: _textController,
onSubmitted: _handleSubmitted,
decoration: new InputDecoration.collapsed(hintText: '发送消息'),
)
),
]
)
);
}
//...
}
我们现在可以创建一个IconButton
控件,显示发送图标。在icon
属性中,使用Icons.send
常量创建一个新的Icon
实例。此常数表示您的控件使用质感图标库提供的“发送”图标。
将您的IconButton
控件放在另一个Container
父窗口控件中。这样我们就可以自定义按钮的边距间距,使其在输入字段旁边更好看。对于onPressed
属性,使用匿名函数也调用_handleSubmitted()
方法,并使用_textController
传递消息的内容。
class ChatScreenState extends State<ChatScreen> {
//...
Widget _buildTextComposer() {
return new Container(
margin: const EdgeInsets.symmetric(horizontal: 8.0),
child: new Row(
children: [
new Flexible(
child: new TextField(
controller: _textController,
onSubmitted: _handleSubmitted,
decoration: new InputDecoration.collapsed(hintText: '发送消息'),
)
),
new Container(
margin: new EdgeInsets.symmetric(horizontal: 4.0),
child: new IconButton(
icon: new Icon(Icons.send),
onPressed: () => _handleSubmitted(_textController.text)),
)
]
)
);
}
//...
}
这里要解释一下,在Dart语法中,=>
函数声明=> expression
是{ return expression; }
的缩写。
使用默认的质感设计主题,按钮的颜色为黑色,要在应用程序中显示主要颜色的图标,可以使用不同的主题。
图标从IconTheme
控件继承其颜色、不透明度和大小,该控件使用IconThemeData
对象来定义这些特征。将IconTheme
控件中的_buildTextComposer()
方法中的所有控件包装起来,并使用其data
属性来指定当前主题的ThemeData
对象。这给了按钮(和控件树的这一部分中的任何其他图标)当前主题的主要颜色。
class ChatScreenState extends State<ChatScreen> {
//...
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: [
new Flexible(
child: new TextField(
controller: _textController,
onSubmitted: _handleSubmitted,
decoration: new InputDecoration.collapsed(hintText: '发送消息'),
)
),
new Container(
margin: new EdgeInsets.symmetric(horizontal: 4.0),
child: new IconButton(
icon: new Icon(Icons.send),
onPressed: () => _handleSubmitted(_textController.text)
),
)
]
)
)
);
}
//...
}
_buildTextComposer()
方法可以从其封装State
对象访问BuildContext
对象,您不需要明确地将上下文传递给该方法。