带你入门Flutter:UI篇——Widget介绍

Flutter Widget采用现代响应式框架构建,这是从 React中获得的灵感,核心思想是用Widget构建你的UI。 Widget描述了对应视图在当前配置和状态下应该是什么样子,当Widget的状态发生变化时,Widget会重新构建UI,Flutter会对比前后变化的不同, 以确定底层渲染树从一个状态转换到下一个状态所需的最小更改

Hello World

一个最简单的Flutter App可以调用runApp()函数,使用一个Widget作为runApp()方法的参数传递进去:

import 'package:flutter/material.dart';
void main() {
runApp(
Center(
child: Text(
'Hello, world!',
textDirection: TextDirection.ltr,
),
),
);
}

上述代码运行起来的截图如下:

一个月带你入门Flutter:UI篇(1)——Widget介绍

Simulator Screen Shot – iPhone Xʀ – 2019-06-08 at 19.42.05

runApp()函数的作用是:接收一个Widget类型的参数,使其成为整个Widget树的根节点。在上面这个例子中,Widget树包含了2个Widget,分别是Center Widget和其孩子——Text Widget。框架会强制使根Widget覆盖整个屏幕,也就意味着”Hello,World”这行字符串会在整个屏幕的正中间。在这个实例中,Text Widget中文字显示的方向需要被显式指定,当使用MaterialApp Widget的时候,Text中文字的方向将不用手动指定(会自动设定),这一点会在后面展开讲解。

注意:这里的MaterialApp Widget是Flutter开发中最常用的复合Material Design设计理念的入口Widget,所谓Material Design直观理解就是一种更加生动的、好看的视觉设计。

当开发一个App的时候,你通常通常会创建新的Widget,这些自定义的Widget必须是StatelessWidget(无状态的)或者StatefulWidget(有状态的)的子类, 具体的选择取决于你的Widget是否需要管理状态。Widget类的核心是实现一个build()函数,在该函数中一般是使用其他Widget当构建当前Widget,这样就会形成一个Widget树,Flutter框架将依次构建这些Widget,直到构建到最底层的子Widget,这些最底层的Widget通常为RenderObject,它会计算并描述Widget的几何形状。

注意:StatelessWidget和StatefulWidget,对应无状态和有状态,可能有的读者不太清除这里的状态时什么意思,直观地来说,如果某个组件从其出生到其死亡整个过程的状态都没有变化(其显示的文字内容没有变化、其背景没有变化…),那么就称之为无状态的,相反,如果某个组件其状态有可能变化(比如一个按钮点击之后其显示的文字需要产生变化),那么这种类型的Widget就是有状态的。

基础Widget

Flutter有一套丰富而又强大的基础Widget,下面列出了一些常用的:

  • Text:Text 这个Widget可以让你在你的App中创建一个带格式的文本(说白了就是展示文字用的);
  • Row,Column:这两个Widget让你分别在水平方向(Row)和垂直方向(Column)创建自适应的灵活布局,即线性布局(当我们需要创建一些处于同一行或者同一列的Widgets时需要用到这两个),其设计使基于web开发中的Flexbox布局模型;
  • Stack:与线性布局不同的是,Stack Widget允许子Widget进行堆叠(比如我们需要在一个背景图片上放置一个按钮,这时背景图片就是第一层,按钮覆盖背景图片作为第二层),你可以在Stack的子Widget中使用Positioned Widget来定位子Widget相对于Stack Widget上、下、左、右四条边的位置。Stack Widget是基于Web开发中的绝对定位布局模型设计的;
  • Container:Container Widget是你可以创建一个矩形的可视元素,一个Container可以被一个BoxDecoration装饰,比如背景(background)、边框(border)、阴影(shadow)。Container也具有margin、padding和应用于其大小的约束(Constraints)。另外,Container可以使用矩阵在三维空间中进行变换。

以下是一些简单的Widget相互组合为新的Widget的例子:

import 'package:flutter/material.dart';
class MyAppBar extends StatelessWidget {
MyAppBar({this.title});
// Widget子类中的字段通常被定义为final的
final Widget title;
@override
Widget build(BuildContext context) {
return Container(
height: 56.0, // 单位是逻辑上的像素(非绝对像素)
padding: const EdgeInsets.symmetric(horizontal: 8.0),
decoration: BoxDecoration(color: Colors.blue[500]),
// 使用Row构建一个水平方向的线性布局
child: Row(
// <Widget>是下面的列表项的类型
children: <Widget>[
IconButton(
icon: Icon(Icons.menu),
tooltip: 'Navigation menu',
onPressed: null, // null表示忽略/禁用当前IconButton的点击事件
),
// Expanded会填充满可用的所有空间
Expanded(
child: title,
),
IconButton(
icon: Icon(Icons.search),
tooltip: 'Search',
onPressed: null,
),
],
),
);
}
}
class MyScaffold extends StatelessWidget {
@override
Widget build(BuildContext context) {
// Material在概念上相当于一张”纸“ ,用户界面是显示在这张”纸“上面的
return Material(
// Column是一个垂直方向的线性布局
child: Column(
children: <Widget>[
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',
home: MyScaffold(),
));
}

请确保在你的pubspec.yaml文件中声明了uses-material-design:true,这个声明确保我们可以使用一些Material icons:

name: my_App
flutter:
uses-material-design: true

很多Material Design风格的Widget需要处于MaterialApp的内部才能正确地显示,这样才能继承Material不同主题下的数据,所以我们通过MaterialApp来运行App。

上面的代码运行起来的效果是这样的:

一个月带你入门Flutter:UI篇(1)——Widget介绍

image-20190608202119521

可以看到,我们在在MyAppBar中创建一个Container,高度为56像素(逻辑像素),其左侧和右侧均有8像素的填充。在容器内部, MyAppBar使用Row 布局来排列其子项。 中间的Widget被标记为Expanded ,这意味着它会填充尚未被其他子项占用的的剩余可用空间。Expanded可以拥有多个children, 然后使用flex参数来确定他们占用剩余空间的比例。

MyScaffold 通过一个Column Widget ,在垂直方向排列其子项。在Column的顶部,放置了一个MyAppBar实例,将一个Text Widget作为其标题传递给应用程序栏。将Widget作为参数传递给其他Widget是一种强大的机制,它可以让您创建各种复杂的Widget。最后,MyScaffold使用了一个正中间包含一条”Hello world”信息的Expanded来填充剩余的空间,

更多信息请参阅布局篇(正在完成中)。

使用Material 组件

Flutter提供了许多Widgets,可帮助您构建遵循Material Design的应用程序。构建一个Material应用程序应该从使用MaterialApp Widget开始, 它在应用程序的根部创建了许多有用的Widget,其中包括一个Navigator, 它管理由字符串标识的Widget栈(即页面路由栈)。Navigator可以让您的应用程序在页面之间的平滑地切换。 当然,是否使用MaterialApp完全是可选的,但是使用它是一个很好的做法。

import 'package:flutter/material.dart';
void main() {
runApp(MaterialApp(
title: 'Flutter Tutorial',
home: TutorialHome(),
));
}
class TutorialHome extends StatelessWidget {
@override
Widget build(BuildContext context) {
// Scaffold是Material中主要的布局组件
return Scaffold(
appBar: AppBar(
leading: IconButton(
icon: Icon(Icons.menu),
tooltip: 'Navigation menu',
onPressed: null,
),
title: Text('Example title'),
actions: <Widget>[
IconButton(
icon: Icon(Icons.search),
tooltip: 'Search',
onPressed: null,
),
],
),
// body属性值屏幕的主体
body: Center(
child: Text('Hello, world!'),
),
floatingActionButton: FloatingActionButton(
tooltip: 'Add',
child: Icon(Icons.add),
onPressed: null,
),
);
}
}

上面的代码运行效果如下:

一个月带你入门Flutter:UI篇(1)——Widget介绍

屏幕快照 2019-06-08 20.35.42

现在我们的代码已经从MyAppBar和MyScaffold变为了AppBar和 Scaffold, 我们的应用程序现在看起来已经有一些“Material”了!例如,应用栏有一个阴影,标题文本会自动继承正确的样式。我们还添加了一个浮动操作按钮,以便进行相应的操作处理。

请注意,我们再次将Widget作为参数传递给其他Widget。Scaffold Widget 会以命名参数的形式接收许多不同的Widget参数,传递给Scaffold的每一个Widget参数对应的Widget都会被放置在Scaffold布局中相应的位置,类似的,AppBar中,我们也给参数leading、actions、title分别传一个Widget作为参数。 这种模式在整个框架中会经常出现,这也可能是您在设计自己的Widget时会考虑到一点。

处理手势

大多数应用程序都包括某种形式的用户与系统的交互。构建交互式应用程序的第一步是检测用户输入的手势。让我们通过创建一个简单的按钮来了解这是如何工作的:

class MyButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () {
print('MyButton was tApped!');
},
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'),
),
),
);
}
}

这里的这个GestureDetector Widget并不具有显示效果(即用户看不到),而是检测由用户的手势。 当用户点击其Child时(这里是Container), GestureDetector会调用它的onTap回调方法, 在回调方法中,将消息打印到控制台。您可以使用GestureDetector来检测各种输入手势,包括点击、拖动和缩放。

许多Widget都会使用一个GestureDetector为其他Widget提供可选的回调。 例如,IconButton、 RaisedButton、 和FloatingActionButton,它们都有一个onPressed回调,它会在用户点击该Widget时被触发。

我们将上面代码所描述的这个Button放置在屏幕中间:

一个月带你入门Flutter:UI篇(1)——Widget介绍

image-20190608204320884

点击按钮之后:

一个月带你入门Flutter:UI篇(1)——Widget介绍

image-20190608204620163

改变Widget状态以响应户的输入

到目前为止,我们只使用了无状态的Widget。无状态Widget从它们的父Widget接收参数, 这些参数被存储在final型的成员变量中。 当一个Widget被要求构建时,它使用这些存储的值作为参数来构建Widget。

为了构建更复杂的体验 (例如,对用户输入事件做出响应), 应用程序通常会携带一些状态。 Flutter使用StatefulWidgets来满足这种需求。StatefulWidgets是特殊的Widget,它知道如何生成State对象,然后用它来保持状态。 下面是个简单的例子,其中使用了前面提到RaisedButton:

class Counter extends StatefulWidget {
//这个类的作用是配置状态(State),它保存着由父级提供并由状态的build()方法使用的值(在本例中暂时没有),Widget子类中的字段总是标记为“final”。
@override
_CounterState createState() => _CounterState();
}
class _CounterState extends State<Counter> {
int _counter = 0;
void _increment() {
setState(() {
/* 调用setState()的作用是告诉flutter框架此状态发生了一些更改,这将导致下面的build()方法被重新调用,以便显示更新后的值。如果你调用setState()方法而直接改变_counter的值,则不会调用build方法,自然界面上什么都不会改变。*/
_counter++;
});
}
@override
Widget build(BuildContext context) {
/**
每次调用setState()时都会重新运行此方法(例如,调用上面的_increment方法)
Flutter框架对build()方法的运行进行了优化,使得其可以快速运行,这样你就可以只重建任何需要更新的东西,不必单独更改需要更新的Widget的实例。
*/
return Row(
children: <Widget>[
RaisedButton(
onPressed: _increment,
child: Text('Increment'),
),
Text('Count: $_counter'),
],
);
}
}

您可能想知道为什么StatefulWidget和State是两个不同的对象。在Flutter中,这两种类型的对象具有不同的生命周期: Widget是临时对象,用于构建当前状态下的应用程序(也就是说使用每调用一次build(),都会产生新的Widget对象以替换旧的Widget对象),而State对象在多次调用build()时保持不变,允许它们记住信息(状态)。

上面的例子接受用户点击,并在点击时使_counter自增,然后直接在其build方法中使用_counter值。在更复杂的应用程序中,Widget结构层次的不同部分可能有不同的职责。例如,一个Widget可能呈现一个复杂的用户界面,其目标是收集特定信息(如日期或位置),而另一个Widget可能会使用该信息来更改整体的显示。

在Flutter中,事件流是“向上”传递的,而状态流是“向下”传递的即子Widget到父Widget是通过事件通信,而父到子是通过状态进行通信,重定向这一流程的共同父元素是State。让我们看看这个稍微复杂的例子是如何工作的:

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<Counter> {
int _counter = 0;
void _increment() {
setState(() {
++_counter;
});
}
@override
Widget build(BuildContext context) {
return Row(children: <Widget>[
CounterIncrementor(onPressed: _increment),
CounterDisplay(count: _counter),
]);
}
}

注意我们是如何创建了两个新的无状态Widget的,我们分离了显示计数器(CounterDisplay)和更改计数器(CounterIncrementor)的逻辑, 尽管最终效果与前一个示例相同,但责任分离允许将复杂性逻辑封装在各个单个的Widget中,同时保持父Widget的简单性。

将所有内容整合到一起

让我们考虑一个更完整的例子,将上面介绍的概念汇集在一起。我们假设一个购物应用程序,该应用程序显示出售的各种产品,并维护一个购物车。 我们先来定义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: ObjectKey(product));
final Product product;
final bool inCart;
final CartChangedCallback onCartChanged;
Color _getColor(BuildContext context) {
// 由于Widget树的不同部分可以有不同的主题(theme),所以这里需要一个context来标识build()方法对应在界面上的位置,这样就能确定当前的主题(theme)。
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: () {
onCartChanged(product, !inCart);
},
leading: CircleAvatar(
backgroundColor: _getColor(context),
child: Text(product.name[0]),
),
title: Text(product.name, style: _getTextStyle(context)),
);
}
}

该ShoppingListItem Widget 是无状态的。它将其在构造函数中接收到的值存储在final成员变量中,然后在build函数中使用它们。 例如,inCart布尔值表示在两种视觉展示效果之间切换:一个使用当前主题的主色,另一个使用灰色。

当用户点击列表项(ListView Item)时,Widget不会直接修改其inCart的值。相反,Widget会调用其父Widget给它的onCartChanged回调函数。 此模式可让您在Widget层次结构中存储更高的状态,从而使状态持续更长的时间。在极端情况下,存储传给runApp()应用程序的Widget的状态将在的整个程序的生命周期中持续存在。

当父Item收到onCartChanged回调时,父Item将更新其内部状态,这将触发父Item使用新inCart值重建ShoppingListItem实例。 虽然父 ShoppingListItem在重建时创建了一个新实例,但该操作开销很小,因为Flutter框架会将新构建的Widget与先前构建的Widget进行比较,并仅将差异部分应用于底层的RenderObject。

我们来看看父Widget存储可变状态的示例:

class ShoppingList extends StatefulWidget {
ShoppingList({Key key, this.products}) : super(key: key);
final List<Product> products;
/**
框架在Widget树中的给定位置首次出现小部件时调用CreateState。如果父对象重建并使用相同类型的Widget(具有相同的Key),框架将重用状态对象,而不是创建新的状态对象。
*/
@override
_ShoppingListState createState() => _ShoppingListState();
}
class _ShoppingListState extends State<ShoppingList> {
Set<Product> _shoppingCart = Set<Product>();
void _handleCartChanged(Product product, bool inCart) {
setState(() {
/**
当用户更改购物车中的内容时,您需要在setstate()中更改购物车以触发rebuild。然后框架会调用下面build(),更新应用程序的外观。
*/
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>[
Product(name: 'Eggs'),
Product(name: 'Flour'),
Product(name: 'Chocolate chips'),
],
),
));
}

ShoppingList类继承自StatefulWidget,这意味着这个Widget可以存储状态。 当ShoppingList首次插入到Widget树中时,框架会调用其 createState 函数以创建一个新的_ShoppingListState实例来与该Widget树中的相应位置关联(请注意,我们通常命名State子类时带一个下划线,这表示其是私有的)。 当这个Widget的父级Widget重建时,父级Widget将创建一个新的ShoppingList实例,Flutter框架将重用已经在Widget树中的_ShoppingListState实例,而不是再次调用createState创建一个新的。

要访问当前ShoppingList的属性,_ShoppingListState可以借助它的widget属性(比如上面代码中的widget.products)。

处理onCartChanged回调时,_ShoppingListState通过添加或删除产品来改变其内部_shoppingCart状态。 为了通知框架它内部的状态发生了改变,需要调用setState()方法。调用setState将该Widget标记为”dirty”(脏的),并且计划在下次应用程序需要更新屏幕时重新构建它。 如果在修改Widget的内部状态后忘记调用setState,框架将不知道您的Widget是”dirty”(脏的),并且可能不会调用Widget的build方法,这意味着用户界面可能不会更新以展示新的状态。

通过以这种方式管理状态,您不需要编写用于创建和更新子Widget的单独代码。相反,您只需实现可以处理这两种情况的build()函数。

上述代码运行起来后如下图所示:

一个月带你入门Flutter:UI篇(1)——Widget介绍

image-20190608212911411

响应Widget的生命周期事件

在StatefulWidget调用createState之后,框架将新的状态对象插入Widget树中,然后调用state对象的initState()方法。 子类State可以重写initState()以完成一些仅需要执行一次的工作。 例如,您可以重写initState()以配置动画或进行一些全局配置。重写initState()的时候需要注意调用super.initState()。

当一个state对象不再需要时,框架会调用该state对象的dispose()方法。 您可以重写该dispose()方法来执行清理工作。例如,您可以重写dispose()方法以取消定时器。 dispose()典型的实现是直接调用super.dispose()。

Key

使用key可以控制当前Widget在重建时与哪个其他的Widget匹配(比如,当前有一个Widget A1和Widget B1,key的作用就是当下一次build()发生时,Widget A2是应该基于WidgetA1重建还是基于Widget B1重建)。默认情况下,Flutter框架根据Widget的runtimeType和它们的显示顺序来匹配。 使用key时,框架要求两个Widget具有相同的key和runtimeType.

Key在构建相同类型Widget的多个实例时很有用。例如,ShoppingList构建很多个ShoppingListItem实例以填充其可见区域时:

  • 如果没有key,当前构建(build)中的第一个条目(Item)将始终与前一个构建(build)中的第一个条目同步,即使在语义上,列表中的第一个条目如果滚动出屏幕,那么它将不会再在窗口中可见。
  • 通过给列表中的每个条目分配为“语义” key,无限列表可以更高效,因为框架将同步条目与匹配的语义key并因此具有相似(或相同)的可视外观。 此外,语义上同步条目意味着在有状态子Widget中,保留的状态将附加到相同的语义条目上,而不是附加到相同数字位置上的条目。

Global Key

您可以使用全局key来唯一标识子Widget。全局key在整个Widget层次结构中必须是全局唯一的,这与局部key不同,后者只需要在同级中唯一。由于它们是全局唯一的,因此可以使用全局key来检索与Widget关联的状态。

未经允许不得转载:PHP100中文网 - 中国第一档PHP资源分享门户 » 带你入门Flutter:UI篇——Widget介绍

赞 (0) 打赏

评论 0

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

微信扫一扫打赏