从头学 Dart 第八集

222 阅读12分钟

最近打算自己做一个电商 App 调研之后选择技术栈为 Flutter 因此打算梳理一下 Flutter 的所有知识点,做个预热。

  1. flutter 中也分 dependencies 和 dev_dependencies 比较常见的依赖库有
  • cupertino_icons@1.0.2
  • uuid@3.0.7
  • intl@0.18.0 而常见的开发依赖是:
  • flutter_lints@2.0.0
  1. 为了能够得到设备的实时方向信息(横屏或者是竖屏)我们需要引入内置库 services
import 'package:flutter/material.dart';

然后将 runApp 放入下面所示的 then 的回调函数中

void main(){
 WidgetsFlutterBinding.ensureInitialized();
 SystemChrome.setPreferredOrientations([
 Device0rientation.portraitUp,
]).then((fn){
  runApp(...);
})}

其中 WidgetsFlutterBinding.ensureInitialized(); 的作用是为了保证执行顺序,和 window.onload 类似。

  1. 组件在设备横竖切换之后会自动调用 build 重新构建页面,可以在 build 函数中打印查看 使用 媒体查询可以得到当前页面的尺寸:
MediaQuery.of(context).size.width
MediaQuery.of(context).size.height

这里的宽和高都是属性而不是方法。

  1. 数组中不仅有 isEmpty 方法还有 isNotEmpty 方法,这并不冗余
Widget mainContent = const Center(
  child: Text('No expenses found. start adding some!'),
);

if (registeredExpenses.isNotEmpty) {
  mainContent = ExpensesList(
    expenses: registeredExpenses,
    onRemoveExpense: removeExpense,
  );
}
  1. 使用媒体查询的结果针对横屏或者竖屏使用不同的渲染布局
body: width < 600
    ? Column(
        children: [
          Chart(expenses: _registeredExpenses),
          Expanded(
            child: mainContent,
          ),
        ],
      )
    : Row(children: [
        Expanded(
          child: Chart(expenses: registeredExpenses),
        ),
        Expanded(
          child: mainContent,
        ),
      ]),

这个能生效完全基于 build 函数的再次执行。

  1. 组件的默认尺寸

Widget 的大小取决于它们的大小偏好和父 Widget 的尺寸约束。以 Scaffold 和 Column 为例,Scaffold 的尺寸约束是最大设备高度和宽度,而 Column 则尽可能多地获取高度,宽度由其子 Widget 决定。最终,Column 的大小将具有最大设备高度和根据子 Widget 决定的宽度。而 Row 刚好是相反的。

在 Flutter 中,无限尺寸的父元素不能直接包含无限尺寸的子元素,否则可能导致布局问题。通常需要在它们之间添加一个有固定尺寸的元素,如 SizedBox,来提供约束。

  1. 弹出键盘之后的空间预留问题 可以查询得到键盘所需的预留高度:
final keyboardSpace = MediaQuery.of(context).viewInsets.bottom;

得到这个值之后我们给一个 padding 预留此位置:

child: singlechildscrollview(
  child: Padding(
    padding:EdgeInsets.fromLTRB(16,48,16,keyboardSpace + 16),
    child: Column(
      children:[...],
    ),
  ),
),

这里使用的组件搭配是 singlechildscrollview -> Padding -> Column

  1. 值得注意的是在页面的头部也存在上述安全区域的问题 但是并不是所有的组件都需要手动去设置安全区域的,如果我们使用的是 Scaffold 组件,那么它会自动留出安全区域,并且提供 AppBar 配置项供开发者自定义。但是对于有些 Modal 你必须显式告诉它需要留出安全区域:
void openAddExpenseOverlay() {
  showModalBottomSheet(
    useSafeArea: true,
    isScrollControlled: true,
    context: context,
    builder: (ctx) => NewExpense(onAddExpense: addExpense),
  );
}

这里是通过 useSafeArea: true 设置实现的。

  1. 得到组件的空间信息 如果我们需要设置 padding 就用 Padding 组件包裹目标组件;同理,如果我们想要获取组件的空间信息那么同样需要为其包裹特殊组件 LayoutBuilder, 这个组建为其子组件提供了一个名为 constraints 的上下文对象,相关的位置信息都保留在此对象中。
@override
Widget build(BuildContext context) {
  final keyboardSpace = MediaQuery.of(context).viewInsets.bottom;
  return LayoutBuilder(
    builder: (ctx, constraints) {
      final width = constraints.maxWidth;
      return SizedBox(
        height: double.infinity,
        child: SingleChildScrollView(
          child: Padding(
            padding: EdgeInsets.fromLTRB(16, 16, 16, keyboardSpace + 16),
            child: Column(
              children: [
                if (width >= 600)
                  Row(
                    children: [
                      TextField(
                        controller: titleController,
                        maxLength: 50,
                        decoration: const InputDecoration(
                          label: Text('Title'),
                        ),
                      ),
                      const SizedBox(width: 24),
                      Expanded(
                        child: TextField(
                          controller: amountController,
                          keyboardType: TextInputType.number,
                          decoration: const InputDecoration(
                            prefixText: '\$',
                          ),
                        ),
                      ),
                    ],
                  ),
                // ... 其他组件
              ],
            ),
          ),
        ),
      );
    },
  );
}

屏幕方向影响的是组件可用空间的变化,因此有的时候比起屏幕的状态,我们更关心组件能用的空间有多少,这就是使用 Layout 组件的意义。

  1. if 中可以省略的花括号 在 Dart 中,你可以在以下情况下省略if-else语句的花括号:
  • ifelse块中只有一个语句时。
  • ifelse块中的语句是一个返回语句(return)时,即使有多个语句也不需要花括号。

以下是一些例子:

单语句省略花括号:

int max(int a, int b) {
  return a > b ? a : b;
}

在这个例子中,if-else表达式被用作返回语句,因此不需要花括号。

多语句省略花括号(当使用return时):

void exampleFunction() {
  String condition = "some condition";
  if (condition == "some condition")
    return "Condition is true";
  else
    return "Condition is false";
}

在这个例子中,即使ifelse块中有多条语句,但由于它们都是返回语句,所以不需要花括号。

单语句省略花括号(非返回语句):

void exampleFunction(bool flag) {
  if (flag) print("Flag is true");
  else print("Flag is false");
}

在这个例子中,ifelse块中都只有一个print语句,因此可以省略花括号。

需要注意的是,虽然可以省略花括号,但在某些情况下,省略花括号可能会导致代码的可读性降低,尤其是在复杂的条件语句中。因此,是否省略花括号应根据具体情况和代码风格指南来决定。

  1. 在 dart 中获取设备信息 最重要的就是获取当前程序是跑在 ios 还是 android 上面。

以下面为例,这里 AppBar 组件的文字在 Android 和 ios 上一个居中一个靠左,所以默认布局是不同的,为此我们可以手动的强制规定其排布的方式:

Scaffold(
  appBar: AppBar(
    centerTitle: false,
    title: const Text('Flutter ExpenseTracker'),
    actions: [
      IconButton(
        onPressed: openAddExpenseOverlay,
        icon: const Icon(Icons.add),
      ),
    ],
  ),
  // ... 其他Scaffold属性和body
)

这样 title 就统一靠左了。

  1. cupertino Cupertino 是苹果公司设计的一种用户界面风格,它以简洁、直观和响应式为特点,广泛应用于 iOS 和 macOS 操作系统中。这种设计风格强调清晰、易读的字体、微妙的动效和自然的过渡效果,以及直观的手势操作。

在 Flutter 框架中,Cupertino 指的是一套与 iOS 原生应用风格一致的 UI 组件库。这些组件包括按钮、滑块、开关、导航栏、标签栏等,它们的外观和行为都与 iOS 系统中的原生组件非常相似。使用 Cupertino 组件,开发者可以为 iOS 设备创建具有原生感的应用程序,而无需使用 Swift 或 Objective-C。

Cupertino 组件库提供了以下优势:

  1. 一致性:确保应用的 UI 与 iOS 系统的其他应用保持一致。
  2. 易用性:用户可以轻松地与熟悉的界面元素进行交互。
  3. 性能:Flutter 的 Cupertino 组件是高效且响应迅速的,它们利用了 Flutter 的高性能渲染引擎。
  4. 定制性:虽然 Cupertino 组件模仿了原生 iOS 组件的外观和行为,但它们仍然可以通过 Flutter 的定制能力进行个性化调整。

使用 Cupertino 组件,开发者可以创建出既符合 iOS 设计规范,又能提供流畅用户体验的应用。

  1. 判断设备使用 ISO 平台并使用 cupertino 风格的组件 首先引入相关的依赖包:
import 'package:flutter/cupertino.dart';

然后引入判断设备平台的包:

import 'dart:io';

最后我们就可以判断性的,在 IOS 平台上使用 cupertino 风格的组件了

void showDialog() {
  if (Platform.isIOS) {
    showCupertinoDialog(
      context: context,
      builder: (ctx) => CupertinoAlertDialog(
        title: const Text('Invalid input'),
        content: const Text(
          'Please make sure a valid title, amount, date and category was entered.',
        ),
        actions: [
          TextButton(
            onPressed: () {
              Navigator.pop(ctx);
            },
            child: const Text('Okay'),
          ),
        ],
      ),
    );
  } else {
    showDialog(
      context: context,
      builder: (ctx) => AlertDialog(
        title: const Text('Invalid input'),
        content: const Text(
          'Please make sure a valid title, amount, date and category was entered.',
        ),
        actions: [
          TextButton(
            onPressed: () {
              Navigator.pop(ctx);
            },
            child: const Text('Okay'),
          ),
        ],
      ),
    );
  }
}

我们进行条件渲染的依据就是:Platform.isIOS.

  1. flutter 中的三大 tree flutter 中有三大 tree 分别是:widget element 和 render tree.
  • Widget 树:这是你的代码中组合的 UI 构建块,是 Flutter 应用界面的内存表示。

  • Element 树:这个树用于确定必要的 UI 更新。当 build 方法被调用时,它会频繁地检查是否有需要更新的地方,并通过 Element 树的比较来决定是否需要重新渲染。

  • Render 树:这是实际可见的 UI 构建块,用于渲染最终用户界面。

强调以下几点:

  • 初始创建是在 build 方法之后进行的。
  • 如果需要,Widget 树会被重新渲染。
  • Element 树会在确定需要更新时通过比较来更新。
  • build 方法被频繁调用以检查所需的更新。
  • 如果可能,Element 会被重用,以优化性能。

与此相关的还有一个非常重要的原则,那就是 flutter 只需更新需要更新的组件,所以会尽可能的进行复用,但是这是有代价的,复用和新建比起来总是会出现问题,后面会看到。

而 flutter 复用组件的原则就是树上的节点在更新前后是否属于同一个 widge. flutter 通过对比 element tree 决定重绘 render tree 的哪一部分。

所以,flutter 中复用的对象从来就不是 widget tree 上的节点而是 element tree 上的节点。

基于此,一个重要的提升 flutter application 性能的原则就是:让可变组件体量越小越好。

  1. widget element 和 state、 在 Flutter 框架中,Widget、Element 和 State 之间存在着一种特殊的关系。Widget 是构建用户界面的蓝图,而 State 则负责管理 Widget 的状态。这种状态可以是任何东西,比如文本字段中的文字、复选框是否被选中,或者是任何其他需要在 Widget 重建之间保持的信息。

Element 是 Widget 的运行时实体,它包含了 Widget 的配置信息(如属性和状态)以及与渲染树的连接。当 Widget 被插入到树中时,Flutter 会为每个 Widget 创建一个 Element。Element 负责将 Widget 的配置信息传递给 Render 树,Render 树则是实际在屏幕上绘制内容的部分。

State 与 Widget 的连接不是直接的,而是通过 Element 来实现的。当 Widget 需要重建时,它的 Element 会保留 State,这样即使 Widget 本身被重建,State 也不会丢失。这种设计允许 Flutter 框架在构建新的 Widget 树时,能够保持状态的连续性,从而为用户提供一致的体验。

这里的关键点是:

  • Widget:定义了用户界面的结构和布局。
  • Element:是 Widget 的实例,负责将 Widget 的配置信息传递给 Render 树。
  • State:与 Element 关联,而不是直接与 Widget 关联,它保存了 Widget 的状态信息。

这种设计模式使得 Flutter 能够在 Widget 树发生变化时,例如在列表项重新排序时,保持 Widget 的状态。但是,如前所述,这也可能导致一些问题,比如在列表项重新排序时,Widget 的状态没有正确更新。在这种情况下,使用key属性可以帮助 Flutter 正确地识别和更新 Widget 的状态,确保状态与 Widget 的新位置同步。

  1. element 复用造成的问题 在 Flutter 中,为了提高性能,框架会尝试重用已经存在的 Widget 实例,尤其是在处理列表时,如果列表项的顺序发生变化,Flutter 会尝试保持 Widget 的实例不变,即使它们在视觉上移动到了不同的位置。这种优化策略被称为“复用”。

然而,这种复用机制可能会带来一些副作用。在 Flutter 中,Widget 的状态(State)是与 Element 关联的,而不是与 Widget 本身直接关联。Element 是 Widget 在 Flutter 渲染树中的运行时表示。当 Widget 的顺序改变时,虽然 Widget 的引用可能发生了变化,但是它们的 Element 和 State 可能保持不变。

这可能导致一些预期之外的行为,特别是对于那些有内部状态的 Widget,比如复选框(Checkbox)。如果你有一个可排序的列表,其中包含了复选框,当列表项的顺序改变时,复选框的显示状态(被选中或未选中)可能不会随之更新,因为状态仍然与原来的 Element 关联,而不是新的 Widget 位置。

为了解决这个问题,你可以使用key属性。在 Flutter 中,key是一个唯一的标识符,用于帮助 Flutter 确定哪些 Widget 是相同的,哪些是不同的。当你为 Widget 指定一个key时,Flutter 会使用这个key来跟踪 Widget 的身份,即使它们在列表中的位置发生变化。这样,当 Widget 的顺序改变时,Flutter 可以正确地更新 Widget 的状态,确保状态与 Widget 的新位置保持同步。

简而言之,使用key可以确保即使 Widget 在视觉上移动了位置,它们的状态也能正确地随之更新,避免了因 Widget 复用而导致的状态错位问题。

  1. keys in flutter flutter 的复用规则是看 widget 前后是否一致,如果一致会复用但是不会检查 state 但是加上 key 之后它也会检查 key 是否一致,如果不一致,则会进一步检查 state, 因此有了 key 就可以保证 state 随动。

为每个组件设置独一无二的 id 即 key,首先我们必须引入相关库:

import "package:flutter internals/keys/keys.dart';
  • 一种是设置 值 key:
key: ValueKey(todo.text)
  • 另一种是设置 组件 key
key: objectKey(todo),

第一种简单所以用的多,但是不管怎么样都不要使用随机数作为 key 生成用的盐。

  1. const 是不可变的常量 为了说明这个问题,看下面两种等价的写法:
const numbers = [1,2,3];
final _numbers = const [1,2,3];
  1. 数组上的一些方法 使用 of 来完成对现有数组的复制,这样做的原因在于有一些数组上的方法会导致数组本身的改变,也就是 js 数组中的 mutation methods 这个时候,如果我们不希望接下来的操作能够影响原数组,通常我们会对原数组进行复制:
final sortedTodos = List.of(_todos);
sortedTodos.sort((a,b){
  return a-b;
});