Flutter 学习笔记

190 阅读17分钟

Flutter 目录结构

  • pubspec.yaml 配置文件,一般存放一些第三方库的依赖
  • lib 目录下,所有业务代码都需要放在这个目录下
  • test 用于存放测试代码

widget 组件

在Flutter中自定义组件其实就是一个类,这个类需要继承StatelessWidget/StatefulWidget前期我们都继承StatelessWidget。后期给大家讲StatefulWidget的使用。StatelessWidget 是无状态组件,状态不可变的widget SstatefulWidget 是有状态组件,持有的状态可能在widget生命周期改变

MaterialApp

是一个方便的Widget,它封装了应用程序实现Material Design所需要的一些Widget。一般作为顶层widget使用。 常用的属性:
home (主页)
title (标题)
color (颜色)
theme (主题)
routes (路由)

Scaffold

Scafold是Material Design布局结构的基本实现。此类提供了用于显示drawer、snackbar和底部sheet的API。 Scaffold 有下面几个主要属性 appBar - 显示在界面顶部的一个 AppBar。 body-当前界面所显示的主要内容 Widget。 drawer -抽屈菜单控件。

StatelessWidget

无状态组件

布局
布局核心:

约束向下传递,尺寸向上传递,位置由父组件决定

Row
  • Row 如果要使用crossAxisAlignment 生效,必须要用父控件包裹着他才行。比如用container包裹着,这个属性是父控件控制子控件的。
  • 如果 Row 外部没有Container 行是自适应的。
Positioned
  • 用在stack 中,相对于外部容器(stack的外部容器)定位,如果没有外部容器,就相对于整个屏幕进行定位。
  • 使用Positioned 定为的时候,如果自控件child 是 Row 组件的话,还需要设置宽度和高度

Dart 相关

final:运行常量 运行时才初始化
final 声明的变量可以只赋值一次,但不要求立即初始化。
final属性是惰性初始化的,它们只有在第一次被访问时才会被初始化
const:定义常量定义时初始化
const 声明的变量必须在声明时初始化,并且其值在编译时就确定。
const 关键词在多个地方创建相同的对象的时候,内存中只保留一个对象。可以通过 identical 进行判断

  • 一个数组,如果用const 修饰,那么他就不可以添加删除元素,但是用final 修饰,却可以添加删除元素。
  • 在 Dart 中,dynamicObject 都用于处理不确定类型的值,但它们之间有一些关键区别:

dynamic 和 Object

  1. 类型检查:

    • dynamicdynamic 是一种特殊类型,它在编译时不会进行类型检查。这意味着您可以将任何类型的值赋给 dynamic 类型的变量,并且编译器不会报错。类型检查是在运行时进行的,因此潜在的类型错误可能不会在编译时捕获到。

    • ObjectObject 是 Dart 中所有对象的根类。虽然 Object 可以用于存储任何对象,但它在编译时进行了类型检查。这意味着编译器会强制执行类型检查,确保您不会错误地执行不兼容的操作。

  2. 使用场景:

    • dynamic:通常用于需要在运行时确定类型的情况,例如反射、处理来自外部数据源的动态数据或动态类型转换。使用 dynamic 具有更大的灵活性,但也需要更多的注意,因为类型错误可能只在运行时发生。

    • Object:通常用于通用的对象引用,或者在不确定对象类型的情况下使用。它提供了一些类型安全性,因为编译器仍会执行类型检查。

示例:

dynamic dynamicVariable = 42;
dynamicVariable = 'Hello'; // 合法,没有编译时类型检查

Object objectVariable = 42;
objectVariable = 'Hello'; // 合法,但仍然会进行编译时类型检查

总结:dynamicObject 都允许您处理不确定类型的值,但它们在类型检查和使用场景方面有区别。选择使用哪种类型取决于您的需求和安全性考虑。dynamic 更灵活,但可能引入类型错误,而 Object 提供了一定程度的类型安全性。

print
print('年龄 == $age'), $ 是格式化符号
print('user=== ${User.current()}');

匿名方法

var fn = (){ print('我是匿名方法') }

自执行方法

((){ print('自执行方法'); })();

Dart所有的东西都是对象,所有的对象都继承自object类
Dart是一门使用类和单继承的面向对象语言,所有的对象都是类的实例,并且所有的类都是Object的子类
一个类通常由属性和方法组成。

构造函数

默认构造函数和类名字一样, 在Dart中,构造函数可以有多个

class Person {
    String name;
    int age = 10;
    // 默认构造函数,只能写一个,默认调用的就这个构造函数
    Person(String name) {
        this.name = name;
    }
    // 上面构造方法可以简写成
    // Person(this.name);
    
    // 这个是命名构造函数,可以写多个
    Person.now() {
    
    }
}

 var person0 = new Person('Space');
 var person01 = new Person.now();

在 Dart 和 Flutter 中,factory 构造函数是一种特殊类型的构造函数,用于创建对象的实例。与普通构造函数不同,factory 构造函数可以在创建对象实例时执行一些自定义逻辑,例如返回一个现有对象的单例,而不必每次都创建新的实例。

factory 构造函数通常用于以下情况:

  1. 实例的创建不仅仅是对象的构造,还可能涉及缓存或其他逻辑。
  2. 返回一个已存在的实例,而不是每次都创建新的实例。
  3. 根据参数返回不同的子类实例。

下面是一个示例,演示如何使用 factory 构造函数:

class MySingleton {
  static MySingleton _instance;

  // 私有构造函数,防止外部实例化
  MySingleton._();

  // 工厂构造函数,用于返回单例实例
  factory MySingleton() {
    if (_instance == null) {
      _instance = MySingleton._(); // 创建单例实例
    }
    return _instance;
  }
}

void main() {
  MySingleton instance1 = MySingleton();
  MySingleton instance2 = MySingleton();

  print(identical(instance1, instance2)); // 输出 true,两个变量引用同一个实例
}
私有属性和方法

Dart和其他面向对象语言不一样,Data中没有 public private protected这些访问修饰符合
但是我们可以使用_把一个属性或者方法定义成私有,并且必须要把类单独抽成一个文件!!
如果外部需要访问,可以在类内部提供一个方法,间接访问私有属性。

getter 和 setter

类似其他语言的计算属性

class Person {
  final String _name = 'John';
  String get name {
    // 在获取属性值前可以添加额外逻辑
    return 'Name: $_name';
  }
}
class Person {
  int _bra = 0;
  set size(int value) {
      _bra = value * 10;
  }
  get aaname {
    return _bra;
  }
}
初始化列表
// Dart中我们也可以在构造函数体运行之前初始化实例变量
class Rect{
    int height;
    int width;
    // 这个就是初始化列表,感觉就是带默认参数的构造方法
    Rect() :height=2,width=10{
    
    }
    getArea(){
        return this.height*this.width;
    }
}
有多个参数,或者你想要提供可选的、有默认值的参数,那么带有命名参数的写法可能更合适。也就是使用初始化列表
class MyClass {
  int age;
  String name;
  MyClass({this.age = 1,this.name = "12"});
}

类方法和静态成员

Dart中的静态成员:
1、使用static 关键字来实现类级别的变量和函数
2、静态方法不能访问非静态成员,非静态方法可以访问静态成员

Dart中的对象操作符:

? 条件运算, 如果前面的对象为空,就不会执行下面的方法,防止报错
as 类型转换
is 类型判断
.. 级联操作 (连缀)

p1.name = "Space";
p1.age = 20;
printInfo();
这三个代码可以用连缀操作符连起来,下面代码和上面代码等价
p1..name = "Space"
  ..age = 20
  ..printInfo();

Dart中的类的继承:

1、子类使用extends关键词来继承父类
2、子类会继承父类里面可见的属性和方法 但是不会继承构造函数
3、厂类能复写父类的方法 getter和setter

Dart中抽象类:

Dart抽象类主要用于定义标准,子类可以继承抽象类,也可以实现抽象类接口
1、抽象类通过abstract 关键字来定义
2、Dart中的抽象方法不能用abstract声明,Dart中没有方法体的方法我们称为抽象方法
3、如果子类继承抽象类必须得实现里面的抽象方法
4、如果把抽象类当做接口实现的话必须得实现抽象类里面定义的所有属性和方法
5、抽象类不能被实例化,只有继承它的子类可以

extendsimplements 的主要区别
  1. 继承 vs. 实现:

    • extends 用于继承一个父类,子类可以继承父类的属性和方法,甚至可以直接使用父类的实现。
    • implements 用于实现接口,类不会继承接口的实现,必须手动实现接口的所有方法。
  2. 继承多个类 vs. 实现多个接口:

    • Dart 不支持多继承,一个类只能继承一个父类。
    • 一个类可以实现多个接口。
  3. 默认实现:

    • 使用 extends 时,子类可以直接使用父类的实现(如果没有重写)。
    • 使用 implements 时,类必须提供所有接口方法的具体实现。
Dart中的多态:

1.允许将子类类型的指针载值给父类类型的指针,同一个函数调用会有不同的执行效果
2.子类的实例赋值给父类的引用
3.多态就是父类定义一个方法不去实现,让继承他的子类去实现,每个子类有不同的表现。

Dart的接口

和Java一样,dart也有接口,但是和Java还是有区别的。
首先,dart的接口没有interface关键字定义接口,而是普通类或抽象类都可以作为接口被实现同样使用implements关键字进行实现。
但是dart的接口有点奇怪,如果实现的类是普通类,会将普通类和抽象中的属性的方法全部需要覆写一遍。
而因为抽象类可以定义抽象方法,普通类不可以,所以一般如果要实现像Java接口那样的方式,一般会使用抽象类。
建议使用抽象类定义接口。

mixin 和 abstract

mixin的中文意思是混入,就是在类中混入其他功能
在Dart中可以使用mixin实现类似多继承的功能
因为mixin使用的条件,随着Dart版本一直在变,这里讲的是Dart2.x中使用mixin的条件:
1、作为mixin的类只能继承自object,不能继承其他类
2、作为mixin的类不能有构造函数
3、一个类可以mixin多个mixin类
4、mixin绝不是继承,也不是接口,而是一种全新的特性
5、如果有相同的方法,后面会替换前面的。
6、mixin 其实就是超类(Object)的子类型

泛型 abstract

指定类型的泛型类,居然可以传入其他类型,但是却会发生运行时报错 Dart中的泛型接口: 实现数据缓存的功能:有文件缓存、和内存缓存。内存缓存和文件缓存按照接口约束实现 1、定义一个泛型接口 约束实现它的子类必须有getByKey(key)和 setByKey(key,value) 2、要求setByKey的时候的value的类型和实例化子类的时候指定的类型一致

abstract class Cache<T>{
    getByKey(String key);
    void setByKey(String key, T value);
}

class FileCache<T> implements Cache<T> {
  @override
  getByKey(String key) {
    // TODO: implement getByKey
    throw UnimplementedError();
  }

  @override
  void setByKey(String key, T value) {
    // TODO: implement setByKey
  }

}
特性abstract 类mixin
实例化不能直接实例化,但可以通过子类间接实例化。不能实例化,只能通过 with 混入到其他类中使用。
继承方式使用 extends,一个类只能继承一个父类。使用 with,可以混入多个 mixin
作用定义接口和规范,子类必须实现抽象方法。为类添加功能,代码复用。
包含内容可以包含抽象方法、具体实现的方法和属性。通常只包含方法的具体实现,少有属性。
依赖关系子类必须使用 extends 继承才能访问 abstract 类的功能。不需要继承,任何类都可以使用 with 来混入 mixin 功能。
状态支持可以定义状态(字段)和行为。不建议定义状态,主要用来提供功能(行为)。
在 Dart 中,你可以使用 is 关键字来判断一个对象是否是某个类型的实例,包括子类和父类之间的类型判断。下面是 Dart 中类型判断的几种常用方法,特别是对子类和父类的判断。
is 用于检查对象是否为指定类型
class Parent {}
class Child extends Parent {}

void main() {
  Parent p = Parent();
  Child c = Child();
  
  // 判断对象是否是 Child 类的实例
  print(c is Child); // true
  print(p is Child); // false
  
  // 判断对象是否是 Parent 类的实例(包括子类实例)
  print(c is Parent); // true, 因为 Child 是 Parent 的子类
  print(p is Parent); // true
}

/------------------------------------------------------------/

void main() {
  Parent p = Parent();
  Child c = Child();
  
  // 判断 p 是否不是 Child 类的实例
  print(p is! Child); // true
  print(c is! Parent); // false, 因为 Child 是 Parent 的子类
}


Dart 的 库

Dart中的库主要有三种: 1、我们自定义的库
import lib/xxx.dart';
2、系统内置库
import dart:math',import dart:io',import'dart:convert' 3、Pub包管理系统中的库
pub.dev/packages
pub.flutter-io.cn/packages
pub.dartlang.org/flutter/

  • 需要在自己想项目根目录新建一个pubspec.yam1
  • 在pubspec.yam1文件 然后配置名称 、描述、依赖等信息
  • 然后运行 pub get 获取包下载到本地
  • 项目中引入库 import'package:http/http.dart'as http;看文档使用
async和await

这两个关键字的使用只需要记住两点:
只有async方法才能使用await关键字 调用方法如果调用别节async方法必须使用await关键字
async是让方法变成异步
await是等待异步方法执行完成

1、冲突解决 当引入两个库中有相同名称标识符的时候,如果是java通常我们通过写上完整的包名路径来指定使
import 'package:lib1/lib1.dart'; import'package:lib2/lib2.dart'as lib2;

Element element1 = new Element() ;lib2.Element element2 = new lib2.Element();

// Uses Element from lib2.
// Uses Element from lib1

import lib/Person1.dart';
import lib/Person2.dart' as lib;


main(List<String> args){
    Person p1=new Person(张三',20);
    p1.printInfo();
    lib.Person p2=new lib.Person('李四',20);
    p2.printInfo();
}
导入部分库和 part 关键字

如果只需要导入库的一部分,有两种模式:
模式一:只导入需要的部分,使用show关键字,如下例子所示:
import package:lib1/lib1.dart' show foo;
模式二:隐藏不需要的部分,使用hide关键字,如下例子所示: import'package:lib2/lib2.dart'hide foo;

在 Flutter 和 Dart 中,part 是一个关键字,用于在多个文件中组织和拆分代码。part 关键字通常与 part of 结合使用,以创建可拆分的 Dart 文件,以便更好地管理和组织大型项目的代码。

具体来说,part 关键字的主要用途是将一个 Dart 文件拆分为多个部分,每个部分使用 part 关键字引入。这些部分通常在相同的库或库文件中,以便在不同的文件中组织和管理相关代码块。这有助于减少文件的大小,提高代码的可读性和维护性。

identical 函数

dart:core 库中identical 函数的用法介绍如下
用法:
bool identical(Object? a,Object? b 检查两个引用是否指向同一个对象

常量构造函数总结如下几点:

1、常量构造函数需以const关键字修饰
2、const构造函数必须用于成员变量都是final的类
3、如果实例化时不加const修饰符,即使调用的是常量构造函数,实例化的对象也不是常量实例
4、实例化常量构造函数的时候,多个地方创建这个对象,如果传入的值相同,只会保留一个实例对象
5、Flutter中const 修饰不仅仅是节省组件构建时的内存开销,Flutter 在需要重新构建组件的时候,因为const 组件是不会改变的,重新构建没有意义,所以flutter 是不会重新构建const 组件的

required关键词:

最开始 @required 是注解
现在它已经作为内置修饰符主要用于允许根据需要标记任何命名参数(函数或类),使得它们不为空。因为可选参数中必须有个 require

try-catch
  • 对于同步的future,我们使用try-catch进行异常捕获所以必须如果要捕获异步函数的(future)错误,必须要在try 里面用 await 等待。
  • 如果在 try 块内部调用多个 Future,并在 catch 块中捕获它们可能抛出的异常。但是,请注意,如果其中任何一个 Future 抛出异常,catch 块将只捕获到第一个抛出的
  • 多个try catch 嵌套使用的情况下,如果try catch 内部使用了rethrow 那么外部try 模块 会直接调用到catch 的代码块,不再执行try 下面的代码块。有点类似return,但是return 到上一个try 的catch 部分。如果上层没有try 了,那就是return 的 作用
try {
  _createUser();
  print('after _createUser');
} catch (e) {
  print("Caught outer exception: $e");
}

_createUser() {
  print("Outer try block");
  try {
    // 内层操作,可能抛出异常
    print("Inner try block");
    int result = 10 ~/ 0; // 故意触发异常
    print("Result: $result"); // 不会执行这行代码,直接到异常模块
  } catch (e) {
    print("Caught inner exception: $e");
    rethrow; // 直接抛到外层的 catch代码,不会执行 ’print("come on");‘
  }
  print("come on");
}

callback

void main() {
  // 不使用 typedef
  void functionWithCallback(String Function(int) callback) {
    int value = 10;
    String result = callback(value);
    print('Result from callback: $result');
  }

  // 调用带回调的函数
  functionWithCallback((int value) {
    return 'Received value: $value';
  });
}


typedef MyCallback = String Function(int);

void main() {
  // 使用 typedef 声明的回调
  void functionWithCallback(MyCallback callback) {
    int value = 10;
    String result = callback(value);
    print('Result from callback: $result');
  }

  // 调用带回调的函数
  functionWithCallback((int value) {
    return 'Received value: $value';
  });
}


class MyClass {
  late String Function(int) myCallback00;
  String Function(int)? myCallback01;
  MyClass(this.myCallback);
  void callCallback(int value) {
    String result = myCallback00(value);
    print('Result from callback: $result');
  }
}


UI 相关

渐变色

DecoratedBox(
  decoration: BoxDecoration(
      gradient: LinearGradient(
    begin: Alignment.topCenter,
    end: Alignment.bottomCenter,
    colors: [Colors.black.withOpacity(0.5), Colors.transparent],
  )),
  child: const SizedBox(
    width: double.infinity,
    height: 400 / 3,
  ),
),
如果需要和图片混个渐变色,需要添加ShaderMask代码如下:

ShaderMask(
  shaderCallback: (Rect bounds) {
    return LinearGradient(
      begin: Alignment(0.0, -.5),
      end: Alignment.bottomCenter,
      colors: [
        Colors.black,
        Colors.black.withOpacity(0.0),
      ],
      stops: [.0, 0.2], // 可根据需要调整渐变的位置
    ).createShader(bounds);
  },
  blendMode: BlendMode.dstIn,
  child: Align(
      alignment: Alignment.topCenter,
      child: Png.name('scene_custom_course_bg', width: screenWidth)),
),

图片透明度渐变

ShaderMask(

  shaderCallback: (Rect bounds) {

    return const LinearGradient(

      begin: Alignment.*topCenter*,

      end: Alignment.*bottomCenter*,

      colors: [Colors.*black*, Colors.*transparent*],

      stops: [.0, 1.0], // 可根据需要调整渐变的位置

    ).createShader(bounds);

  },

  blendMode: BlendMode.dstIn,

  child: icon,

),

Flutter TextField 放在positioned 组件内

需要用SizedBox(width: 100, 设置宽才能正常layout,要不就报错!!

Positioned 组件存在的情况下,padding 组件失效?

TextField

const InputDecoration({
    this.icon, // 左边添加一个 Widget
    this.labelText, // 顶部描述字符串,如果输入框成为焦点,会发生改变
    this.labelStyle, // 设置顶部描述的文字样式
    this.helperText, // 底部描述文字
    this.helperStyle, // 底部描述文字样式
    this.helperMaxLines, // 底部描述文字最大行数
    this.hintText, // 占位文字,类似于iOS中的 placeholder,只有当输入框没有输入内容并且成为焦点才会显示出来
    this.hintStyle, // 占位文字样式
    this.hintMaxLines, // 占位文字最大行数
    this.errorText, // 错误提示文字,如果同时设置helperText,会优先显示 errorText
    this.errorStyle, // 错误提示文字样式
    this.errorMaxLines, // 错误文字提示最大行数
    this.hasFloatingPlaceholder = true, // 设置是否需要展示 labelText,如果设置为false,TextField 成为焦点时不显示labelText,未成为焦点还会显示出来
    this.isDense,
    this.contentPadding, // 设置内容padding(内切),设置后输入框内容、helperText、counterText、errorText都会影响
    this.prefixIcon, // 在 icon 和 prefix(prefixText) 之间的一个 Widget
    this.prefix, // 输入之前的一个Widget,基线与输入框基线对齐,如果同时设置了 prefixIcon,则会排布到prefixIcon后面去
    this.prefixText, // 文本,如果设置了 prefix 就不能设置 prefixText,这两者是冲突的
    this.prefixStyle, // 设置文本样式
    this.suffixIcon, // 类似 prefixIcon,只不过这是在尾部
    this.suffix, // 类似 suffix
    this.suffixText, // 类似 prefixText,在这里也不能同时设置 suffix
    this.suffixStyle, // suffixText 的样式
    this.counter, // 一般用来文字计数的Widget,如果同时设置了 counterText,优先展示 counter Widget
    this.counterText, // 计数文本展示
    this.counterStyle, // 计数文本样式
    this.filled, // 是否需要装饰容器
    this.fillColor, // 设置容器的颜色
    this.focusColor, // 
    this.hoverColor, // 
    this.errorBorder, // 输入框输入错误时,并且没有成为焦点边框展示样式
    this.focusedBorder, // 输入框成为了焦点并且没有设置 errorText 展示样式
    this.focusedErrorBorder, // 当设置了 errorText时,输入成为焦点边框展示样式
    this.disabledBorder, // 当输入框不可用时,边框样式
    this.enabledBorder, // 输入框可用没有成为焦点时边框展示样式
    this.border, // 设置边框样式,优先级低于其他 border,如果需要设置没有边框样式使用 InputBorder.none
    this.enabled = true, // 设置输入框是否可用
    this.semanticCounterText,
    this.alignLabelWithHint,
  }) : assert(enabled != null),
       assert(!(prefix != null && prefixText != null), 'Declaring both prefix and prefixText is not supported.'),
       assert(!(suffix != null && suffixText != null), 'Declaring both suffix and suffixText is not supported.'),
       isCollapsed = false;

快速过滤 有空的数组

List<Widget>.from([
  icon,
  text.isEmpty ? null : FittedBox(
    fit: BoxFit.fitHeight,
    child: Baseline(
      baseline: 48,
      baselineType: TextBaseline.alphabetic,
      child: Text(text, style: const TextStyle(color: Colors.white, fontSize: 48)),
    ),
  ),
].where((element) => element != null)),

图片缓存预加载

if (url != null) {
  // 预加载chat_page 的大图片
  CachedNetworkImageProvider(url).resolve(ImageConfiguration.empty);
}

毛玻璃效果规范代码

ClipRRect(
  borderRadius: BorderRadius.circular(100),
  child: BackdropFilter(
    filter: ImageFilter.blur(sigmaX: 8.0, sigmaY: 8.0),
    child: ColoredBox(
        color: const Color.fromRGBO(50, 50, 50, 0.30),
        child: ConstrainedBox(
            constraints: const BoxConstraints(maxHeight: 40),
            child: your widgets!!))))

圆角效果

Container(
  width: 80,
  height: 25,
  decoration: const BoxDecoration(
    color: Color(0xFF3F3F3F),
    borderRadius: BorderRadius.all(
      Radius.circular(10),
    ),
  ),

圆形loading 加载中

Center(
  child: SizedBox(
    height: 20,
    width: 20,
    child: CircularProgressIndicator(
      color: Colors.white,
      strokeWidth: 2.0,
    ),
  ),
),

文字换行

在Column中的Text不用任何处理,能够自动换行。
在Row中的Text需要用Expanded包裹。
另外Text自动换行有两个属性控制: 【software】: 是否自动换行【bool】

【overflow】:自动换行效果【TextOverfow】

换行效果分别是:clip【截取】,fade【隐藏】,ellipsis【三点省略】,visible【不换行】

Richtext 应用实例

RichText(
  text: TextSpan(
    style: TextStyle(
      color: Colors.white,
      fontSize: 16,
    ),
    children: [
      TextSpan(text: " "),
      WidgetSpan(
        alignment: PlaceholderAlignment.middle,
        child: Container(
          width: 18 / 3,
          height: 18 / 3,
          decoration: BoxDecoration(
            color: Colors.white,
            shape: BoxShape.circle,
          ),
        ),
      ),
      TextSpan(text: " "),
      TextSpan(text: widget.chat.completion ?? ""),
    ],
  ),
),

如何让内容过长的Colomn 当超出最大高度的时候,就可以滚动

ConstrainedBox(
  constraints: const BoxConstraints(
    maxHeight: 200, // 最大高度
  ),
  child: SingleChildScrollView(
    child: Column(
      children: children,
    ),
  ),
),

如何GestureDetector 点击Spacer()空白处也有响应

GestureDetector(
  behavior: HitTestBehavior.opaque,

键盘布局相关

// 当页面B键盘弹起,之后pop的到A时候,会导致A 报错A 
RenderFlex overflowed by 53 pixels on the bottom.,只需要把A页面的Scaffold中的属性设置为如下值:
resizeToAvoidBottomInset:false,

resizeToAvoidBottomPadding 默认为true,就是Scaffold 中的布局跟随键盘变化而变化的意思

Dart 数据处理

去掉字符串的中的所有标点符号
String result = word.replaceAll(RegExp(r'\p{P}', unicode: true), '');