以下内容基本翻译自A Tour of the Flutter Widget Framework,翻译的可能并不完全!作为自己学习的笔记,加入了自己的理解,可能有疏漏错误,欢迎指正!
PS:Widget可能会翻译为小部件、组件、控件等等,都是一个东西,不要太在意细节
引言
Flutter组件使用现代的响应式框架(react-style framework)建立,灵感来源React。核心思想,通过组件(widget)构建UI。通过给组件(Widgets)设置它们当前的配置(configuration )和状态(state)来描述它们(Widgets)的长相。当组件的状态发生改变,组件重建它的描述,为了确定过度到下一个状态所需最小改变,该描述是框架对比之前的描述等到的差异。
原文:When a widget’s state changes, the widget rebuilds its description, which the framework diffs against the previous description in order to determine the minimal changes needed in the underlying render tree to transition from one state to the next.
英语太次,翻译不准确,个人理解大意:就是framework取得了前后两个状态的最小改变,没有变的属性不操作,改变了的算差值进行改变,而不是清空一个状态,再设置另一个状态。
tips:如果想通过深入代码更好了解Flutter,查看Building Layouts in Flutter 和Adding Interactivity to Your Flutter App.
Hello World
最小的FlutterApp仅仅通过组件调用runApp
函数。
import 'package:flutter/material.dart';
void main() {
runApp(
new Center(
child: new Text(
'Hello, world!',
textDirection: TextDirection.ltr,
),
),
);
}
runApp
方法获取到给它的组件( Widget
)并把组件作为它的组件树(widget tree)的根。
此例中,组件树持有两个组件, Center
(继承Align
) 和它的子组件- Text
。框架强制根组件(The root of the widget tree)铺满(cover)屏幕,这意味着Hello Worldtext
最终位于屏幕中心。此例中的text
的方向需要指定。
The text direction needs to be specified in this instance; when the MaterialApp widget is used, this is taken care of for you, as demonstrated later.
当写app时,你通常会创建 StatelessWidget
或 StatefulWidget
的子类作为组件,继承那个类取决与你的组件是否管理状态state
。一个组件的主要工作时实现build
方法,这个方法描述了这个组件与其他组件或子组件的条约(terms)。框架framework会依次创建这些组件,直到超出组件的底部,这代表计算和描述组件的几何形状的底层渲染对象RenderObject
.
The framework will build those widgets in turn until the process bottoms out in widgets that represent the underlying RenderObject
, which computes and describes the geometry of the widget.
Basic Widgets
Main article: Widgets Overview - Layout Models
Flutter自带一套强大的基础组件(Basic widgets),以下是其中一些常用的:
-
Text
:Text
用于创建带样式的文本 -
Row
,Column
: 这些灵活(flex)地组件可以让你在水平(Row
)和垂直(Column
)方向创建灵活的布局。它的设计是基于Web的flexbox
布局模型 -
Stack
:Stack
组件可以让你的组件在绘制顺序上层积(stack)在彼此顶部,而不是线性(水平或垂直)的。可以在子Stack
上使用Positioned
组件来放置他们到这个Stack
的top,right,bottom或left边界。Stack设计是基于Web的absolute positioning layout model(绝对位置布局模型)。 -
Container
:Container
组件帮你创建一个矩形元素。一个Container
可以被一个BoxDecoration
修饰,如背景(background)、边线(border)、阴影(shadow)。一个Container
也可以有margin,padding和固定大小定义。另外,Container
可以用矩阵(matrix)在三维控件改变。
以下是一些组合这些组件的例子:
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
设置,它允许使用预定义的Material icons集合。
name: my_app
flutter:
uses-material-design: true
运行效果
许多组件需要在 MaterialApp
内部才正确显示,继承他们的主题数据(Theme data),因此我们运行一个MaterialApp
程序。
MyAppBar
组件创建一个高56dip( device-independent pixels)及内部padding 8px,从左到右的Container
组件。在Container
中,MyAppBar
使用Row
布局(layout)来管理它的子控件。中间儿子,title
组件,标记为Expanded
,意为它可以扩展填充任何剩余的、未被其他子控件占用的空间。你可能有多重Expanded
子控件,使用flex
来确定他们各自占用可用空间的比例(You can have multiple Expanded
children and determine the ratio in which they consume the available space using the flex
argument to Expanded
)。
MyScaffold
组件在垂直列方向(vertical column)管理它的子控件。在列顶,它放了一个MyAppBar
实例,传递一个Text
组件做app bar的title。传递组件(Passing widgets)作为另一个组建的参数是一个强大的技术,它允许你创建的常用组件多样重用。最后,居中显示信心的MyScaffold
使用Expanded
来填充剩余的空间。
Using Material Components
Main article: Widgets Overview - Material Components
Flutter提供了许多遵循Material Design的组件帮助你创建app。一个Material app始于MaterialApp
组件,MaterialApp
组件作为你app的根(root)创建许多有用的组件,包括 管理一堆使用strings区分的组件、亦被称为routes
的Navigator
。Navigator
让你平滑在app的screens间切换。使用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
and MyScaffold
为来自material.dart
的AppBar
和Scaffold
,我们的app看起来更加Material。例如,app bar有阴影了,title文本自动继承了正确的样式。我们也添加了一个合适的浮动按钮(FloatingActionButton)。
注意,我们再次把组件作为参数传递给另一个组件。Scaffold
需要许多不同的组件作为参数,他们将被放在Scaffold的适当位置。类似的,AppBar
组件让我们传递组件作为 title
组件的leading
和 actions
。这种模式遍布整个框架,你在设计自己的组件时也需要考虑。
Handling gestures 处理手势
Main article: Gestures in Flutter
大部分app包含一些用户与系统交互的表单。第一步就是要创建一个可交互的app去检测输入的手势getsures。创建一个实例按钮来看看这是如何工作的:
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
组件不可见,但可以检测用户手势gestures。当用户轻击Container
,GestureDetector
将回调它的onTap
方法,这个例子中,在控制台console打印了一条消息。你可以使用GestureDetector
来检测一系列输入手势,包括点击tap,拖动drag和缩放scale。
许多组件使用GestureDetector
来用于其他组件的可选回调。例如,IconButton
,RaisedButton
, FloatingActionButton
有一个 onPressed
回调方法,当用户点击这些组件时触发。
Changing widgets in response to input
Main articles: StatefulWidget
, State.setState
目前,我们仅使用了stateless
组件。Stateless widgets从他们的父类接受参数,他们保存了 final
成员变量。当一个组件调用build
时,它使用这些已存的值提取新参数(derive new arguments )来创建组件。
为了创建更加复杂的体验,例如,以更有趣的方式响应用户的输入,app通常带有某些状态。Flutter使用StatefulWidgets
组件获取这些idea.StatefulWidgets
是知道如何创建State
对象的特殊组件,State
持有state。在此例中,使用RaisedButton
mentioned earlier:
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中,这两类对象有不同的生命周期。Widgets是临时对象,用于构建app当前状态的表达(presentation)。另一方面,State
是在build()
之间是持续的,允许他们记忆信息。
上例中,接收用户输入和也接受它的build
方法中的结果。在更复杂的app中,不同层级的组件可能负责不同关注点。例如,一个组件可能呈现复杂的用户界面,其目的是收集特定信息,如信息或位置,而另一个组件可能使用这些信息更改总体表现。
在Flutter中,更改信息流依赖回调组件层次结构,当当前状态流向stateless组件。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),
]);
}
}
请注意我们如何创建2个stateless组件,清晰分离了关注点:显示displaying计数器(CounterDisplay)和改变changing计数器(CounterIncrementor)。虽然最终结果和之前一样,但费力责任允许在单个组件中加入更多复杂性,同时保持父级的简单性。
Bringing it all together
我们来思考一个更复杂的例子,把以上观点都汇聚在一起。我们来假设一个购物app,展示各种待售产品,并维护一个购物车用于购买。我们先来定义 presentation class-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
继承自一个stateless 组件的通用模式。它保存来自它的构造函数接受的值给它的 final
成员变量,这些值在它的build
方法中使用。例如,inCart
切换两种可视外观,一个使用当前主题的primary颜色,另一个使用灰色gray。
当用户点击条目,组件不直接改变它的inCart
的值。而是调用父类的onCartChanged
方法。这种模式让你保存状态state到更高的组件层级,这是状态持续更长时期的原因。这个例子中,保存在组件中的状态state通过runApp
持续存在于app的生命周期中。
当父类收到onCartChanged
回调,父类将更新它的内部状态state,这将引发父类重建并新建一个带有新inCart
值的新的ShopingListItem
实例。虽然父类重建时创建一个新的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
组件首次插入树(tree)中,框架调用createState
方法来创建一个最新的_ShoppingListState
实例来连接树(tree)。(注意我们通常使用_
开头来定义State
的子类来表示他们私有实现详情(private implementation details)) 当组件的父类重建,父类会创建一个新的ShoppingList
实例,但是框架会重用已经存在树(tree)中的_ShoppingListState
实例,而不是再次调用createState
。为了获取当前
ShoppingList
特性(properties ),_ShoppingListState
会使用它自身widget
特性。如果父类重建并创建一个新的ShoppingList
,_ShoppingListState
也会重新创建一个新的widget
值。如果你希望当 widget
特性改变时得到通知,可以复写didUpdateWidget
方法,它会传递oldWidget
,你可以与当前widget
进行对比。当处理
onCartChanged
回调时,_ShoppingListState
通过给_shoppingCart
增加或删除一个product
改变它内部状态。通过调用setState
通知框架(framework)它内部状态(state)的改变。调用setState
标记widget
为脏dirty
并安排它在下次更新屏幕时重建。如果内部状态改变时你忘记调用setState
,框架(Framework)不会知道你的widget
是脏的(dirty),也就不会调用widget
的build
方法,这意味着,用户界面不会更新反应状态的改变。通过这种方式管理state,你不必分别为创建和更新子widget写代码,只需简单的实现
build
方法,它会处理两种情况。
Responding to widget lifecycle events 组件生命周期响应
Main article: State
StatefulWidget
调用 createState
之后,框架(framework)插入一个新的state对象到树(tree)中,然后调用state对象的 initState
。State
的子类可以复写(override)initState
来做只需执行一次的工作。例如,你可以复写(override)initState
来配置动画或订阅平台服务(subscribe to platform services)。initState
的实现要求以调用super.initState
开始。
当一个state对象不在被需要,框架(framework)调用state对象的dispose
。你可以复写(override)dispose
方法来清理工作。例如,通过复写dispose
来取消计时器或解除平台服务订阅。dispose
实现通常以调用super.dispose
结束。
Keys
Main article: Key
你可以使用keys来控制当一个组件重建时,框架将匹配那些其他组件。默认的,框架会根据他们的 runtimeType
和他们出现的顺序匹配当前和前一个组件。带有keys的,框架(framework)会要求2个组件有相同的key
和相同的runtimeType
。
当创建很多相同类型组件的实例时Keys是非常有用的。例如,上例中ShoppingList
组件,创建了足够多的ShoppingListItem
实例来填充它的可视区域:
- 没有keys,当前创建的第一个entry会一直与前一次创建的第一个entry同步,即使列表中的第一个entry滚出屏幕而且不再可见。
通过指派列表中每一个entry一个semantic* key,无限列表可以更有效,因为框架会同步entries 匹配semantic keys和因此相同或近似的外观。此外,同步entries语义semantically意味着在stateful的子组件中的state会保持连接到相同semantic entry,而不是连接到在viewport中的相同数值位置的entry。
Global Keys 全局Keys
Main article: GlobalKey
你可以使用全局keys唯一的标识子组件。全局keys必须在整个组件层级是全局唯一的,不像本地local key只需在兄弟之间唯一。因为他们全局唯一,可以用全局key检索与组件连接状态。
网友评论