欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Flutter第一部分(UI)第一篇:初识Widget

程序员文章站 2022-03-11 11:06:07
...

前言:Flutter系列的文章我应该会持续更新至少一个月左右,从User Interface(UI)到数据相关(文件、数据库、网络)再到Flutter进阶(平台特定代码编写、测试、插件开发等),欢迎感兴趣的读者持续关注(可以扫描左边栏二维码或者微信搜索”IT工匠“关注微信公众号哦,会同步推送)。

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

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)第一篇:初识Widget

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

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

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

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

基础Widget

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

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

可以看到,我们在在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)第一篇:初识Widget

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

请注意,我们再次将Widget作为参数传递给其他WidgetScaffold Widget 会以命名参数的形式接收许多不同的Widget参数,传递给Scaffold的每一个Widget参数对应的Widget都会被放置在Scaffold布局中相应的位置,类似的,AppBar中,我们也给参数leadingactionstitle分别传一个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提供可选的回调。 例如,IconButtonRaisedButton、 和FloatingActionButton ,它们都有一个onPressed回调,它会在用户点击该Widget时被触发。

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

Flutter第一部分(UI)第一篇:初识Widget

点击按钮之后:

Flutter第一部分(UI)第一篇:初识Widget

改变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'),
      ],
    );
  }
}

您可能想知道为什么StatefulWidgetState是两个不同的对象。在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”(脏的),并且可能不会调用Widgetbuild方法,这意味着用户界面可能不会更新以展示新的状态。

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

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

Flutter第一部分(UI)第一篇:初识Widget

响应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 A1Widget B1key的作用就是当下一次build()发生时,Widget A2是应该基于WidgetA1重建还是基于Widget B1重建)。默认情况下,Flutter框架根据WidgetruntimeType和它们的显示顺序来匹配。 使用key时,框架要求两个Widget具有相同的keyruntimeType.

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

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

Global Key

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

相关标签: flutter widget