Flutter小部件采用现代反应式框架构建,从React中获得灵感。 中心思想是你从小部件中构建你的UI。 小组件描述了他们的视图在给定其当前配置和状态时应该看起来像什么。 当小部件的状态发生变化时,小部件会重新构建它的描述,该描述与前面的描述不同,以确定底层渲染树从一个状态转换到下一个状态所需的最小更改。
注意:如果您想通过深入了解某些代码来熟悉Flutter,请查看构建Flutter布局并为Flutter App添加交互功能。
最小的Flutter应用程序只需使用一个小部件调用runApp函数:
import 'package:flutter/material.dart';
void main() {
runApp(
new Center(
child: new Text(
'Hello, world!',
textDirection: TextDirection.ltr,
),
),
);
}
runApp函数使用给定的Widget并使其成为Widget树的根。 在此示例中,部件树由两个小部件组成,即Center部件及其子部件,即Text部件。框架强制根部件覆盖屏幕,这意味着文本“Hello, world”最终集中在屏幕上。文本方向需要在此实例中指定; 当使用MaterialApp部件时,将为您处理好,稍后将进行演示。
在编写应用程序时,通常会根据您的部件是否管理任何状态来创建新的部件,这些部件是StatelessWidget或StatefulWidget的子类。部件的主要工作是实现一个build函数,它根据其他较低级别的部件描述部件。该框架将依次构建这些部件,直到该过程落在代表底层RenderObject的部件中,该部件计算并描述部件的几何形状。
主要文章:部件集概述 - 布局模型
Flutter带有一套强大的基本小部件,其中以下是非常常用的:
以下是一些简单的小部件,它们结合了这些和其他小部件:
import 'package:flutter/material.dart';
class MyAppBar extends StatelessWidget {
MyAppBar({this.title});
// Fields in a Widget subclass are always marked "final".
final Widget title;
@override
Widget build(BuildContext context) {
return new Container(
height: 56.0, // in logical pixels
padding: const EdgeInsets.symmetric(horizontal: 8.0),
decoration: new BoxDecoration(color: Colors.blue[500]),
// Row is a horizontal, linear layout.
child: new Row(
// <Widget> is the type of items in the list.
children: <Widget>[
new IconButton(
icon: new Icon(Icons.menu),
tooltip: 'Navigation menu',
onPressed: null, // null disables the button
),
// Expanded expands its child to fill the available space.
new Expanded(
child: title,
),
new IconButton(
icon: new Icon(Icons.search),
tooltip: 'Search',
onPressed: null,
),
],
),
);
}
}
class MyScaffold extends StatelessWidget {
@override
Widget build(BuildContext context) {
// Material is a conceptual piece of paper on which the UI appears.
return new Material(
// Column is a vertical, linear layout.
child: new Column(
children: <Widget>[
new MyAppBar(
title: new Text(
'Example title',
style: Theme.of(context).primaryTextTheme.title,
),
),
new Expanded(
child: new Center(
child: new Text('Hello, world!'),
),
),
],
),
);
}
}
void main() {
runApp(new MaterialApp(
title: 'My app', // used by the OS task switcher
home: new MyScaffold(),
));
}
请确保在pubspec.yaml文件的flutter部分有一个uses-material-design:true条目。 它允许使用预定义的一组材料图标。
name: my_app
flutter:
uses-material-design: true
为了继承主题数据,许多小部件需要位于MaterialApp中才能正常显示。 因此,我们使用MaterialApp运行应用程序。
MyAppBar小部件创建一个Container,其高度为56个设备无关像素,内部填充像素为8像素,均位于左侧和右侧。在容器内部,MyAppBar使用Row布局来组织其子项。中间的孩子,标题小部件被标记为Expanded,这意味着它扩展以填充其他孩子尚未消费的剩余可用空间。您可以有多个Expanded子项,并使用Expanded的flex参数确定它们占用可用空间的比率。
MyScaffold小部件在垂直列中组织其子女。在列顶部,它放置了MyAppBar的一个实例,将应用程序栏传递给一个Text小部件用作其标题。将小部件作为参数传递给其他小部件是一种强大的技术,可以让您创建可以以各种方式重用的通用小部件。最后,MyScaffold使用Expanded来填充剩余空间,其中包含一个中心消息。
主要文章:小工具概述 - 材料组件
Flutter提供了许多小工具,可帮助您构建遵循Material Design的应用程序。材质应用程序以MaterialApp小部件开始,该小部件在应用程序根部创建了许多有用的小部件,其中包括一个Navigator,该导航器管理由字符串(也称为“routes”)标识的小部件堆栈。Navigator可让您在应用程序的各个屏幕之间平滑过渡。 使用MaterialApp小部件完全是可选的,但是一种很好的做法。
import 'package:flutter/material.dart';
void main() {
runApp(new MaterialApp(
title: 'Flutter Tutorial',
home: new TutorialHome(),
));
}
class TutorialHome extends StatelessWidget {
@override
Widget build(BuildContext context) {
// Scaffold is a layout for the major Material Components.
return new Scaffold(
appBar: new AppBar(
leading: new IconButton(
icon: new Icon(Icons.menu),
tooltip: 'Navigation menu',
onPressed: null,
),
title: new Text('Example title'),
actions: <Widget>[
new IconButton(
icon: new Icon(Icons.search),
tooltip: 'Search',
onPressed: null,
),
],
),
// body is the majority of the screen.
body: new Center(
child: new Text('Hello, world!'),
),
floatingActionButton: new FloatingActionButton(
tooltip: 'Add', // used by assistive technologies
child: new Icon(Icons.add),
onPressed: null,
),
);
}
}
现在我们已经从MyAppBar和MyScaffold切换到material.dart的AppBar和Scaffold窗口小部件,我们的应用程序开始查看更多的Material。例如,应用栏有一个阴影,标题文本会自动继承正确的样式。 我们还添加了一个浮动动作按钮,以便您采取措施。
请注意,我们再次将小部件作为参数传递给其他小部件。Scaffold小部件将许多不同的小部件作为命名参数,每个小部件放置在适当位置的Scaffold布局中。同样,AppBar小部件允许我们传递小部件以获取title小部件的leading和actiions。这种模式在整个框架中重复出现,并且在设计自己的小部件时可能会考虑到这一点。
主要文章:Flutter的手势
大多数应用程序包括某种形式的与系统的用户交互。 构建交互式应用程序的第一步是检测输入手势。 让我们通过创建一个简单的按钮来了解它的工作原理:
class MyButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new GestureDetector(
onTap: () {
print('MyButton was tapped!');
},
child: new Container(
height: 36.0,
padding: const EdgeInsets.all(8.0),
margin: const EdgeInsets.symmetric(horizontal: 8.0),
decoration: new BoxDecoration(
borderRadius: new BorderRadius.circular(5.0),
color: Colors.lightGreen[500],
),
child: new Center(
child: new Text('Engage'),
),
),
);
}
}
GestureDetector小部件没有可视化表示,而是检测用户做出的手势。当用户点击Container时,GestureDetector将调用其onTap回调,在这种情况下,将消息打印到控制台。您可以使用GestureDetector检测各种输入手势,包括点击,拖动和缩放。
许多小部件使用GestureDetector为其他小部件提供可选的回调。 例如,IconButton,RaisedButton和FloatingActionButton小部件具有onPressed回调,这些回调在用户轻击小部件时触发。
主要文章:StatefulWidget,State.setState
到目前为止,我们只使用无状态的小部件。 无状态小部件从他们的父部件接收参数,它们存储在final的成员变量中。 当一个小部件被要求build时,它会使用这些存储的值来为它创建的小部件派生新的参数。
为了构建更复杂的体验 - 例如,以更有趣的方式对用户输入做出反应 - 应用程序通常会携带一些状态。Flutter使用StatefulWidgets来捕捉这个想法。 StatefulWidgets是特殊的小部件,它知道如何生成状态对象,然后用它来保持状态。考虑这个基本的例子,使用前面提到的RaisedButton:
class Counter extends StatefulWidget {
// This class is the configuration for the state. It holds the
// values (in this nothing) provided by the parent and used by the build
// method of the State. Fields in a Widget subclass are always marked "final".
@override
_CounterState createState() => new _CounterState();
}
class _CounterState extends State<Counter> {
int _counter = 0;
void _increment() {
setState(() {
// This call to setState tells the Flutter framework that
// something has changed in this State, which causes it to rerun
// the build method below so that the display can reflect the
// updated values. If we changed _counter without calling
// setState(), then the build method would not be called again,
// and so nothing would appear to happen.
_counter++;
});
}
@override
Widget build(BuildContext context) {
// This method is rerun every time setState is called, for instance
// as done by the _increment method above.
// The Flutter framework has been optimized to make rerunning
// build methods fast, so that you can just rebuild anything that
// needs updating rather than having to individually change
// instances of widgets.
return new Row(
children: <Widget>[
new RaisedButton(
onPressed: _increment,
child: new Text('Increment'),
),
new Text('Count: $_counter'),
],
);
}
}
您可能想知道为什么StatefulWidget和State是单独的对象。 在Flutter中,这两种类型的对象具有不同的生命周期。 小部件是临时对象,用于构建当前状态下的应用程序演示文稿。 另一方面,State对象在调用build()之间是持久的,允许它们记住信息。
上面的例子接受用户输入并直接在其构建方法中使用结果。在更复杂的应用程序中,小部件层次结构的不同部分可能对不同的问题负责; 例如,一个小部件可能呈现一个复杂的用户界面,其目标是收集特定信息(如日期或位置),而另一个小部件可能会使用该信息来更改整体呈现。
在Flutter中,更改通知通过回调的方式“向上”流,而当前状态则“向下”流向呈现的无状态小部件。重定向这一流程的共同父母是State。 让我们看看这个稍微复杂的例子在实践中是如何工作的:
class CounterDisplay extends StatelessWidget {
CounterDisplay({this.count});
final int count;
@override
Widget build(BuildContext context) {
return new Text('Count: $count');
}
}
class CounterIncrementor extends StatelessWidget {
CounterIncrementor({this.onPressed});
final VoidCallback onPressed;
@override
Widget build(BuildContext context) {
return new RaisedButton(
onPressed: onPressed,
child: new Text('Increment'),
);
}
}
class Counter extends StatefulWidget {
@override
_CounterState createState() => new _CounterState();
}
class _CounterState extends State<Counter> {
int _counter = 0;
void _increment() {
setState(() {
++_counter;
});
}
@override
Widget build(BuildContext context) {
return new Row(children: <Widget>[
new CounterIncrementor(onPressed: _increment),
new CounterDisplay(count: _counter),
]);
}
}
注意我们如何创建了两个新的无状态小部件,干净地分隔了显示计数器(CounterDisplay)和更改计数器(CounterIncrementor)的顾虑。尽管最终结果与前一个示例相同,但责任分离允许将更大的复杂性封装在各个小部件中,同时保持父项的简单性。
让我们考虑一个更完整的例子,将上面介绍的概念汇集在一起。 我们将与一个假设的购物应用程序一起工作,该应用程序显示出售的各种产品,并维护用于预期购买的购物车。 我们首先定义我们的演示类ShoppingListItem:
class Product {
const Product({this.name});
final String name;
}
typedef void CartChangedCallback(Product product, bool inCart);
class ShoppingListItem extends StatelessWidget {
ShoppingListItem({Product product, this.inCart, this.onCartChanged})
: product = product,
super(key: new ObjectKey(product));
final Product product;
final bool inCart;
final CartChangedCallback onCartChanged;
Color _getColor(BuildContext context) {
// The theme depends on the BuildContext because different parts of the tree
// can have different themes. The BuildContext indicates where the build is
// taking place and therefore which theme to use.
return inCart ? Colors.black54 : Theme.of(context).primaryColor;
}
TextStyle _getTextStyle(BuildContext context) {
if (!inCart) return null;
return new TextStyle(
color: Colors.black54,
decoration: TextDecoration.lineThrough,
);
}
@override
Widget build(BuildContext context) {
return new ListTile(
onTap: () {
onCartChanged(product, !inCart);
},
leading: new CircleAvatar(
backgroundColor: _getColor(context),
child: new Text(product.name[0]),
),
title: new Text(product.name, style: _getTextStyle(context)),
);
}
}
ShoppingListItem小部件遵循无状态小部件的常见模式。 它将它在构造函数中接收到的值存储在final的成员变量中,然后在build函数中使用它。例如,inCart布尔值可以在两个可视外观之间切换:一个使用当前主题的主要颜色,另一个使用灰色。
当用户点击列表项时,小部件不会直接修改其inCart值。 相反,小部件会调用它从其父部件接收到的onCartChanged函数。此模式可让您在小部件层次结构中存储更高层级的状态,从而使状态持续更长的时间。 在极端情况下,传递给runApp的存储在窗口小部件上的状态会在应用程序的整个生命周期中持续存在。
当父级收到onCartChanged回调时,父级将更新其内部状态,这将触发父级重建并使用新的inCart值创建ShoppingListItem的新实例。尽管父级在重建时创建了ShoppingListItem的新实例,但该操作很便宜,因为该框架将新构建的小部件与先前构建的小部件进行比较,并仅将差异应用于基础RenderObject。
我们来看看存储可变状态的示例父部件:
class ShoppingList extends StatefulWidget {
ShoppingList({Key key, this.products}) : super(key: key);
final List<Product> products;
// The framework calls createState the first time a widget appears at a given
// location in the tree. If the parent rebuilds and uses the same type of
// widget (with the same key), the framework will re-use the State object
// instead of creating a new State object.
@override
_ShoppingListState createState() => new _ShoppingListState();
}
class _ShoppingListState extends State<ShoppingList> {
Set<Product> _shoppingCart = new Set<Product>();
void _handleCartChanged(Product product, bool inCart) {
setState(() {
// When user changes what is in the cart, we need to change _shoppingCart
// inside a setState call to trigger a rebuild. The framework then calls
// build, below, which updates the visual appearance of the app.
if (inCart)
_shoppingCart.add(product);
else
_shoppingCart.remove(product);
});
}
@override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text('Shopping List'),
),
body: new ListView(
padding: new EdgeInsets.symmetric(vertical: 8.0),
children: widget.products.map((Product product) {
return new ShoppingListItem(
product: product,
inCart: _shoppingCart.contains(product),
onCartChanged: _handleCartChanged,
);
}).toList(),
),
);
}
}
void main() {
runApp(new MaterialApp(
title: 'Shopping App',
home: new ShoppingList(
products: <Product>[
new Product(name: 'Eggs'),
new Product(name: 'Flour'),
new Product(name: 'Chocolate chips'),
],
),
));
}
ShoppingList类扩展了StatefulWidget,这意味着这个小部件存储可变状态。当ShoppingList小部件首次插入到树中时,框架将调用createState函数来创建_ShoppingListState的新实例,以便与该树中的该位置关联。(请注意,我们通常使用前导下划线来命名State的子类,以指示它们是私有实现细节。)当此小部件的父级重建时,父级将创建ShoppingList的新实例,但该框架将重新使用树已存在的_ShoppingListState实例 而不是再次调用createState。
要访问当前ShoppingList的属性,_ShoppingListState可以使用其widget属性。如果父级重建并创建新的ShoppingList,则_ShoppingListState也将使用新的widget值重建。如果您希望在小部件属性发生更改时收到通知,您可以覆盖didWargetWidget函数,该函数通过oldWidget传递,以便将旧小部件与当前widget进行比较。
在处理onCartChanged回调时,_ShoppingListState会通过添加或删除_shoppingCart中的产品来改变其内部状态。为了通知框架它改变了它的内部状态,它将这些调用包装在setState调用中。调用setState会将这个小部件标记为肮脏,并计划在下一次您的应用程序需要更新屏幕时重新构建它。如果您在修改窗口小部件的内部状态时忘记调用setState,则框架将不知道您的窗口小部件是脏的,并且可能不会调用窗口小部件的build函数,这意味着用户界面可能不会更新以反映已更改的状态。
通过以这种方式管理状态,您不需要编写用于创建和更新子部件的单独代码。 相反,您只需实现可以处理这两种情况的构建函数。
主要文章:State
在StatefulWidget上调用createState之后,框架将新的状态对象插入树中,然后在状态对象上调用initState。State的一个子类可以覆盖initState来完成只需要发生一次的工作。 例如,您可以覆盖initState来配置动画或订阅平台服务。 initState的实现需要通过调用super.initState来启动。
当一个状态对象不再需要时,框架在状态对象上调用dispose。 您可以覆盖dispose函数来执行清理工作。 例如,您可以覆盖dispose以取消定时器或取消订阅平台服务。 通常,通过调用super.dispose执行dispose。
主要文章:Key
您可以使用键来控制框架在小部件重建时哪个小部件匹配哪个其他小部件。默认情况下,框架根据它们的runtimeType和它们出现的顺序来匹配当前构建和以前构建中的小部件。使用键,框架要求两个小部件具有相同的key以及相同的runtimeType。
键在构建相同类型的部件的许多实例的部件中最有用。例如,ShoppingList窗口部件构建了足够的ShoppingListItem实例来填充其可见区域:
主要文章:GlobalKey
您可以使用全局键来唯一标识子窗口部件。 全局键在整个窗口部件层次结构中必须是全局唯一的,这与局部键不同,后者只需要在同级中唯一。 由于它们是全局唯一的,因此可以使用全局键来检索与窗口部件关联的状态。