Flutter速来系列3: 被提及了一万遍的Widget

1,271 阅读11分钟

Widget

Widget

Widget

Widget

在Fluter的世界一切都是Widget,所以这个玩意,是空气,是金钱,是上帝,是Flutter生活之必须。

每个写Flutter的都天天敲打他,他都豆豆还辛苦,吃饭睡觉打Widget。

那么,今天。还是要无聊的说一下widget这个破东西。


Widget的分类

在Flutter中,Widget主要分为两种:Stateless WidgetStateful Widget。

无状态 Stateless Widget

Stateless Widget是简单的Widget,它描述了一种在给定配置下的固定视图。一旦创建,Stateless Widget的UI就不会发生变化。例如,一个图标(Icon)就是一个Stateless Widget。

有状态 Stateful Widget

相比之下,Stateful Widget可以在生命周期内进行变化。这是通过与一个独立的State对象进行交互来实现的。当State对象改变时,Stateful Widget的UI就会更新。例如,一个复选框(Checkbox)就是一个Stateful Widget。

有状态 Stateful Widget本质也是不可变的

在Flutter中,Widget是一切的基础。无论你是要创建一个按钮,一个文字,还是一个滑动列表,都是通过Widget来实现的。它们都是不可变的,并且具有较短的生命周期。换句话说,你不能直接改变一个Widget,而是应该通过创建一个新的Widget来更新UI。

每一个Widget都有一个build方法,这是其核心部分。build方法描述了Widget如何根据给定的配置和状态构建自己的UI。

你不能直接改变一个Widget,而是应该通过创建一个新的Widget来更新UI。 对于这句话,是否有疑惑呢?

改变一个Stateful Widget"时,实际上是指改变与该Widget关联的State对象

在Flutter中,Widget本身确实是不可变的,包括Stateful Widget。当我们说"改变Widget"时,实际上是指创建一个新的Widget实例,并可能会与新的状态对象关联。

对于Stateful Widget,虽然Widget本身是不可变的,但它与一个State对象关联,这个State对象可以在Widget的生命周期中改变。当我们说"改变一个Stateful Widget"时,实际上是指改变与该Widget关联的State对象。

所以,当我们要更新UI时,实际上是通过改变State对象,然后创建新的Widget来实现的。这是Flutter框架的工作原理,也是其性能优化的一个重要方面。

举个例子,假设你有一个带有计数器的Stateful Widget。当用户点击按钮时,你可能会更新State对象(也就是计数器的值)。Flutter框架将会创建一个新的Widget,并根据新的State对象(新的计数器值)来构建UI。这个过程中,Stateful Widget的build方法就起到了关键作用,它描述了如何根据新的State对象来构建UI。

Stateless Widget 无状态

在Flutter开发中,Stateless Widget(无状态小部件)是一种非常重要且常用的组件。它提供了一种简单的方式来构建不需要维护状态的UI元素,非常适用于静态内容的展示和简单交互。

什么是Stateless Widget?

Stateless Widget是Flutter框架中的一个概念,它代表了一个不可变的UI组件。这意味着Stateless Widget在创建后不会发生任何改变,其内部的属性和状态都是不可变的。由于不需要维护状态,Stateless Widget通常被用于展示静态内容,例如文本、图像等。

与Stateful Widget(有状态小部件)相比,Stateless Widget更加简单且易于使用。它没有生命周期方法,不需要处理状态变化,只需要根据传入的属性进行渲染,因此具有更高的性能。

下面我们通过多个代码例子来详细说明Stateless Widget的使用方法和场景。

代码例子1:简单的文本展示

import 'package:flutter/material.dart';

class MyTextWidget extends StatelessWidget {
  final String text;

  MyTextWidget(this.text);

  @override
  Widget build(BuildContext context) {
    return Text(
      text,
      style: TextStyle(fontSize: 16.0),
    );
  }
}

上述代码展示了一个简单的文本展示的Stateless Widget。它接收一个字符串作为参数,然后使用Text小部件将该字符串以16号字体大小展示出来。

在这个例子中,Stateless Widget非常适合用于展示静态的文本内容,因为它不需要维护任何状态。

代码例子2:按钮点击事件

import 'package:flutter/material.dart';

class MyButtonWidget extends StatelessWidget {
  final VoidCallback onPressed;

  MyButtonWidget(this.onPressed);

  @override
  Widget build(BuildContext context) {
    return RaisedButton(
      onPressed: onPressed,
      child: Text('Click me'),
    );
  }
}

上述代码展示了一个简单的按钮点击事件的Stateless Widget。它接收一个回调函数作为参数,并将该回调函数绑定到RaisedButton小部件的onPressed属性上。

Stateless Widget在这种场景下非常适用,因为它只需要根据传入的回调函数进行UI渲染,不需要维护任何状态。这使得开发者可以专注于处理按钮点击事件的逻辑,而无需关心组件的状态管理。

生命周期和适用场景

由于Stateless Widget没有状态,因此它没有明确的生命周期。它的构建方法build()在每次需要渲染时都会被调用。

Stateless Widget适用于以下场景:

  • 静态内容展示:例如文本、图像、图标等静态UI元素。
  • 简单交互:例如按钮点击事件、路由跳转等简单交互逻辑。
  • 子组件:作为其他有状态小部件的子组件,用于构建复杂的UI结构。

在这些场景中,Stateless Widget的性能较好,代码结构清晰,易于理解和维护。

在日常开发中的典型使用

以下是一些日常开发中常见的使用Stateless Widget的例子:

例子1:静态文本展示

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: Text('My App'),
        ),
        body: Center(
          child: MyTextWidget('Hello, Flutter!'),
        ),
      ),
    );
  }
}

在这个例子中,我们使用Stateless Widget MyTextWidget 来展示一个静态文本。

例子2:登录页面

class LoginPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Login'),
      ),
      body: Container(
        padding: EdgeInsets.all(16.0),
        child: Column(
          children: [
            TextField(
              decoration: InputDecoration(
                labelText: 'Username',
              ),
            ),
            TextField(
              decoration: InputDecoration(
                labelText: 'Password',
              ),
              obscureText: true,
            ),
            RaisedButton(
              onPressed: () {
                // 处理登录逻辑
              },
              child: Text('Login'),
            ),
          ],
        ),
      ),
    );
  }
}

在这个例子中,我们使用Stateless Widget构建了一个简单的登录页面。其中包含了两个文本输入框和一个登录按钮,通过Stateless Widget的方式来组织UI结构,使得代码更加清晰易读。

通过以上例子,我们可以看到Stateless Widget在日常开发中的典型使用方式,它简单、高效,并能提升代码的可读性和可维护性。

Stateful Widget

什么是State?

首先,我们需要理解什么是State(状态)。在Flutter中,State就是数据。这些数据可以影响UI,当数据改变时,UI就会重建,反映新的信息。一个Widget的State,就是它持有的数据,以及这些数据如何影响UI的方式。

对于Stateful Widget来说,它们的状态就是我们要管理的核心。每个Stateful Widget都有一个与之关联的State对象。此对象在Widget的整个生命周期中都存在。

生命周期

Stateful Widget的生命周期主要包括以下几个步骤:

  1. createState(): 这是在插入widget时调用的。它返回一个新的State对象。
  2. initState(): 在创建State对象后,此方法会被立即调用。这是初始化数据或启动动画的地方。
  3. build(): 这是Flutter构建UI的主要方式。当你的数据发生改变时,Flutter会调用build方法,然后根据新的数据重建UI。
  4. dispose(): 当Widget被从树中移除时,dispose方法被调用,这是一个清理资源的好地方。

下面是一个简单的例子:

class ExampleWidget extends StatefulWidget {
  @override
  _ExampleWidgetState createState() => _ExampleWidgetState();
}

class _ExampleWidgetState extends State<ExampleWidget> {
  int counter = 0; // 这就是我们的state,一个简单的计数器

  @override
  void initState() {
    super.initState();
    // 初始化状态
    counter = 0;
  }

  @override
  Widget build(BuildContext context) {
    // 每次我们的计数器改变,build方法都会被调用,UI会被重建
    return FlatButton(
      child: Text('我被按了 $counter 次'),
      onPressed: () {
        setState(() {
          // 改变状态
          counter++;
        });
      },
    );
  }

  @override
  void dispose() {
    // 清理工作
    super.dispose();
  }
}

获取State对象

有两种主要的方法可以获取State对象:

  1. context.findAncestorStateOfType<_ExampleWidgetState>(): 从当前的context开始向上遍历,直到找到对应类型的State对象。
_ExampleWidgetState state = context.findAncestorStateOfType<_ExampleWidgetState>();
  1. GlobalKey: 创建一个全局键,然后在我们需要的时候,使用这个键来获取状态。
GlobalKey<_ExampleWidgetState> globalKey = GlobalKey();

// 你可以使用这个globalKey创建你的Widget
ExampleWidget(key: globalKey);

// 然后,你可以在任何地方使用这个globalKey来获取状态
_ExampleWidgetState state = globalKey.currentState;

请注意,过度使用GlobalKey可能会导致性能问题。所以在可能的情况下,尽量避免使用它。

如何创建Stateful Widget

要创建一个Stateful Widget,你需要定义两个类:一个扩展StatefulWidget,另一个扩展State。下面的代码显示了一个基本的Stateful Widget结构:

class MyStatefulWidget extends StatefulWidget {
  @override
  _MyStatefulWidgetState createState() => _MyStatefulWidgetState();
}

class _MyStatefulWidgetState extends State<MyStatefulWidget> {
  // 这里定义你的状态

  @override
  Widget build(BuildContext context) {
    // 这里返回你的Widget
  }
}

MyStatefulWidget是Widget本身,而_MyStatefulWidgetState类则是存储Widget状态的地方。

createState函数是StatefulWidget中必须实现的方法,它返回一个State对象,该对象在Widget的生命周期中持久存在。

build函数则是在State类中必须实现的,它描述了Widget在给定其当前配置和状态时应如何显示。

使用Stateful Widget

我们可以通过实现一个简单的计数器来了解如何使用Stateful Widget。我们在State类中创建一个变量作为计数器,然后在每次点击按钮时更新它。

class CounterWidget extends StatefulWidget {
  @override
  _CounterWidgetState createState() => _CounterWidgetState();
}

class _CounterWidgetState extends State<CounterWidget> {
  int _counter = 0; // 这就是我们的状态——一个简单的计数器

  void _incrementCounter() {
    setState(() { // 使用setState来告知Flutter状态已经改变,需要重建UI
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('计数器示例'),
      ),
      body: Center(
        child: Text(
          '你已经点击了 $_counter 次',
          style: TextStyle(fontSize: 24),
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: '增加',
        child: Icon(Icons.add),
      ),
    );
  }
}

在这个例子中,我们使用setState方法来更新状态。setState将触发UI的重新构建,因此每次点击按钮,计数器就会增加,同时在屏幕上显示的数字也会更新。

Stateful Widget的优点

Stateful Widget是Flutter的核心,它们允许我们创建交互式的应用程序。它们的优点主要体现在以下几个方面:

  1. 持久性:Stateful Widget能够在多次调用build方法之间保持状态,这让我们可以在用户与应用程序的交互过程中保持持久性的信息。
  2. 交互性:Stateful Widget能够响应用户的操作,如点击按钮、滑动屏幕等,然后根据这些操作来更新UI。
  3. 动画:Stateful Widget是创建动画的基础,因为动画需要随着时间的推移改变状态,并显示这些改变。

下面是一个实际的例子,一个简单的购物车应用程序:

class ShoppingCart extends StatefulWidget {
  @override
  _ShoppingCartState createState() => _ShoppingCartState();
}

class _ShoppingCartState extends State<ShoppingCart> {
  List<String> cartItems = []; // 购物车中的商品

  void addItem(String item) {
    setState(() {
      cartItems.add(item);
    });
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      children: <Widget>[
        // 显示购物车中的商品
        Expanded(
          child: ListView.builder(
            itemCount: cartItems.length,
            itemBuilder: (context, index) {
              return ListTile(
                title: Text(cartItems[index]),
              );
            },
          ),
        ),
        // 添加商品的按钮
        RaisedButton(
          child: Text('添加商品'),
          onPressed: () => addItem('商品 ${cartItems.length + 1}'),
        ),
      ],
    );
  }
}

自定义Stateful Widget

自定义Stateful Widget的过程非常简单。你只需要继承StatefulWidget类,并实现一个返回新的State对象的createState方法。下面是一个简单的计数器示例:

class CustomCounter extends StatefulWidget {
  @override
  _CustomCounterState createState() => _CustomCounterState();
}

class _CustomCounterState extends State<CustomCounter> {
  int counter = 0;

  @override
  Widget build(BuildContext context) {
    return Column(
      children: <Widget>[
        Text('你已经点击了 $counter 次'),
        RaisedButton(
          child: Text('点击我'),
          onPressed: () {
            setState(() {
              counter++;
            });
          },
        ),
      ],
    );
  }
}

Stateful Widget和Stateless Widget的对比

Stateful Widget和Stateless Widget是Flutter的两个基础类,它们都是用于创建自定义Widget的。主要区别在于Stateful Widget能够保持状态,而Stateless Widget不能。

一个Stateful Widget可以在用户与其交互时(如点击按钮,滑动屏幕)或者当它的内部状态改变时(如动画)重绘自己。另一方面,Stateless Widget一旦创建,其所有配置都是最终的,它不会在其生命周期中发生改变。这意味着,如果Widget的外观在其生命周期中需要改变,你可能需要使用Stateful Widget。

什么是BuildContext?

在Flutter中,所有的UI都是由Widget构成的。这些Widget按照特定的顺序和结构组织在一起,形成了一个Widget树。

BuildContext是一个代表Widget在Widget树中的位置的引用。每个Widget都有一个BuildContext,这个上下文在Widget的生命周期内保持不变。BuildContext可以让你访问和操作Widget树中的特定数据。

BuildContext如何工作?

让我们用一个例子来了解BuildContext如何工作的。假设我们有以下的Widget树:

MaterialApp
└── Scaffold
    ├── AppBar
    └── Column
        ├── Text
        └── RaisedButton

在这个例子中,每个Widget都有一个BuildContext,代表它在这个树中的位置。你可以把BuildContext想象成一个指向Widget的指针,它可以让你访问这个Widget的父Widget、子Widget、主题等等。

例如,你可能已经使用过这样的代码来导航到新的页面:

Navigator.of(context).push(MaterialPageRoute(builder: (context) => NewPage()));

在这段代码中,Navigator.of(context)会向上遍历Widget树,找到最近的Navigator Widget。然后,push方法会在这个Navigator上添加一个新的路由。

使用BuildContext

现在我们已经知道了BuildContext是什么以及它如何工作,那么我们应该如何使用它呢?

在大多数情况下,你会在build方法中使用BuildContext。这个BuildContext代表的是正在构建的Widget在Widget树中的位置。你可以用它来访问你的应用中的许多信息,比如主题、MediaQuery等等。

下面是一个例子:

Widget build(BuildContext context) {
  // 你可以通过BuildContext来访问主题数据
  final theme = Theme.of(context);
  
  return Container(
    color: theme.primaryColor,
  );
}

在上面的例子中,我们使用BuildContext来访问主题数据,并将容器的颜色设置为主题的主色。

InheritedWidget

InheritedWidget是一个非常强大的构件,可以让你在Widget树中有效地传递数据。

什么是InheritedWidget?

在Flutter中,Widgets是不可变的。这意味着一旦你创建了一个Widget,就不能再更改它。这在许多情况下非常有用,但是当你需要在Widget树中的多个地方共享同一份数据时,这就可能成为一个问题。

这就是InheritedWidget发挥作用的地方。InheritedWidget是一个特殊类型的Widget,它可以在它的子Widget树中共享数据。

如何使用InheritedWidget?

让我们通过一个例子来看看如何使用InheritedWidget。假设我们在应用中有一个主题颜色,我们希望在应用的多个地方使用这个颜色。

首先,我们创建一个InheritedWidget:

class ThemeColor extends InheritedWidget {
  final Color color;

  ThemeColor({
    Key key,
    @required this.color,
    @required Widget child,
  }) : super(key: key, child: child);

  @override
  bool updateShouldNotify(ThemeColor old) => color != old.color;

  static ThemeColor of(BuildContext context) {
    return context.dependOnInheritedWidgetOfExactType<ThemeColor>();
  }
}

在这个InheritedWidget中,我们有一个color属性来存储主题颜色。我们还提供了一个静态的of方法来让子Widget可以方便地访问这个InheritedWidget。

我们可以像下面这样使用这个InheritedWidget:

void main() {
  runApp(
    ThemeColor(
      color: Colors.blue,
      child: MyApp(),
    ),
  );
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final themeColor = ThemeColor.of(context).color;

    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          backgroundColor: themeColor,
          title: Text('InheritedWidget示例'),
        ),
        body: Center(
          child: Text(
            'Hello, Flutter!',
            style: TextStyle(color: themeColor),
          ),
        ),
      ),
    );
  }
}

在这个例子中,我们创建了一个ThemeColor,并将MyApp设置为它的子Widget。然后,在MyApp中,我们可以使用ThemeColor.of(context).color来获取我们在ThemeColor中定义的颜色。

结论

InheritedWidget 是一个强大的工具,让我们可以在 Widget 树中有效地共享数据。理解和使用InheritedWidget可以让你更好地管理你的应用的状态,提高你的代码的可读性和可维护性。