通过 widgets 构建 UI,描述当前的配置和状态,当状态 State 改变时,框架找出前后的变化,以确定底层渲染树从一个状态转换到下一个状态所需的最小更改。
一个最简单的 Flutter 程序就是在入口方法 main 中调用 runApp,这个函数将参数的 Widget 作为整个应用的 Widget 树的根,将这个根 Widget 完全覆盖到屏幕上。
import 'package:flutter/material.dart';
void main() {
runApp(
Center(
child: Text(
'Hello, world!',
textDirection: TextDirection.ltr,
),
),
);
}
Widget 分两种:无状态的 StatelessWidget
和有状态的 StatefulWidget
,主要工作就是实现 build
方法,用于描述更低级的 Widget 以构建自身。最底层的 widget 通常为 RenderObject,它会计算并描述 widget 的几何形状。
基础的 Widget
- Text:带格式文本
- Row, Column: flex 布局,类似 Web 的 flexbox,Android 的 FlexboxLayout
- Stack:堆叠,类似 Web 的 absolute,也像 Android 的 RelativeLayout,内部使用 Positioned 并控制其在 Stack 内的上下左右的边距
- Container: 创建一个矩形可见元素,可以通过 BoxDecoration 来做样式,如背景,边框,阴影。可以设置 margin,padding 和应用于尺寸大小的约束 constraints。可以通过矩阵在三维空间变换。
下面是一个示例代码,首先确保 pubspec.yaml
有
flutter:
uses-material-design: true
新创建的工程默认就是这个配置。
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 Container(
height: 56.0, // in logical pixels
padding: const EdgeInsets.symmetric(horizontal: 8.0),
decoration: BoxDecoration(color: Colors.blue[500]),
// Row is a horizontal, linear layout.
child: Row(
// is the type of items in the list.
children: [
IconButton(
icon: Icon(Icons.menu),
tooltip: 'Navigation menu',
onPressed: null, // null disables the button
),
// Expanded expands its child to fill the available space.
Expanded(
child: title,
),
IconButton(
icon: 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 Material(
// Column is a vertical, linear layout.
child: Column(
children: [
MyAppBar(
title: Text(
'Example title',
style: Theme.of(context).primaryTextTheme.title,
),
),
Expanded(
child: Center(
child: Text('Hello, world!'),
),
),
],
),
);
}
}
void main() {
runApp(MaterialApp(
title: 'My app', // used by the OS task switcher
home: MyScaffold(),
));
}
将上面的代码放到一个新建的 newroute.dart
文件中,去掉 main 方法,在 main.dart
的 AppBar 上添加一个按钮
IconButton(icon: Icon(Icons.add), onPressed: _toOther),
void _toOther() {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) {
return MyScaffold();
})
);
}
要使用另一个文件的 MyScaffold 类,需要 import
import 'newroute.dart';
结果成功跳转。
-
MyScaffold
是StatelessWidget
,它的build
方法返回一个Material
,这就是页面显示的内容。 -
Material
里只有一个 child,就是一个Column
,一个垂直的线性布局。包含一个MyAppBar
和Expanded
-
Expanded
是Column
或Row
或Flex
的 child,意思就是填充尚未被其他子项占用的的剩余可用空间,如果多个 child 都是Expanded
,通过flex
来指定各自的比例,有些类似 Android 的weight
-
MyAppBar
是一个StatelessWidget
,内部是Container
,然后设置它的高度,内边距。child 是Row
,和Column
类似,只是水平的。左右各一个IconButton
手势处理
class MyButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () {
print('MyButton was tapped!');
Navigator.of(context).pop(this);
},
child: Container(
height: 36.0,
padding: const EdgeInsets.all(8.0),
margin: const EdgeInsets.symmetric(horizontal: 8.0),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(5.0),
color: Colors.lightGreen[500],
),
child: Center(
child: Text('Engage'),
),
),
);
}
}
修改显示的内容:
class MyScaffold extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Material(
child: MyButton(),
};
}
}
当用户触摸 GestureDector
的 child,即这里的 Container
,就会调用 onTap
,GestureDector
可以发现包括 tap,drag 和 scales 事件。
在 onTap
里让页面返回,效果如下:
交互
class CounterDisplay extends StatelessWidget {
CounterDisplay({this.count});
final int count;
@override
Widget build(BuildContext context) {
return Text('Count: $count');
}
}
class CounterIncrementor extends StatelessWidget {
CounterIncrementor({this.onPressed});
final VoidCallback onPressed;
@override
Widget build(BuildContext context) {
return RaisedButton(
onPressed: onPressed,
child: Text('Increment'),
);
}
}
class Counter extends StatefulWidget {
@override
_CounterState createState() => _CounterState();
}
class _CounterState extends State {
int _counter = 0;
void _increment() {
setState(() {
++_counter;
});
}
@override
Widget build(BuildContext context) {
return Row(children: [
CounterIncrementor(onPressed: _increment),
CounterDisplay(count: _counter),
RaisedButton(
onPressed: () {
Navigator.of(context).pop(this);
},
child: Text('返回'),
),
]);
}
}
在前一篇里已经遇到了 StatefulWidget
,首先 Counter
的 createState()
返回一个 _CounterState
实例。
在 _CounterState
里调用是 CounterIncrementor(onPressed: _increment)
,_increment
将 _counter
加 1,setState
会引起再次 build
,于是 CounterDisplay
里的 Text 内容也就变了。
综合例子
// 一个产品类,一个 name 属性
class Product {
const Product({this.name});
final String name;
}
typedef void CartChangedCallback(Product product, bool inCart);
// 一个 StatelessWidget
class ShoppingListItem extends StatelessWidget {
ShoppingListItem({Product product, this.inCart, this.onCartChanged})
: product = product,
super(key: ObjectKey(product));
final Product product;
final bool inCart;
final CartChangedCallback onCartChanged;
Color _getColor(BuildContext context) {
// 根据布尔值返回不同的颜色
return inCart ? Colors.black54 : Theme.of(context).primaryColor;
}
TextStyle _getTextStyle(BuildContext context) {
if (!inCart) return null;
return TextStyle(
color: Colors.black54,
decoration: TextDecoration.lineThrough,
);
}
@override
Widget build(BuildContext context) {
return ListTile(
onTap: () {
// 触摸的时候,inCart 取反,然后回调外界传入的 onCartChanged
onCartChanged(product, !inCart);
},
leading: CircleAvatar(
backgroundColor: _getColor(context), // 调上面的方法返回不同的背景色
child: Text(product.name[0]),
),
title: Text(product.name, style: _getTextStyle(context)), // 这个 item 的文字
);
}
}
这是一个列表的 item,onCartChanged
是回调函数,由父 Widget 传入,函数被调用后,父 Widget 更新内部的 State
,然后导致利用这个新的 inCart
值重新创建一个 ShoppingListItem
实例。
class ShoppingList extends StatefulWidget {
ShoppingList({Key key, this.products}) : super(key: key);
final List products;
@override
_ShoppingListState createState() => _ShoppingListState();
}
class _ShoppingListState extends State {
Set _shoppingCart = Set();
// 传给 item 的回调函数,setState 去重新 build
void _handleCartChanged(Product product, bool inCart) {
setState(() {
if (inCart)
_shoppingCart.add(product);
else
_shoppingCart.remove(product);
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Shopping List'),
),
body: ListView(
padding: EdgeInsets.symmetric(vertical: 8.0),
children: widget.products.map((Product product) {
return ShoppingListItem(
product: product,
inCart: _shoppingCart.contains(product),
onCartChanged: _handleCartChanged,
);
}).toList(),
),
);
}
}
// void main() {
// runApp(MaterialApp(
// title: 'Shopping App',
// home: ShoppingList(
// products: [
// Product(name: 'Eggs'),
// Product(name: 'Flour'),
// Product(name: 'Chocolate chips'),
// ],
// ),
// ));
// }
在前面的页面加个按钮跳过来
RaisedButton(
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) {
ShoppingList(
products: [
Product(name: 'Eggs'),
Product(name: 'Flour'),
Product(name: 'Chocolate chips'),
],
);
})
);
)
},
...
)
当 ShoppingList 首次插入到树中时,框架会调用其 createState 函数以创建一个新的 _ShoppingListState 实例来与该树中的相应位置关联。当这个 widget 的父级重建时,父级将创建一个新的 ShoppingList 实例,但是 Flutter 框架将重用已经在树中的 _ShoppingListState 实例,而不是再次调用 createState 创建一个新的。
调用 setState 将该 widget 标记为 dirty,并且计划在下次应用程序需要更新屏幕时重新构建它。通过这种方式管理状态,不需要编写用于创建和更新子 widget 的单独代码。相反,只需实现可以处理这两种情况的 build 函数。