dart 语言学习日记(3)

dart 语言学习日记(3)

前言

基于第一篇学习日记,继续学习 dart 语言

其主要思路是通过 flutter 中的一个实例,讲一讲 flutter 中构建一个 app 的主要思路

  1. ui 构建
  2. 事件处理

参考资料:

flutterUI

主要内容

代码准备

首先找一个地方放我们的代码, 并初始化一个 flutter 应用,

> cd ~/Study/flutterStudy/
> flutter create flutter_example
> cd flutter_example
> flutter run

根据上述的步骤, 我们创建了一个简单的 flutter 应用, 使用 flutter run 命令即可启动这个简单的应用

接下来, 我们需要在 lib/main.dart 中清空示例代码 我也不知道这样子到底对不对啊

.
├── analysis_options.yaml
├── android
├── build
├── flutter_example.iml
├── ios
├── lib
├── linux
├── macos
├── pubspec.lock
├── pubspec.yaml
├── README.md
├── test
├── web
└── windows

10 directories, 5 files

> tree -L1 ./lib
./lib
└── main.dart # 这个就是我们需要修改的文件

> rm ./lib/main.dart
> touch ./lib/main.dart

接下来, 我们需要在 lib/main.dart 中添加一个简单的代码, 这个代码是一个简单的 flutter 应用

import 'package:flutter/material.dart';

// 定义一个 product 类
class Product {
  const Product({required this.name});

  final String name;
}

typedef CartChangedCallback = Function(Product product, bool inCart);

class ShoppingListItem extends StatelessWidget {
  ShoppingListItem({
    required this.product,
    required this.inCart,
    required this.onCartChanged,
  }) : super(key: ObjectKey(product));

  final Product product;
  final bool inCart;
  final CartChangedCallback onCartChanged;

  // 根据 inCart 的数值来决定颜色
  Color _getColor(BuildContext context) {
    // The theme depends on the BuildContext because different
    // parts of the tree can have different themes.
    // The BuildContext indicates where the build is
    // taking place and therefore which theme to use.

    return inCart //
        ? Colors.black54
        : Theme.of(context).primaryColor;
  }

  // 根据 inCart 的数值来决定文字样式
  TextStyle? _getTextStyle(BuildContext context) {
    if (!inCart) return null;

    return const TextStyle(
      color: Colors.black54,
      decoration: TextDecoration.lineThrough,
    );
  }

  @override
  Widget build(BuildContext context) {
    return ListTile(
      // 当用户点击 ListTile 时候,会调用 onCartChanged 回调函数
      onTap: () {
        // onCartChanged 传递给 ShoppingListItem 的回调函数
        onCartChanged(product, inCart);
      },
      leading: CircleAvatar(
        backgroundColor: _getColor(context),
        child: Text(product.name[0]),
      ),
      title: Text(product.name, style: _getTextStyle(context)),
    );
  }
}

// 继承 StatefulWidget 并抽象出一个 State 对象
class ShoppingList extends StatefulWidget {
  const ShoppingList({required this.products, super.key});

  final List<Product> products;

  // The framework calls createState the first time
  // a widget appears at a given location in the tree.
  // If the parent rebuilds and uses the same type of
  // widget (with the same key), the framework re-uses
  // the State object instead of creating a new State object.

  // 创建一个 State 对象,用于管理 ShoppingList 的状态
  @override
  State<ShoppingList> createState() => _ShoppingListState();
}

// 继承 State 并实现其抽象方法
class _ShoppingListState extends State<ShoppingList> {
  final _shoppingCart = <Product>{};

  void _handleCartChanged(Product product, bool inCart) {
    setState(() {
      // When a user changes what's in the cart, you need
      // to change _shoppingCart inside a setState call to
      // trigger a rebuild.
      // The framework then calls build, below,
      // which updates the visual appearance of the app.

      if (!inCart) {
        _shoppingCart.add(product);
      } else {
        _shoppingCart.remove(product);
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Shopping List')),
      body: ListView(
        padding: const EdgeInsets.symmetric(vertical: 8),
        children:
            widget.products.map((product) {
              return ShoppingListItem(
                product: product,
                inCart: _shoppingCart.contains(product),
                onCartChanged: _handleCartChanged,
              );
            }).toList(),
      ),
    );
  }
}

void main() {
  runApp(
    const MaterialApp(
      title: 'Shopping App',
      // home 默认路由的入口,存一个 widget
      home: ShoppingList(
        products: [
          Product(name: 'Eggs'),
          Product(name: 'Flour'),
          Product(name: 'Chocolate chips'),
        ],
      ),
    ),
  );
}

接下来, 尝试跑一下上面的应用

> flutter run

然后我们就可以看到一个简单的应用, 如下图所示

该应用实现了如下图功能

总结:

该章节准备了一个简单的 flutter 应用,并且顺利的运行了起来

代码结构

接下来,我们分析一下代码的结构,在上文的示例中,我们从 ui 以及事件处理两个方面来分析这个应用

ui

我们先看一看这个应用的 ui 由哪些部分组成

如上图所示,该 app 由两个部分组成,1部分 是 app 的标题,2部分 是 app 的主体内容

我们先聊一聊这两部分的 ui 该如何构建

关于第一部分,我们可以用以下代码实现

import 'package:flutter/material.dart';

void main() {
  runApp(MaterialApp(title: 'Shopping List', home: ShoppingList()));
}

class ShoppingList extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Shopping List')),
      body: const Center(child: Text('Hello World')),
    );
  }
}

使用 flutter run 命令即可得到如下 app

上文代码由一个 main 函数,一个 ShoppingList 类组成, 我你自己看

flutter 的核心概念为使用组件构建自己的 ui , 组件通常会定义其外观以及事件

在上述代码的 main 函数, 将MaterialApp (一个组件), 传入 runApp 方法中,从而构建了一个 app

runApp 提供了 flutter 组件树的根组件,同时传入 MaterialApp 作为根组件下的子组件

Calling [runApp] again will detach the previous root widget from the view and
attach the given widget in its place.

runApp 的函数原型为 Type: void Function(Widget)

MaterialApp 则创建了一个 WidgetApp 的实例,runApp 接受了该实例,从而返回一个组件树,如下图所示

flowchart TD runApp --> MaterialApp

而对于 MaterialApp 而言,也有其必须传入的参数

At least one of [home], [routes], [onGenerateRoute], or [builder] must be non-null.

在本章节代码中,我们传入了 String? title 以及 Widget? home 两个参数

title 参数如字面意思理解即可,而 home 参数则要求我们传入一个 Widget 实例

所以我们创建了一个继承 StatelessWidget 的 ShoppingList

StatelessWidget 类在 flutter 中通常用于构建用户 UI

我们首先查看要实现相关的类需要哪些东西

  • Minimize the number of nodes transitively created by the build method and
    any widgets it creates.
  • Use const widgets where possible, and provide a const constructor for
    the widget so that users of the widget can also do so.

我们需要一个 build 方法来构建一个 Widget ,同时我们也需要一个 const 方法将这个Widget 实例化

build 方法的原型为 Widget build(BuildContext context), 其描述如下

Describes the part of the user interface represented by this widget.

也就是说,该方法用于定义 Widget 的 UI, 该方法返回一个 Widget 类型,并需要传入一个 BuildContext 类型参数

在 UI 方面,我们构建了一个 Scaffold 作为 Widget 的脚手架

在这个脚手架中我们定义了 appBar 以及 body 两个成员

appBar 详情可参考appBar Class

其中的 UI 定义如下图所示

在上述示例代码中,我们定义了 title 为 Shopping List, 也就是上文所提到的 1部分

body 详情可参考body in Scaffold

body 的默认位置在 appBar 下方的左上角,但是我们额外定义了 const Center(child: Text('Hello World'))

所以如 2部分 所示,Hello World 字样在 app 中间

同时我们看一看 build 方法所传入的参数 BuildContext context

The framework calls this method when this widget is inserted into the tree
in a given [BuildContext] and when the dependencies of this widget change
...
The given [BuildContext] contains information about the location in the
tree at which this widget is being built.

在上述文档中,我们可以看到,当 flutter 需要用到 widget 的时候,会自动插入 BuildContext

而 BuildContext 通常用于传入在父节点中定义的 WidgetTree 位置

通过上述代码,我们解释了在 flutter 中 UI 是如何构建的,接下来我们打开 flutter devtool 验证我们的解释是否合理

在 nvim 普通模式中输入 :FlutterDevTools 并打开弹出的 URL, 如下图所示

我们可以看到其中的 widgettree 构成,进入 Select Widget Mode, 点击我们的应用,我们可以看到组件的组织方式

至此,针对上述代码,我们做了比较详尽的解释,而关于 ShoppingListItem 的 UI 构建,参考上文以及文档即可

另外一个重要的概念,就是 widgettree, 我觉得我讲的不够好,可以参考以下文章

Understanding the Flutter Widget Tree: A Comprehensive Guide
Flutter’s Widget Tree: A Deep Dive into the World of Flutter UI
Widget Class

把握几个核心的点即可

  • flutter 的 UI 以 Widget 的形式构建,包括 Column Row Text Stack

    • 组件都是不可变的状态,如果需要根据用户行为改变组件,则需要进行事件的传递,然后由 flutter 进行重新渲染
  • Widget 之间通过树关系进行组织

  • 通过改变 Widget 里的位置 尺寸等关系来构建 UI 界面

事件结构

参考资料:

State management
State class

在我们的代码中,除了 UI 的构建,还涉及到 UI 随着用户操作而改变.

所以深入了解 flutter 事件是有必要的

整体的逻辑为,用户操作 -> 改变组件状态 -> flutter 重新渲染 UI

其中 state 的生命周期为

  • flutter 使用 StatefulWidget.createState 创建一个 State 对象
  • State 与 BuildContext 进行关联,这个关联是永久性的
  • 在 flutter 启动的时候会对 State 进行初始化
  • State 会通过 setState 方法重新构建组件树,并调用 didUpdateWidget 方法,从而重新构建整个 app

上述代码是个简单的 app ,并不涉及到 state 在不同组件间的流转,所以只涉及到局部 state 的构建

我们首先定义一个能够通过成员属性定义自身 layout 的 widget

class ShoppingListItem extends StatelessWidget {
  ShoppingListItem({
    required this.product,
    required this.inCart,
    required this.onCartChanged,
  }) : super(key: ObjectKey(product));

  final Product product;
  final bool inCart;
  final CartChangedCallback onCartChanged;

  // 根据 inCart 的数值来决定颜色
  Color _getColor(BuildContext context) {
    // The theme depends on the BuildContext because different
    // parts of the tree can have different themes.
    // The BuildContext indicates where the build is
    // taking place and therefore which theme to use.

    return inCart //
        ? Colors.black54
        : Theme.of(context).primaryColor;
  }

  // 根据 inCart 的数值来决定文字样式
  TextStyle? _getTextStyle(BuildContext context) {
    if (!inCart) return null;

    return const TextStyle(
      color: Colors.black54,
      decoration: TextDecoration.lineThrough,
    );
  }

  @override
  Widget build(BuildContext context) {
    return ListTile(
      // 当用户点击 ListTile 时候,会调用 onCartChanged 回调函数
      onTap: () {
        // onCartChanged 传递给 ShoppingListItem 的回调函数
        onCartChanged(product, inCart);
      },
      leading: CircleAvatar(
        backgroundColor: _getColor(context),
        child: Text(product.name[0]),
      ),
      title: Text(product.name, style: _getTextStyle(context)),
    );
  }
}

首先这是一个 StatelessWidget 组件,这个组件是静态的,是不会动态改变的

同时,该组件有两种形态,随着 inCart 的值不同,该组件会呈现不同的 UI 状态

也就是说,随着该组件渲染时所传入的 inCart 参数不同,该组件会呈现不同的 UI 状态

观察我们的 App ,我们不只需要一个 ShoppingListItem ,我们还需要一个 list 来存储多个 item 对象

// 继承 StatefulWidget 并抽象出一个 State 对象
class ShoppingList extends StatefulWidget {
  const ShoppingList({required this.products, super.key});

  final List<Product> products;

  // The framework calls createState the first time
  // a widget appears at a given location in the tree.
  // If the parent rebuilds and uses the same type of
  // widget (with the same key), the framework re-uses
  // the State object instead of creating a new State object.

  // 创建一个 State 对象,用于管理 ShoppingList 的状态
  @override
  State<ShoppingList> createState() => _ShoppingListState();
}

// 继承 State 并实现其抽象方法
class _ShoppingListState extends State<ShoppingList> {
  final _shoppingCart = <Product>{};

  void _handleCartChanged(Product product, bool inCart) {
    setState(() {
      // When a user changes what's in the cart, you need
      // to change _shoppingCart inside a setState call to
      // trigger a rebuild.
      // The framework then calls build, below,
      // which updates the visual appearance of the app.

      if (!inCart) {
        _shoppingCart.add(product);
      } else {
        _shoppingCart.remove(product);
      }
    });
  }


  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Shopping List')),
      body: ListView(
        padding: const EdgeInsets.symmetric(vertical: 8),
        children:
            widget.products.map((product) {
              return ShoppingListItem(
                product: product,
                inCart: _shoppingCart.contains(product),
                onCartChanged: _handleCartChanged,
              );
            }).toList(),
      ),
    );
  }
}

上述代码一共涉及三个类,类与类之间的关系如下如所示

classDiagram class ShoppingList { +List<Product> products +State<ShoppingList> createState() } class _ShoppingListState { -Set<Product> _shoppingCart +void _handleCartChanged(Product product, bool inCart) +Widget build(BuildContext context) } class ShoppingListItem { +Product product +bool inCart +Function onCartChanged } class Product { +String name +double price } ShoppingList --> _ShoppingListState : creates _ShoppingListState --> ShoppingListItem : uses ShoppingListItem --> Product : references ShoppingList --> Product : contains _ShoppingListState --> Product : manages

可以看到,在 ShoppingList 中,我们定义了一个 products 的成员变量, 该变量存储了多个 Product 对象
同时我们还定义了一个 createState 方法, 该方法返回一个 State<ShoppingList> 对象
通过定义该方法, flutter 会自动创建一个 _ShoppingListState 对象

_ShoppingListState 继承了 State<ShoppingList>

该类是一个抽象类, 该类的作用是管理 ShoppingList 的状态

并且_ShoppingListState同时管理 ShoppingListItem 的状态,

通过调用 _handleCartChanged 方法, 我们可以改变 ShoppingListItem 的状态

下面我们描述一下在整个事件中发生了什么事情

  • 用户点击 ShoppingListItem 组件:\

  • 用户点击了 ShoppingListItem,触发了其内部的 onTap 方法。

  • 调用 onCartChanged 回调:\

  • ShoppingListItem 的实现中,onTap 方法调用了 onCartChanged 回调函数,并将当前的 Product 对象作为参数传递。

  • 事件传递到 _ShoppingListState_handleCartChanged 方法:\

  • onCartChanged 回调实际上是 _ShoppingListState 中的 _handleCartChanged 方法 \
    这个方法接收了 ProductinCart 参数,用于处理购物车状态的变化。

  • 更新 _shoppingCart 集合:\

  • _handleCartChanged 方法根据 inCart 的值决定是将 Product 添加到 _shoppingCart 集合中,还是从中移除。

  • 调用 setState 更新 UI:\

  • _handleCartChanged 方法在修改 _shoppingCart 后调用了 setState。\
    setState 通知 Flutter 框架需要重新构建 UI。

  • 重新构建 UI:
    Flutter 框架调用 _ShoppingListStatebuild 方法,重新生成整个 ShoppingList 的 UI。\
    此时,ShoppingListIteminCart 属性会根据 _shoppingCart 的状态更新,从而反映最新的购物车状态。

其流程图为

flowchart TD A[用户点击 ShoppingListItem] --> B[调用 onCartChanged 回调] B --> C[事件传递到 _ShoppingListState 的 _handleCartChanged 方法] C --> D[更新 _shoppingCart 集合] D --> E[调用 setState 更新 UI] E --> F[重新构建 UI]

所以在本文中,状态处理组件的核心职责为

  • 接收回调函数:
    父组件(或子组件)通过回调函数将用户交互事件传递给 _ShoppingListState

  • 定义状态处理方法:
    _ShoppingListState 定义了一个方法(如 _handleCartChanged),用于处理状态的变化。

  • 调用 setState 更新状态:
    在状态处理方法中,通过调用 setState 来更新组件的状态。

  • 重新构建 UI:
    调用 setState 后,Flutter 框架会重新调用 build 方法,生成新的 UI 树,从而反映最新的状态。

这实际也是 flutter 组件该做的事情,这种设计模式通常被称为 lifting state up,
其核心内容在于

  • 避免使用子组件控制 state 相反的,将 state 的管理放于父组件中
  • 父组件通过使用回调函数控制子组件的状态

同时可以查看以下资料

Lifting State Up & Callbacks in Flutter/
这篇博客比较详细的描述了 状态提升 的定义
Lifting State Up in Flutter: A Smarter Approach to Navigation Management
这篇博客举了一个比较简单例子,也可以进行进一步的参考
关于状态管理的官方文档

总结

本文在提供了一个简单的代码示例的同时,对以下两个方面进行了讲解

  • 如何构建基础的 flutter UI
  • 如何在简单的应用中对 flutter state 进行管理
posted @ 2025-04-26 10:59  五花肉炒河粉  阅读(11)  评论(0)    收藏  举报