MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Flutte有状态Widget的工作原理剖析

2021-08-087.7k 阅读

Flutter有状态Widget基础概念

在Flutter开发中,Widget是构建用户界面的基本元素。而有状态Widget(StatefulWidget)则是其中一类特殊的Widget,它能够在运行时改变自身状态。

简单来说,状态就是Widget在不同时刻所呈现出的数据或条件。比如一个计数器,计数值就是它的状态;再比如一个开关按钮,开或关的状态就是其状态信息。

有状态Widget由两部分组成:StatefulWidget类本身和对应的State类。StatefulWidget主要负责创建State对象,而State类则负责管理Widget的状态以及处理状态变化时的UI更新。

下面通过一个简单的计数器示例来初步认识有状态Widget。

import 'package:flutter/material.dart';

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

class _CounterWidgetState extends State<CounterWidget> {
  int _count = 0;

  void _increment() {
    setState(() {
      _count++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Text('Count: $_count'),
        ElevatedButton(
          onPressed: _increment,
          child: Text('Increment'),
        )
      ],
    );
  }
}

在上述代码中,CounterWidget 是一个有状态Widget,它通过 createState 方法创建了 _CounterWidgetState_CounterWidgetState 中定义了状态变量 _count,并且提供了 _increment 方法来更新状态。在 build 方法中,根据 _count 的值构建UI。每次点击按钮调用 _increment 方法时,通过 setState 通知Flutter框架状态发生了变化,从而触发UI的重新构建。

有状态Widget的生命周期

  1. 创建阶段
    • 构造函数:当StatefulWidget被创建时,首先会调用其构造函数。在构造函数中,可以初始化一些基本的属性,但此时State对象还未创建。
    • createState:这是StatefulWidget类中最重要的方法之一,它负责创建与之关联的State对象。这个方法会在Widget插入到Widget树时被调用一次。
  2. 插入阶段
    • initState:在State对象被创建后,Flutter框架会调用 initState 方法。这个方法只在State对象的生命周期中调用一次,通常用于初始化一些一次性的操作,比如订阅流(Stream)、加载数据等。
    • didChangeDependencies:此方法在 initState 之后调用,并且每当State对象依赖的InheritedWidget发生变化时也会被调用。可以在这个方法中获取InheritedWidget的数据。
  3. 更新阶段
    • build:这是State类中最核心的方法,用于构建Widget的UI。每当状态发生变化(通过 setState 触发)或者父Widget的配置(如尺寸、方向等)发生变化时,build 方法都会被调用。
    • didUpdateWidget:当StatefulWidget的配置发生变化时(例如父Widget传递了新的参数),Flutter框架会调用此方法。在这个方法中,可以根据新的配置来更新状态。
  4. 移除阶段
    • deactivate:当Widget从Widget树中移除时,会调用 deactivate 方法。通常在这个方法中取消一些正在进行的操作,如取消流的订阅等。
    • dispose:这是State对象生命周期的最后一个方法,在 deactivate 之后调用。用于释放资源,比如关闭数据库连接、释放动画控制器等。

下面通过一个具体的示例来展示有状态Widget的生命周期方法的调用顺序。

import 'package:flutter/material.dart';

class LifecycleWidget extends StatefulWidget {
  @override
  _LifecycleWidgetState createState() => _LifecycleWidgetState();
}

class _LifecycleWidgetState extends State<LifecycleWidget> {
  @override
  void initState() {
    super.initState();
    print('initState called');
  }

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    print('didChangeDependencies called');
  }

  @override
  Widget build(BuildContext context) {
    print('build called');
    return Container();
  }

  @override
  void didUpdateWidget(LifecycleWidget oldWidget) {
    super.didUpdateWidget(oldWidget);
    print('didUpdateWidget called');
  }

  @override
  void deactivate() {
    super.deactivate();
    print('deactivate called');
  }

  @override
  void dispose() {
    super.dispose();
    print('dispose called');
  }
}

在上述代码中,每个生命周期方法中都添加了打印语句。通过在应用中观察控制台输出,可以清晰地看到这些方法的调用顺序。

状态管理与setState

  1. 状态管理的重要性 在有状态Widget中,有效地管理状态是保证应用正常运行和良好用户体验的关键。合理的状态管理可以使代码结构清晰,易于维护和扩展。例如,在一个复杂的表单应用中,管理表单的输入状态、验证状态等,能够确保用户输入的正确性并且在不同状态下展示合适的UI。
  2. setState原理 setState 是State类中的一个方法,它的作用是通知Flutter框架状态发生了变化,从而触发UI的重新构建。当调用 setState 时,Flutter框架会标记该State对象为脏(dirty),然后在下一帧绘制时,会重新调用 build 方法来重新构建UI。

需要注意的是,setState 方法接受一个函数作为参数,在这个函数中更新状态变量。之所以这样设计,是因为Flutter框架需要在这个函数执行完毕后,根据状态的变化来决定如何重新构建UI。如果直接在 setState 之外更新状态变量,而不调用 setState,Flutter框架不会知道状态已经改变,也就不会触发UI的重新构建。

以下代码展示了 setState 的正确使用方式和错误使用方式。

import 'package:flutter/material.dart';

class SetStateExample extends StatefulWidget {
  @override
  _SetStateExampleState createState() => _SetStateExampleState();
}

class _SetStateExampleState extends State<SetStateExample> {
  int _number = 0;

  // 正确使用setState更新状态
  void _incrementCorrectly() {
    setState(() {
      _number++;
    });
  }

  // 错误使用,直接更新状态而不调用setState
  void _incrementIncorrectly() {
    _number++;
    // 这里没有调用setState,UI不会更新
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Text('Number: $_number'),
        ElevatedButton(
          onPressed: _incrementCorrectly,
          child: Text('Increment Correctly'),
        ),
        ElevatedButton(
          onPressed: _incrementIncorrectly,
          child: Text('Increment Incorrectly'),
        )
      ],
    );
  }
}

在上述代码中,点击 “Increment Correctly” 按钮会正确更新UI,因为调用了 setState;而点击 “Increment Incorrectly” 按钮,虽然 _number 变量的值增加了,但由于没有调用 setState,UI不会更新。

有状态Widget与无状态Widget的对比

  1. 无状态Widget特点 无状态Widget(StatelessWidget)是不可变的,它的状态在创建时就已经确定,之后不能改变。例如,一个简单的文本标签 Text 就是无状态Widget,一旦创建,其文本内容、样式等就不会再改变。无状态Widget的 build 方法只会在Widget第一次插入到Widget树时被调用一次。
  2. 有状态Widget特点 有状态Widget与之相反,它可以在运行时改变状态。如前面的计数器示例,计数值可以随着按钮点击而变化。有状态Widget的 build 方法会在状态改变或父Widget配置改变时被多次调用。
  3. 应用场景
    • 无状态Widget:适用于那些不需要改变状态的UI元素,比如静态文本、图标等。它们的不可变性使得性能优化更容易,因为不需要处理状态变化带来的复杂情况。
    • 有状态Widget:用于需要动态改变状态的场景,如表单输入、动画效果控制等。通过合理管理状态,能够实现丰富的用户交互功能。

下面通过一个简单的示例对比两者的使用场景。

import 'package:flutter/material.dart';

// 无状态Widget示例
class StaticText extends StatelessWidget {
  final String text;
  StaticText({required this.text});

  @override
  Widget build(BuildContext context) {
    return Text(text);
  }
}

// 有状态Widget示例
class DynamicText extends StatefulWidget {
  @override
  _DynamicTextState createState() => _DynamicTextState();
}

class _DynamicTextState extends State<DynamicText> {
  String _text = 'Initial Text';

  void _updateText() {
    setState(() {
      _text = 'Updated Text';
    });
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Text(_text),
        ElevatedButton(
          onPressed: _updateText,
          child: Text('Update Text'),
        )
      ],
    );
  }
}

在上述代码中,StaticText 是无状态Widget,其文本内容在创建时确定且不可改变;而 DynamicText 是有状态Widget,通过点击按钮可以改变文本内容。

有状态Widget的性能优化

  1. 避免不必要的重建 虽然 setState 是更新状态和UI的有效方式,但如果频繁调用 setState 或者在 build 方法中执行复杂的计算,可能会导致性能问题。为了避免不必要的UI重建,可以将一些不变的计算逻辑移出 build 方法。例如,将一些只依赖于初始化数据的计算放在 initState 中执行。
import 'package:flutter/material.dart';

class PerformanceWidget extends StatefulWidget {
  @override
  _PerformanceWidgetState createState() => _PerformanceWidgetState();
}

class _PerformanceWidgetState extends State<PerformanceWidget> {
  List<int> _dataList = [];

  @override
  void initState() {
    super.initState();
    // 在initState中初始化数据,避免在build方法中重复计算
    _dataList = List.generate(100, (index) => index);
  }

  @override
  Widget build(BuildContext context) {
    return ListView.builder(
      itemCount: _dataList.length,
      itemBuilder: (context, index) {
        return ListTile(
          title: Text('Item ${_dataList[index]}'),
        );
      },
    );
  }
}

在上述代码中,通过在 initState 中生成 _dataList,避免了每次 build 时都重新生成数据,从而提高了性能。 2. 使用GlobalKey 在某些情况下,可能需要直接访问State对象,而不仅仅是通过父Widget传递数据。这时可以使用 GlobalKeyGlobalKey 是一个唯一标识Widget的对象,可以通过它获取到对应的State对象。但需要注意的是,过度使用 GlobalKey 可能会导致代码耦合度增加,应谨慎使用。

import 'package:flutter/material.dart';

class KeyedWidget extends StatefulWidget {
  const KeyedWidget({Key? key}) : super(key: key);

  @override
  _KeyedWidgetState createState() => _KeyedWidgetState();
}

class _KeyedWidgetState extends State<KeyedWidget> {
  int _count = 0;

  void _increment() {
    setState(() {
      _count++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Text('Count: $_count'),
        ElevatedButton(
          onPressed: _increment,
          child: Text('Increment'),
        )
      ],
    );
  }
}

class KeyUsageExample extends StatefulWidget {
  @override
  _KeyUsageExampleState createState() => _KeyUsageExampleState();
}

class _KeyUsageExampleState extends State<KeyUsageExample> {
  final GlobalKey<KeyedWidgetState> _key = GlobalKey();

  void _incrementFromOutside() {
    if (_key.currentState != null) {
      _key.currentState!._increment();
    }
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        KeyedWidget(key: _key),
        ElevatedButton(
          onPressed: _incrementFromOutside,
          child: Text('Increment from Outside'),
        )
      ],
    );
  }
}

在上述代码中,通过 GlobalKey 在外部直接调用了 KeyedWidget_increment 方法来更新状态,展示了 GlobalKey 的使用方式。

嵌套有状态Widget的处理

  1. 状态提升 当有多个嵌套的有状态Widget需要共享状态时,一种常见的做法是将状态提升到它们最近的共同父Widget中。这样可以减少状态管理的复杂性,并且使代码结构更加清晰。例如,在一个包含多个子组件的表单中,子组件可能需要共享表单的整体验证状态。
import 'package:flutter/material.dart';

class ChildWidget extends StatelessWidget {
  final bool isValid;
  final Function onSubmit;

  ChildWidget({required this.isValid, required this.onSubmit});

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Text(isValid? 'Form is valid' : 'Form is invalid'),
        ElevatedButton(
          onPressed: () => onSubmit(),
          child: Text('Submit'),
        )
      ],
    );
  }
}

class ParentWidget extends StatefulWidget {
  @override
  _ParentWidgetState createState() => _ParentWidgetState();
}

class _ParentWidgetState extends State<ParentWidget> {
  bool _isValid = false;

  void _validateForm() {
    // 模拟表单验证逻辑
    setState(() {
      _isValid = true;
    });
  }

  @override
  Widget build(BuildContext context) {
    return ChildWidget(
      isValid: _isValid,
      onSubmit: _validateForm,
    );
  }
}

在上述代码中,ChildWidget 依赖于 ParentWidget 中的 _isValid 状态,通过将状态提升到 ParentWidget,实现了状态的共享和统一管理。 2. 传递回调函数 除了状态提升,还可以通过在父Widget向子Widget传递回调函数的方式,让子Widget通知父Widget状态的变化。子Widget通过调用回调函数,将相关信息传递给父Widget,父Widget再根据这些信息更新状态。

import 'package:flutter/material.dart';

class Child extends StatelessWidget {
  final Function(int) onValueChanged;

  Child({required this.onValueChanged});

  @override
  Widget build(BuildContext context) {
    return ElevatedButton(
      onPressed: () => onValueChanged(42),
      child: Text('Send value'),
    );
  }
}

class Parent extends StatefulWidget {
  @override
  _ParentState createState() => _ParentState();
}

class _ParentState extends State<Parent> {
  int _receivedValue = 0;

  void _handleValueChange(int value) {
    setState(() {
      _receivedValue = value;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Text('Received value: $_receivedValue'),
        Child(onValueChanged: _handleValueChange),
      ],
    );
  }
}

在上述代码中,Child 组件通过 onValueChanged 回调函数将值传递给 Parent 组件,Parent 组件在接收到值后更新自身状态并重新构建UI。

通过深入理解有状态Widget的工作原理,包括其生命周期、状态管理、性能优化以及嵌套处理等方面,开发者能够更加高效地构建复杂且交互性强的Flutter应用程序。在实际开发中,合理运用这些知识可以避免常见的问题,提高代码质量和应用性能。