update
This commit is contained in:
parent
f84f192f20
commit
b731660b06
|
@ -0,0 +1,174 @@
|
|||
# Dart 3.3 发布:扩展类型、JavaScript Interop 等
|
||||
|
||||
> 参考链接:https://medium.com/dartlang/dart-3-3-325bf2bf6c13
|
||||
|
||||
跟随 [Flutter 3.19 发布](https://juejin.cn/post/7334503381200781363)的还有 Dart 3.3 ,Dart 3.3 主要包含扩展类型增强,性能优化和 native 代码交互推进,例如本次改进的**JavaScript Interop** 模型就引入了类型安全,[所以这一切都为 WebAssembly 支持铺平了道路](https://juejin.cn/post/7232164444985622588)。
|
||||
|
||||
>在[《Flutter 2024 路线规划里》](https://juejin.cn/post/7335067315452428297) ,**Web 平台上未来 CanvasKit 将成为默认渲染**,所以未来 Dart 在 Web 上肯定是 Wasm Native 的路线。
|
||||
|
||||
|
||||
|
||||
# 扩展类型
|
||||
|
||||
扩展类型是一种编译时抽象,它用不同的纯静态接口来 “Wrapper” 现有类型,同时它们也是 Dart 和[静态 JS 互操作](https://dart.dev/go/next-gen-js-interop)的主要实现基础,因为它们可以轻松修改现有类型的接口(对于任何类型的相互调用都至关重要),而不会产生实际 Wrapper 的成本。
|
||||
|
||||
```dart
|
||||
extension type E(int i) {
|
||||
// Define set of operations.
|
||||
}
|
||||
```
|
||||
|
||||
扩展类型引入了类型的零成本 wrappers,使用它们来优化性能敏感的代码,尤其是在与 native 平台交互时,扩展类型提供了具有特定成员自定义类型的便利性,同时消除了典型的 wrappers 分配开销。
|
||||
|
||||
```dart
|
||||
extension type Wrapper(int i) {
|
||||
void showValue() {
|
||||
print('my value is $i');
|
||||
}
|
||||
}
|
||||
|
||||
void main() {
|
||||
final wrapper = Wrapper(42);
|
||||
wrapper.showValue(); // Prints 'my value is 42'
|
||||
}
|
||||
```
|
||||
|
||||
上面的例子实现了一个 **`Wrapper`** 扩展类型,但将其用作普通的 Dart 类型,在实际使用里,开发者可以实例化它并调用函数。
|
||||
|
||||
这里的主要区别在于 Dart 将其编译为普通 Dart **`int`** 类型,扩展类型允许创建具有唯一的成员类型,而无需分配典型 wrappers 类型的间接成本,例如以下例子包装了对应的 `int` 类型以创建仅允许对 ID 号有意义的操作的扩展类型。
|
||||
|
||||
```dart
|
||||
extension type IdNumber(int id) {
|
||||
// Wraps the 'int' type's '<' operator:
|
||||
operator <(IdNumber other) => id < other.id;
|
||||
// Doesn't declare the '+' operator, for example,
|
||||
// because addition does not make sense for ID numbers.
|
||||
}
|
||||
|
||||
void main() {
|
||||
// Without the discipline of an extension type,
|
||||
// 'int' exposes ID numbers to unsafe operations:
|
||||
int myUnsafeId = 42424242;
|
||||
myUnsafeId = myUnsafeId + 10; // This works, but shouldn't be allowed for IDs.
|
||||
|
||||
var safeId = IdNumber(42424242);
|
||||
safeId + 10; // Compile-time error: No '+' operator.
|
||||
myUnsafeId = safeId; // Compile-time error: Wrong type.
|
||||
myUnsafeId = safeId as int; // OK: Run-time cast to representation type.
|
||||
safeId < IdNumber(42424241); // OK: Uses wrapped '<' operator.
|
||||
}
|
||||
```
|
||||
|
||||
因此,虽然 [extension members](https://dart.dev/language/extension-methods) 功能(Dart 2.7 开始)允许向现有类型添加函数和属性,但扩展类型功能也可以执行相同的操作,**并且还允许定义隐藏底层表示的新 API**。
|
||||
|
||||
这对于与 native code 的相互调用特别有用。可以直接使用原生类型,无需创建 Wrapper 和相关间接的成本,同时仍然提供干净的生产 Dart API。
|
||||
|
||||
> 扩展类型与 Wrapper 具有相同的用途,但不需要创建额外的运行时对象,当开发者需要包装大量对象时,Wrapper 这个行为可能会变得昂贵,由于扩展类型仅是静态的并且在运行时编译,因此它们本质上是零成本。
|
||||
>
|
||||
> [**扩展方法**](https://dart.dev/language/extension-methods)(也称为“扩展”)是类似于扩展类型的静态抽象。但是扩展方法是**直接**向其基础类型的每个实例添加功能;扩展类型不同,**扩展类型的接口仅适用于静态类型为该扩展类型的表达式**。
|
||||
>
|
||||
> 默认情况下它们与其基础类型的接口不同。
|
||||
|
||||
扩展类型有两个同样有效但本质上不同的核心用例:
|
||||
|
||||
- **为现有类型提供扩展接口**,当扩展类型实现其表示类型时,一般可以认为它是“透明的”,因为它允许扩展类型“看到”底层类型。
|
||||
|
||||
透明扩展类型可以调用表示类型的所有成员([未重新声明的](https://dart.dev/language/extension-types#redeclare)),以及它定义的任何辅助成员,这将为现有类型创建一个新的扩展接口,新接口可用于静态类型为扩展类型的表达式,例如如下代码里,`v1.i` 可以正常调用,但是 int 类似的` v2` 不可以调用 `v2.i`:
|
||||
|
||||
```dart
|
||||
extension type NumberT(int value)
|
||||
implements int {
|
||||
// Doesn't explicitly declare any members of 'int'.
|
||||
NumberT get i => this;
|
||||
}
|
||||
void main () {
|
||||
// All OK: Transparency allows invoking `int` members on the extension type:
|
||||
var v1 = NumberT(1); // v1 type: NumberT
|
||||
int v2 = NumberT(2); // v2 type: int
|
||||
var v3 = v1.i - v1; // v3 type: int
|
||||
var v4 = v2 + v1; // v4 type: int
|
||||
var v5 = 2 + v1; // v5 type: int
|
||||
// Error: Extension type interface is not available to representation type
|
||||
v2.i;
|
||||
}
|
||||
```
|
||||
![](http://img.cdn.guoshuyu.cn/20240216_Dart-303/image1.png)
|
||||
|
||||
- **为现有类型提供不同的接口**,[不透明](https://dart.dev/language/extension-types#transparency)的扩展类型(不是 [`implement `](https://dart.dev/language/extension-types#implements)其表示类型)被静态地视为全新类型,与其表示类型不同,所以无法将其分配给其表示类型,并且它不会公开其表示类型的成员,例如 `NumberE` 不能为 int ,并且 :
|
||||
|
||||
```dart
|
||||
extension type NumberE(int value) {
|
||||
NumberE operator +(NumberE other) =>
|
||||
NumberE(value + other.value);
|
||||
|
||||
NumberE get next => NumberE(value + 1);
|
||||
bool isValid() => !value.isNegative;
|
||||
}
|
||||
|
||||
void testE() {
|
||||
var num1 = NumberE(1);
|
||||
int num2 = NumberE(2); // Error: Can't assign 'NumberE' to 'int'.
|
||||
|
||||
num1.isValid(); // OK: Extension member invocation.
|
||||
num1.isNegative(); // Error: 'NumberE' does not define 'int' member 'isNegative'.
|
||||
|
||||
var sum1 = num1 + num1; // OK: 'NumberE' defines '+'.
|
||||
var diff1 = num1 - num1; // Error: 'NumberE' does not define 'int' member '-'.
|
||||
var diff2 = num1.value - 2; // OK: Can access representation object with reference.
|
||||
var sum2 = num1 + 2; // Error: Can't assign 'int' to parameter type 'NumberE'.
|
||||
|
||||
List<NumberE> numbers = [
|
||||
NumberE(1),
|
||||
num1.next, // OK: 'i' getter returns type 'NumberE'.
|
||||
1, // Error: Can't assign 'int' element to list type 'NumberE'.
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20240216_Dart-303/image2.png)
|
||||
|
||||
另外需要注意,**扩展类型是编译时包装构造,在运行时绝对没有扩展类型的踪迹**,任何类型查询或类似的运行时操作都适用于表示类型,在任何情况下,扩展类型的表示类型都不是其子类型,因此在需要扩展类型的情况下表示类型不能互换使用。
|
||||
|
||||
# JavaScript Interop
|
||||
|
||||
Dart 3.3 引入了一种与 JavaScript 和 Web 相互调用的新模型,它从一组用于与 JavaScript 交互的新 API 开始:[dart:js_interop](https://api.dart.dev/dart-js_interop/dart-js_interop-library.html) 。
|
||||
|
||||
现在,**Dart 开发人员可以访问类型化 API 来与 JavaScript 交互**,该 API 通过静态强制明确定义了两种语言之间的边界,在编译之前消除了许多问题。
|
||||
|
||||
除了用于访问 JavaScript 代码的新 API 之外,Dart 现在还包含一个新模型,用于使用扩展类型在 Dart 中表示 JavaScript 类型,如下代码就是前面拓展类型的实际使用实例:
|
||||
|
||||
```dart
|
||||
import 'dart:js_interop';
|
||||
|
||||
/// Represents the `console` browser API.
|
||||
extension type MyConsole(JSObject _) implements JSObject {
|
||||
external void log(JSAny? value);
|
||||
external void debug(JSAny? value);
|
||||
external void info(JSAny? value);
|
||||
external void warn(JSAny? value);
|
||||
}
|
||||
```
|
||||
|
||||
基于扩展类型的语法比扩展成员允许更多的表达和健全性。这简化了 Dart 中 JavaScript API 的利用,更多详细信息可以查看:https://dart.dev/interop/js-interop 。
|
||||
|
||||
# 改进 browser libraries
|
||||
|
||||
从 1.0 版本开始,Dart SDK 就包含了一套全面的 browser libraries,其中包括核心 [dart:html](https://api.dart.dev/dart-html/dart-html-library.html) 库以及 SVG、WebGL 等库。
|
||||
|
||||
改进后的 JavaScript 调用模型提供了重新构想这些库的机会,未来 browser libraries 支持将集中在 [package:web](https://pub.dev/packages/web)上,这简化了版本控制、加速了更新并与[MDN](https://developer.mozilla.org/)资源保持一致,这一系列的改进推动了[将 Dart 编译为 WebAssembly](https://juejin.cn/post/7232164444985622588)。
|
||||
|
||||
# 从今天开始,开启 WebAssembly 的未来
|
||||
|
||||
Dart 3.3 为WebAssembly 的 Web 应用奠定基础,虽然 Flutter Web 中的 WebAssembly 支持仍处于试验阶段,但是这对于 Dart 和 Flutter 是明显的方向。
|
||||
|
||||
要使用 WebAssembly 在 Web 上运行 Flutter 应用,需要使用新的 JavaScript Interop 机制和 `package:web` ,旧版 JavaScript 和 browser libraries 保持不变,并支持编译为 JavaScript 代码。但是,如果编译为 WebAssembly 需要迁移,例如:
|
||||
|
||||
```dart
|
||||
import 'dart:html' as html; // Remove
|
||||
import 'package:web/web.dart' as web; // Add
|
||||
|
||||
dependencies:
|
||||
web: ^0.5.0
|
||||
```
|
||||
|
||||
> 更多可见:https://dart.dev/interop/js-interop/package-web
|
|
@ -0,0 +1,102 @@
|
|||
# Flutter 小技巧之升级适配 Xcode15
|
||||
|
||||
美好的 2024 从「适配」开始,按照苹果的尿性,2024 春季开始大家将不得使用 Xcode15 来构建 App ,另外根据[《2024 的 iOS 的隐私清单》](https://juejin.cn/post/7311876701909549065) 要求,使用 Flutter 的开发者是无法逃避适配 Xcode15 更新的命运。
|
||||
|
||||
另外,众所周知,**安装 Xcode15 需要系统升级到 macOS Sonoma ,而 Sonoma 版本无法直接使用 Xcode14** ,所以升级到 Sonoma 系统后你会看到 Xcode 无法打开,不要急,因为升级 Xcode15 现在只要 3G+ ,模拟器(7G+)部分可以下载完成后再手动二次下载,老板再也不用当心我更新 Xcode 时「摸鱼」了。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20240108_xcode15/image1.png)
|
||||
|
||||
> PS,如果因为特殊情况,你想先升级 Sonoma 又暂时想使用 Xcode14,但是不想降级系统 ,可以在命令行通过 `/Applications/Xcode.app/Contents/MacOS/Xcode` 执行激活 14 安装,然后通过命令行编译。
|
||||
|
||||
那么,接下来开始适配 Xcode15 吧~
|
||||
|
||||
# Crash 问题
|
||||
|
||||
**使用 Xcode 15 构建 Flutter 的时候,你可能会有低于 iOS 17 的真机设备上发现 App 运行崩溃**,这个问题提交在 [#136060](https://github.com/flutter/flutter/issues/136060) ,直接引发问题的点大部分来自引入的 Plugin,例如 `connectivity_plus` ,而核心问题其实算是 Xcode 本身的一个 bug。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20240108_xcode15/image2.png)
|
||||
|
||||
解决问题的点很简单,**就是将 IPHONEOS_DEPLOYMENT_TARGET 设置为 12.0** , 另外有时候 Xcode 可能会删除不受支持的`IPHONEOS_DEPLOYMENT_TARGET` 值,而导致使用了最新的 (17.0),这将导致二进制文件只能在 iOS 17+ 上启动。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20240108_xcode15/image3.png)
|
||||
|
||||
类似问题也体现在如 `connectivity_plus 4.xx` 的 IPHONEOS_DEPLOYMENT_TARGET 为11.0,而现在connectivity_plus 5.0.0 中也调整到 12 从而规避问题。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20240108_xcode15/image4.png)
|
||||
|
||||
另外,如果 Plugin 的 IPHONEOS_DEPLOYMENT_TARGET 影响编译,你也可以在 Profile 下添加 `config.build_settings.delete` 来强制移除。
|
||||
|
||||
```
|
||||
post_install do |installer|
|
||||
installer.pods_project.targets.each do |target|
|
||||
flutter_additional_ios_build_settings(target)
|
||||
target.build_configurations.each do |config|
|
||||
config.build_settings.delete 'IPHONEOS_DEPLOYMENT_TARGET' <--- add this
|
||||
```
|
||||
|
||||
目前这个问题在模拟器上运行一般不会出现,主要是 Xcode 15 带有 IPHONEOS_DEPLOYMENT_TARGET 的 iOS 11(以前 Flutter 默认最低版本)在使用该 `Networking` 框架时会崩溃 ,具体表现在:
|
||||
|
||||
- 16.x -> 崩溃
|
||||
- 17.0.x -> 正常
|
||||
|
||||
**所以在升级到 Xcode 15 的时候,最好将 App 运行到 16.x 的真机上测试一下是否存在同样问题**,目前看来主要是 iOS 的 `Network` 在存在 target iOS 11 导致,能够用 `NWProtocolIP.Metadata ` ,`NWEndpoint.hostPort` 去复现,其实编译时也会有一些警告,只是一般都被大家忽略:
|
||||
|
||||
```
|
||||
…/Test737672.xcodeproj The iOS deployment target 'IPHONEOS_DEPLOYMENT_TARGET' is set to 11.0, but the range of supported deployment target versions is 12.0 to 17.0.99.
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# `Flutter/Flutter.h` file not found
|
||||
|
||||
Flutter 在 Xcode 15 上的这个问题提交于 [#135099](https://github.com/flutter/flutter/issues/135099) ,其实算是 Cocoapods 的问题,这个问题可能涉及`DT_TOOLCHAIN_DIR cannot be used to evaluate LIBRARY_SEARCH_PATHS, use TOOLCHAIN_DIR instead` 。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20240108_xcode15/image5.png)
|
||||
|
||||
根据反馈,**基本上就是你升级 Cocoapods 升级到 v1.13.0 之后的版本就可以解决**,注意升级之后记得手动重新运行 `pod install` 来确保正确执行,当然,如果你因为某些原因不想升级 Cocoapods ,那么可以临时通过CocoaPods 的 [#12012#issuecomment-1659803356](https://github.com/CocoaPods/CocoaPods/issues/12012#issuecomment-1659803356) ,在 Profile 下添加相关路径:
|
||||
|
||||
```
|
||||
post_install do |installer|
|
||||
installer.pods_project.targets.each do |target|
|
||||
flutter_additional_ios_build_settings(target)
|
||||
target.build_configurations.each do |config|
|
||||
xcconfig_path = config.base_configuration_reference.real_path
|
||||
xcconfig = File.read(xcconfig_path)
|
||||
xcconfig_mod = xcconfig.gsub(/DT_TOOLCHAIN_DIR/, "TOOLCHAIN_DIR")
|
||||
File.open(xcconfig_path, "w") { |file| file << xcconfig_mod }
|
||||
end
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
> PS ,如果更新 pod 的时候,不知道卡在那里,可以通过 `gem install cocoapods -v 1.13.0 -V` 后面的 -V 看详细操作日志,如果是网络问题,可以通过 gem sources --add https://gems.ruby-china.com/ --remove https://rubygems.org/ 来解决,可以通过 gem sources -l 查看镜像地址。
|
||||
|
||||
|
||||
|
||||
# Library 'iconv.2.4.0' not found
|
||||
|
||||
如果你在 Xcode 15 上运行发现如下所示错误,不要相信什么 `other link flags add "-ld64"` ,而是应该在 `Build Phases > Link Binary With Libraries` 下找到 iconv.2.4.0 ,然后删除它,然后添加 iconv.2,因为在 Xcode15 里,现在只有 iconv.2 。
|
||||
|
||||
```
|
||||
Error (Xcode): Library 'iconv.2.4.0' not found
|
||||
|
||||
Error (Xcode): Linker command failed with exit code 1 (use -v to see invocation)
|
||||
```
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20240108_xcode15/image6.png)
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20240108_xcode15/image7.png)
|
||||
|
||||
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20240108_xcode15/image8.png)
|
||||
|
||||
> **如果还有问题,可以全局搜索 'iconv.2.4.0',在出来的文件里将 iconv.2.4.0 替换为 iconv.2 即可**。
|
||||
|
||||
|
||||
|
||||
# 最后
|
||||
|
||||
好了,Xcode 15 的适配上主要就这些问题,更多的还是[《2024 的 iOS 的隐私清单》](https://juejin.cn/post/7311876701909549065) 部分的适配,属于审核要求,相比起代码上能解决的,平台要求就需要精神领会了,因为这些的要求内容其实很主观理解,总的来说, Flutter & Xcode 15 ,跑得通,可以上。
|
|
@ -92,6 +92,10 @@
|
|||
* [Flutter 与 Dart 的市场应用](Flutter-WH.md)
|
||||
* [Flutter 小技巧之不一样的思路实现炫酷 3D 翻页折叠动画](Flutter-GLSL.md)
|
||||
* [Flutter 小技巧之 3.16 升级最坑 M3 默认适配技巧](Flutter-M3D.md)
|
||||
* [2024 Flutter 重大更新,Dart 宏(Macros)编程开始支持,JSON 序列化有救](Flutter-macros.md)
|
||||
* [Flutter 小技巧之升级适配 Xcode15 ](Flutter-X15.md)
|
||||
* [Flutter 2024 路线规划,更多可期待的功能正在路上](Flutter-2024.md)
|
||||
* [2024 Impeller:快速了解 Flutter 的渲染引擎的优势](Flutter-Impeller.md)
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -0,0 +1,596 @@
|
|||
# Flutter 3.19 发布,快来看看有什么更新吧?
|
||||
|
||||
> 参考链接:https://medium.com/flutter/whats-new-in-flutter-3-19-58b1aae242d2
|
||||
|
||||
新年假期的尾巴,Flutter 迎来了 3.19 更新,该版本带来了 **Gemini(Google's most capable AI ) 的 Dart SDK,更好控制动画颗粒度的 Widget ,Impeller 的性能增强和 Android 优化支持,deep links 工具支持,Android 和 iOS 上的特定平台新支持,Windows Arm64 支持**等等。
|
||||
|
||||
> 普遍优化修复居多。
|
||||
|
||||
# Gemini Dart SDK
|
||||
|
||||
Google 的 AI Dart SDK Gemini 目前已经发布,pub 上的 [google_generative_ai](https://pub.dev/packages/google_generative_ai) 将 Gemini 的生成式 AI 功能支持到 Dart 或 Flutter 应用里,Google Generative AI SDK 可以更方便地让 Dart 开发人员在 App 里集成 LLM 的 AI 能力。
|
||||
|
||||
```dart
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:google_generative_ai/google_generative_ai.dart';
|
||||
|
||||
void main() async {
|
||||
// Access your API key as an environment variable (see first step above)
|
||||
final apiKey = Platform.environment['API_KEY'];
|
||||
if (apiKey == null) {
|
||||
print('No \$API_KEY environment variable');
|
||||
exit(1);
|
||||
}
|
||||
// For text-and-image input (multimodal), use the gemini-pro-vision model
|
||||
final model = GenerativeModel(model: 'gemini-pro-vision', apiKey: apiKey);
|
||||
final (firstImage, secondImage) = await (
|
||||
File('image0.jpg').readAsBytes(),
|
||||
File('image1.jpg').readAsBytes()
|
||||
).wait;
|
||||
final prompt = TextPart("What's different between these pictures?");
|
||||
final imageParts = [
|
||||
DataPart('image/jpeg', firstImage),
|
||||
DataPart('image/jpeg', secondImage),
|
||||
];
|
||||
final response = await model.generateContent([
|
||||
Content.multi([prompt, ...imageParts])
|
||||
]);
|
||||
print(response.text);
|
||||
}
|
||||
```
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20240216_Flutter-319/image1.png)
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20240216_Flutter-319/image2.png)
|
||||
|
||||
# Framework
|
||||
|
||||
## 滚动优化
|
||||
|
||||
在 3.19 之前,使用两根手指在 Flutter 列表上进行滑动时,Flutter 的滚动速度会加快到两倍,这一直是一个饱受争议的问题,现在,从 3.19 开始,**开发者可以使用 `MultiTouchDragStrategy.latestPointer` 来配置默认的 `ScrollBehavior`** ,从而让滑动效果与手指数量无关。
|
||||
|
||||
`ScrollBehavior.multitouchDragStrategy` 默认情况下会防止多个手指同时与可滚动对象进行交互,从而影响滚动速度,如果之前你已经依赖老板本这个多指滑动能力,那么可以通过 `MaterialApp.scrollBehavior` / `CupertinoApp.scrollBehavior` 去恢复:
|
||||
|
||||
```dart
|
||||
class MyCustomScrollBehavior extends MaterialScrollBehavior {
|
||||
// Override behavior methods and getters like multitouchDragStrategy
|
||||
@override
|
||||
MultitouchDragStrategy get multitouchDragStrategy => MultitouchDragStrategy.sumAllPointers;
|
||||
}
|
||||
|
||||
// Set ScrollBehavior for an entire application.
|
||||
MaterialApp(
|
||||
scrollBehavior: MyCustomScrollBehavior(),
|
||||
// ...
|
||||
);
|
||||
```
|
||||
|
||||
或者通过 `ScrollConfiguration` 进行局部配置:
|
||||
|
||||
```dart
|
||||
class MyCustomScrollBehavior extends MaterialScrollBehavior {
|
||||
// Override behavior methods and getters like multitouchDragStrategy
|
||||
@override
|
||||
MultitouchDragStrategy get multitouchDragStrategy => MultitouchDragStrategy.sumAllPointers;
|
||||
}
|
||||
|
||||
// ScrollBehavior can be set for a specific widget.
|
||||
final ScrollController controller = ScrollController();
|
||||
ScrollConfiguration(
|
||||
behavior: MyCustomScrollBehavior(),
|
||||
child: ListView.builder(
|
||||
controller: controller,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
return Text('Item $index');
|
||||
},
|
||||
),
|
||||
);
|
||||
```
|
||||
|
||||
> 详细可参考:https://docs.flutter.dev/release/breaking-changes/multi-touch-scrolling
|
||||
|
||||
另外,本次 3.19 还修复了 [SingleChildScrollView#136871](https://github.com/flutter/flutter/pull/136871) 和 [ReorderableList#136828](https://github.com/flutter/flutter/pull/136828) 相关的崩溃问题,同时 [two_dimensional_scrollables](https://pub-web.flutter-io.cn/packages/two_dimensional_scrollables) 也修复了一些问题,比如在任一方向上正在进行滚动时出现拖动或者点击,scroll activity 将按预期停止。
|
||||
|
||||
最后,two_dimensional_scrollables 上的 TableView 控件也进行了多次更新,提供了需要改进,例如添加了对合并单元格的支持,并在上一个稳定版本 3.16 之后适配了更多 2D foundation。
|
||||
|
||||
## AnimationStyle
|
||||
|
||||
来自社区 [@TahaTesser](https://github.com/TahaTesser) 的贡献,现在 Flutter 开发者使用 AnimationStyle ,可以让用户快速覆盖 Widget 中的默认动画行为,就像 `MaterialApp` 、 `ExpansionTile` 和 `PopupMenuButton` :
|
||||
|
||||
```dart
|
||||
popUpAnimationStyle: AnimationStyle(
|
||||
curve: Easing.emphasizedAccelerate,
|
||||
duration: Durations.medium4,
|
||||
),
|
||||
```
|
||||
|
||||
```dart
|
||||
return MaterialApp(
|
||||
themeAnimationStyle: AnimationStyle.noAnimation,
|
||||
```
|
||||
|
||||
|
||||
|
||||
## SegmentedButton.styleFrom
|
||||
|
||||
来自社区成员 [@AcarFurkan](https://github.com/AcarFurkan) 的贡献,该静态方式就像其他按钮类型提供的方法一样。可以快速创建分段按钮的按钮样式,可以与其他分段按钮共享或用于配置应用的分段按钮主题。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20240216_Flutter-319/image3.png)
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20240216_Flutter-319/image4.png)
|
||||
|
||||
|
||||
|
||||
## Adaptive Switch
|
||||
|
||||
Adaptive Switch 可以让 Widget 在 macOS 和 iOS 上看起来和感觉是原生的效果,并且在其他地方具有 Material Design 的外观和感觉,它不依赖于 Cupertino 库,因此它的 API 在所有平台上都完全相同。
|
||||
|
||||
```dart
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Flutter code sample for [Switch.adaptive].
|
||||
|
||||
void main() => runApp(const SwitchApp());
|
||||
|
||||
class SwitchApp extends StatefulWidget {
|
||||
const SwitchApp({super.key});
|
||||
|
||||
@override
|
||||
State<SwitchApp> createState() => _SwitchAppState();
|
||||
}
|
||||
|
||||
class _SwitchAppState extends State<SwitchApp> {
|
||||
bool isMaterial = true;
|
||||
bool isCustomized = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final ThemeData theme = ThemeData(
|
||||
platform: isMaterial ? TargetPlatform.android : TargetPlatform.iOS,
|
||||
adaptations: <Adaptation<Object>>[
|
||||
if (isCustomized) const _SwitchThemeAdaptation()
|
||||
]);
|
||||
final ButtonStyle style = OutlinedButton.styleFrom(
|
||||
fixedSize: const Size(220, 40),
|
||||
);
|
||||
|
||||
return MaterialApp(
|
||||
theme: theme,
|
||||
home: Scaffold(
|
||||
appBar: AppBar(title: const Text('Adaptive Switches')),
|
||||
body: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
OutlinedButton(
|
||||
style: style,
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
isMaterial = !isMaterial;
|
||||
});
|
||||
},
|
||||
child: isMaterial
|
||||
? const Text('Show cupertino style')
|
||||
: const Text('Show material style'),
|
||||
),
|
||||
OutlinedButton(
|
||||
style: style,
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
isCustomized = !isCustomized;
|
||||
});
|
||||
},
|
||||
child: isCustomized
|
||||
? const Text('Remove customization')
|
||||
: const Text('Add customization'),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
const SwitchWithLabel(label: 'enabled', enabled: true),
|
||||
const SwitchWithLabel(label: 'disabled', enabled: false),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class SwitchWithLabel extends StatefulWidget {
|
||||
const SwitchWithLabel({
|
||||
super.key,
|
||||
required this.enabled,
|
||||
required this.label,
|
||||
});
|
||||
|
||||
final bool enabled;
|
||||
final String label;
|
||||
|
||||
@override
|
||||
State<SwitchWithLabel> createState() => _SwitchWithLabelState();
|
||||
}
|
||||
|
||||
class _SwitchWithLabelState extends State<SwitchWithLabel> {
|
||||
bool active = true;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
Container(
|
||||
width: 150,
|
||||
padding: const EdgeInsets.only(right: 20),
|
||||
child: Text(widget.label)),
|
||||
Switch.adaptive(
|
||||
value: active,
|
||||
onChanged: !widget.enabled
|
||||
? null
|
||||
: (bool value) {
|
||||
setState(() {
|
||||
active = value;
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SwitchThemeAdaptation extends Adaptation<SwitchThemeData> {
|
||||
const _SwitchThemeAdaptation();
|
||||
|
||||
@override
|
||||
SwitchThemeData adapt(ThemeData theme, SwitchThemeData defaultValue) {
|
||||
switch (theme.platform) {
|
||||
case TargetPlatform.android:
|
||||
case TargetPlatform.fuchsia:
|
||||
case TargetPlatform.linux:
|
||||
case TargetPlatform.windows:
|
||||
return defaultValue;
|
||||
case TargetPlatform.iOS:
|
||||
case TargetPlatform.macOS:
|
||||
return SwitchThemeData(
|
||||
thumbColor: MaterialStateProperty.resolveWith<Color?>(
|
||||
(Set<MaterialState> states) {
|
||||
if (states.contains(MaterialState.selected)) {
|
||||
return Colors.yellow;
|
||||
}
|
||||
return null; // Use the default.
|
||||
}),
|
||||
trackColor: const MaterialStatePropertyAll<Color>(Colors.brown),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
> 详细可见:https://main-api.flutter.dev/flutter/material/Switch/Switch.adaptive.html
|
||||
|
||||
|
||||
|
||||
### SemanticsProperties 可访问性标识符
|
||||
|
||||
3.19 里`SemanticsProperties` 添加了新的可访问性标识符,为 native 可访问性层次结构中的语义节点提供标识符。
|
||||
|
||||
- 在 Android 上,它在辅助功能层次结构中显示为 “resource-id”
|
||||
|
||||
- 在 iOS 上是设置里 `UIAccessibilityElement.accessibilityIdentifier`
|
||||
|
||||
|
||||
|
||||
## MaterialStatesController
|
||||
|
||||
`TextField` 和 `TextFormField` 添加了 `MaterialStatesController` ,因为在此之前,开发者无法确定 `TextFormField` 当前是否处于错误状态,例如:
|
||||
|
||||
- 它显示错误消息并使用了`errorBorder`
|
||||
- 确定它是否 foucs,但前提是提供自己的 `FocusNode`
|
||||
|
||||
而现在允许开发者提供自己的 `MaterialStatesController`(类似于`ElevatedButton`),以便开发者可以完全访问有关这些控件的状态信息。
|
||||
|
||||
```dart
|
||||
final MaterialStatesController statesController = MaterialStatesController();
|
||||
statesController.addListener(valueChanged);
|
||||
|
||||
TextField(
|
||||
statesController: statesController,
|
||||
controller: textEditingController,
|
||||
)
|
||||
```
|
||||
|
||||
|
||||
|
||||
## UndoHistory stack
|
||||
|
||||
修复了 undo/redo 历史在日语键盘上可能消失的问题,并使其现在可以在将条目推送到 UndoHistory 堆栈之前对其进行修改。
|
||||
|
||||
`UndoHistory` 是一个提供撤消/重做功能的 Widget,它还具有绑定到特定于平台的实现的底层接口,监听了键盘事件以实现 undo/redo 操作。
|
||||
|
||||
从 Flutter 3.0.0 开始,可以将 `UndoHistoryController` 传递给 `TextField`它附带了 `UndoHistoryValue` 。
|
||||
|
||||
对于一个非常简单的展示,我将创建一个 UndoHistoryController 实例,将其传递给 TextField,使用 ValueListenableBuilder 监听该实例,并在构建器中的按钮上返回一行以执行撤消/重做操作。
|
||||
|
||||
```dart
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Flutter code sample for [UndoHistoryController].
|
||||
|
||||
void main() {
|
||||
runApp(const UndoHistoryControllerExampleApp());
|
||||
}
|
||||
|
||||
class UndoHistoryControllerExampleApp extends StatelessWidget {
|
||||
const UndoHistoryControllerExampleApp({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const MaterialApp(
|
||||
home: MyHomePage(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class MyHomePage extends StatefulWidget {
|
||||
const MyHomePage({super.key});
|
||||
|
||||
@override
|
||||
State<MyHomePage> createState() => _MyHomePageState();
|
||||
}
|
||||
|
||||
class _MyHomePageState extends State<MyHomePage> {
|
||||
final TextEditingController _controller = TextEditingController();
|
||||
final FocusNode _focusNode = FocusNode();
|
||||
final UndoHistoryController _undoController = UndoHistoryController();
|
||||
|
||||
TextStyle? get enabledStyle => Theme.of(context).textTheme.bodyMedium;
|
||||
TextStyle? get disabledStyle =>
|
||||
Theme.of(context).textTheme.bodyMedium?.copyWith(color: Colors.grey);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
TextField(
|
||||
maxLines: 4,
|
||||
controller: _controller,
|
||||
focusNode: _focusNode,
|
||||
undoController: _undoController,
|
||||
),
|
||||
ValueListenableBuilder<UndoHistoryValue>(
|
||||
valueListenable: _undoController,
|
||||
builder: (BuildContext context, UndoHistoryValue value,
|
||||
Widget? child) {
|
||||
return Row(
|
||||
children: <Widget>[
|
||||
TextButton(
|
||||
child: Text('Undo',
|
||||
style: value.canUndo ? enabledStyle : disabledStyle),
|
||||
onPressed: () {
|
||||
_undoController.undo();
|
||||
},
|
||||
),
|
||||
TextButton(
|
||||
child: Text('Redo',
|
||||
style: value.canRedo ? enabledStyle : disabledStyle),
|
||||
onPressed: () {
|
||||
_undoController.redo();
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
|
||||
|
||||
# Engine
|
||||
|
||||
## Impeller 进度
|
||||
|
||||
### Android OpenGL 预览
|
||||
|
||||
在 3.16 稳定版本中,Flutter 官方邀请了用户在支持 Vulkan 的 Android 设备上试用 Impeller,覆盖了该领域 77% 的 Android 设备,而在过去的几个月里,Flutter 官方团队让 Impeller 的 OpenGL 达到了与 Vulkan 同等的功能,例如添加[支持 MSAA](https://github.com/flutter/engine/pull/47030)。
|
||||
|
||||
这意味着几乎所有 Android 设备上的 Flutter 应用都有望支持 Impeller 渲染,除了少数即将推出的剩余功能除外,例如自定义着色器和对外部纹理的完全支持,目前官方团队表示在今年晚些时候 Androd 也会将 Impeller 作为默认渲染器。
|
||||
|
||||
另外,Impeller 的 Vulkan 在“调试”构建中启用了超出 Skia 的附加调试功能,并且这些功能会产生额外的运行时开销。因此,有关 Impeller 性能的反馈你许来自 profile 或 release 版本,并且需要包括 DevTools 的时间表以及与同一设备上的 Skia 后端的比较。
|
||||
|
||||
### 路线
|
||||
|
||||
在实现了渲染保真度之后,在 Impeller Android 预览期间的主要关注点是性能,另外一些更大的改进也正在进行,例如能够利用 [Vulkan subpasses](https://github.com/flutter/flutter/issues/128911) 大大提高高级混合模式的性能。
|
||||
|
||||
此外,Flutter 官方还期望渲染策略发生变化,不再总是将 CPU 上的每条路径细分为[先模板后覆盖](https://github.com/flutter/flutter/issues/137714) ,这样的实现将大大降低 Android 和 iOS 上 Impeller 的 CPU 利用率。
|
||||
|
||||
最后,Flutter 还期望新的[高斯模糊](https://github.com/flutter/flutter/issues/131580)实施能匹配 Skia 实现的吞吐量,并改进 iOS 上模糊的惯用用法。
|
||||
|
||||
## API 改进
|
||||
|
||||
### 字形信息
|
||||
|
||||
3.19 版本包括两个新的 dart:ui 方法:`Paragraph ` 的 `getClosestGlyphInfoForOffset` 和 `getGlyphInfoAt`,这两个方式都会返回一个新类型的对象字形信息,包含段落内字符(或视觉上相连的字符序列)的尺寸。
|
||||
|
||||
- [Paragraph.getGlyphInfoAt](https://main-api.flutter.dev/flutter/dart-ui/Paragraph/getGlyphInfoAt.html),查找与文本中的代码单元关联的 [GlyphInfo ](https://main-api.flutter.dev/flutter/dart-ui/GlyphInfo-class.html)
|
||||
- [Paragraph.getClosestGlyphInfoForOffset](https://main-api.flutter.dev/flutter/dart-ui/Paragraph/getClosestGlyphInfoForOffset.html),查找屏幕上最接近给定 Offset 的字形的 GlyphInfo
|
||||
|
||||
### GPU追踪
|
||||
|
||||
Metal 下的 Impeller(iOS、macOS、模拟器) 和在支持 Vulkan 的 Android 设备上,Flutter 引擎现在将在调试和 profile 构建中报告时间线中每个帧的 GPU 时间,可以在 DevTools 中的 “GPUTracer” 下检查 GPU 帧时序。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20240216_Flutter-319/image5.png)
|
||||
|
||||
请注意,由于非 Vulkan Android 设备可能会误报其对查询 GPU 计时的支持,因此只能通过在 AndroidManifest.xml 设置标志来启用 Impeller 的 GPU 跟踪:
|
||||
|
||||
```xml
|
||||
<meta-data
|
||||
android:name="io.flutter.embedding.android.EnableOpenGLGPUTracing"
|
||||
android:value="true" />
|
||||
```
|
||||
|
||||
## 性能优化
|
||||
|
||||
### Specialization Constants
|
||||
|
||||
Impeller 添加支持 [Specialization Constants](https://github.com/flutter/flutter/issues/119357) ,利用 Impeller 着色器中的这一功能,减少了 Flutter 引擎的未压缩二进制大小 350KB。
|
||||
|
||||
### Backdrop Filter 加速
|
||||
|
||||
3.19 版本包含一些不错的性能改进,其中就包括了 Impeller 的 Backdrop Filter 和模糊优化,特别是开源贡献者 [knopp](https://github.com/knopp) [noticed](https://github.com/flutter/flutter/issues/131567#issuecomment-1678210475) 注意到 Impeller 错误地请求了读取屏幕纹理的功能,[删除这个](https://github.com/flutter/engine/pull/47808)功能支持,在基准测试中,根据复杂程度,将包含多个背景滤镜的场景改进了 20-70%。
|
||||
|
||||
同时,Impeller 在每个背景滤镜上[不再无条件存储模板缓冲区](https://github.com/flutter/engine/pull/47397),相反,任何影响操作的剪辑都会被记录下来,并在恢复背景滤镜的保存层时重播到新的模板缓冲区中。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20240216_Flutter-319/image6.png)
|
||||
|
||||
通过这一更改,在运行具有 Vulkan 的 Impeller 的 Pixel 7 Pro 上进行动画高级混合模式基准测试时,将平均 GPU 帧时间从 55 毫秒改进到 16 毫秒,并将 90% 的光栅线程 CPU 时间从大约 110 毫秒改进到 22 毫秒。
|
||||
|
||||
# Android
|
||||
|
||||
## Deeplinking web 验证器
|
||||
|
||||
3.19 开始,Flutter 的 Deeplinking web 验证器的早期版本将被推出使用。
|
||||
|
||||
在该版本中,Flutter Deeplinking 验证器支持 Android 上的 Web 检查,这意味着可以验证 assetlinks.json 文件的设置。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20240216_Flutter-319/image7.png)
|
||||
|
||||
开发者可以打开DevTools,单击 “Deep Links” 选项,然后导入包含 Deeplinking 的 Flutter 项目,Deeplinking 验证器将告诉你配置是否正确。
|
||||
|
||||
Flutter 希望这个工具能够成为简化的 Deeplinking ,后续将继续补全 iOS 上的 Web 检查以及 iOS 和 Android 上的应用检查的支持。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20240216_Flutter-319/image8.png)
|
||||
|
||||
> 更多可以查阅 :https://docs.google.com/document/d/1fnWe8EpZleMtSmP0rFm2iulqS3-gA86z8u9IsnXjJak/edit
|
||||
|
||||
## 支持 Share.invoke
|
||||
|
||||
Android 平台上 Flutter 之前缺少默认 “share” 按钮,而本次 3.19 里将开始支持它,作为 Flutter 持续努力的一部分,以确保所有默认上下文菜单按钮在每个平台上都可用。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20240216_Flutter-319/image9.png)
|
||||
|
||||
> 更多相关进度可见:https://github.com/flutter/flutter/issues/107578
|
||||
|
||||
## Native assets
|
||||
|
||||
如果需要 Flutter 代码中与其他语言的其他函数进行互操作,现在可以在 Android 上通过执行 FFI 来处理 Native assets 。
|
||||
|
||||
简单来说就是,在此之前, Dart interop 一直在全面支持与 [Java 和 Kotlin](https://link.juejin.cn/?target=https%3A%2F%2Fdart.dev%2Fguides%2Flibraries%2Fjava-interop) 和 [Objective C 和 Swift](https://link.juejin.cn/?target=https%3A%2F%2Fdart.dev%2Fguides%2Flibraries%2Fobjective-c-interop) 的直接调用支持,例如在 Dart 3.2 开始,Native assets 就作为实验性测试支持,一直在解决与依赖于 Native 代码的 Dart 包分发相关的许多问题,它通过提供统一的钩子来与构建 Flutter 和独立 Dart 应用所涉及的各种构建需要。
|
||||
|
||||
Native Assets 可以让 Dart 包更无缝依赖和使用 Native 代码,通过 `flutter run`/`flutter build `和 `dart run`/`dart build` 构建并捆绑 Native 代码 。
|
||||
|
||||
> 备注:可通过 `flutter config --enable-native-assets` 和 `flutter create --template=package_ffi [package name]` 启用。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20240216_Flutter-319/image10.png)
|
||||
|
||||
Demo [`native_add_library`](https://github.com/dart-lang/native/tree/main/pkgs/native_assets_cli/example/native_add_library) 就展示了相关使用,当 Flutter 项目依赖 `package:native_add_library` 时, 脚本会自动在 `build.dart` 命令上调用:
|
||||
|
||||
```dart
|
||||
import 'package:native_add_library/native_add_library.dart';
|
||||
|
||||
void main() {
|
||||
print('Invoking a native function to calculate 1 + 2.');
|
||||
final result = add(1, 2);
|
||||
print('Invocation success: 1 + 2 = $result.');
|
||||
}
|
||||
```
|
||||
|
||||
> 更多可见:https://github.com/flutter/flutter/issues/129757
|
||||
|
||||
## 纹理层混合合成 (THLC) 模式
|
||||
|
||||
现在使 Google 地图 SDK 和文本输入框的放大镜功能时,他们都是工作在 TLHC 模式下,这会让 App 的性能得到不错的提升。
|
||||
|
||||
## 自定义 system-wide text selection toolbar 按键
|
||||
|
||||
Android 应用可以添加出现在所有文本选择菜单(长按文本时出现的菜单)中的自定义文本选择菜单项, Flutter 的 TextField 选择菜单现在包含了这些项目。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20240216_Flutter-319/image11.png)
|
||||
|
||||
> 在 Android 上,一般可以编写一个应用,将自定义按钮添加到系统范围的文本选择工具栏上。例如上图这里 Android 应用 AnkiDroid 在文本选择工具栏中添加了 “Anki Card ”按钮,并且它可以出现在任何应用中。
|
||||
|
||||
# iOS
|
||||
|
||||
## Flutter iOS 原生字体
|
||||
|
||||
Flutter 文本现在在 iOS 上看起来更紧凑、更像 native,因为根据苹果设计指南,iOS 上较小的字体应该更加分散,以便在移动设备上更容易阅读,而较大的字体应该更加紧凑,以免占用太多空间。
|
||||
|
||||
在此之前,我们在所有情况下都错误地使用了更小、间距更大的字体,现在默认情况下 Flutter 将为较大的文本使用紧凑字体。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20240216_Flutter-319/image12.png)
|
||||
|
||||
# 开发工具
|
||||
|
||||
## 开发工具更新
|
||||
|
||||
3.19 版本的 DevTools 的一些亮点包括:
|
||||
|
||||
● 在 DevTools 中添加了新功能以进行验证 Deeplinking
|
||||
|
||||
● 在 “Enhance Tracing” 菜单中添加了一个选项,用于跟踪平台 channel activity,这对于带有插件的应用很有用
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20240216_Flutter-319/image13.jpg)
|
||||
|
||||
● 当没有连接的应用时,性能和 CPU 分析器现在也可以使用,可以重新加载之前从 DevTools 保存的性能数据或 CPU 配置文件
|
||||
|
||||
● VS Code 中的 Flutter 侧边栏现在能够在当前项目未启用的情况下启用新平台,并且侧边栏中的 DevTools 菜单现在有一个在外部浏览器窗口中使用 DevTools 的选项
|
||||
|
||||
|
||||
|
||||
# 桌面
|
||||
|
||||
## Windows Arm64 支持
|
||||
|
||||
Windows 上的 Flutter 现在开始初步支持 Arm64 架构,目前仍处于开发阶段, 可以在GitHub [#62597 ](https://github.com/flutter/flutter/issues/62597)上查看进度,虽然目前对于 Flutter 开发者来说可能用处不是特别明显,但是也算是一个难得的桌面增强。
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# 生态系统
|
||||
|
||||
## 隐私清单
|
||||
|
||||
**Flutter 现在包含 iOS 上的隐私清单以满足[即将推出的 Apple 要求](https://juejin.cn/post/7311876701909549065) ,所以看来一般情况下,这个 Flutter 3.19 非升不可**。
|
||||
|
||||
## 包生态的进展
|
||||
|
||||
2023 年,pub package 生态增长了 26%,从 1 月份的 38,000 个 package 增加到 12 月底的 48,000 个,截至 2024 年 1 月,Pub.dev 现在每月活跃用户超过 700,000 名,
|
||||
|
||||
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20240216_Flutter-319/image14.png)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# 弃用和重大变更
|
||||
|
||||
## 放弃 Windows 7 和 8 支持
|
||||
|
||||
Dart 3.3 和 Flutter 3.19 版本停止对 Windows 7 和 8 的支持
|
||||
|
||||
## Impeller Dithering flag
|
||||
|
||||
正如 3.16 发布时所说的,现在全局标志 `Paint.enableDithering` 已经[删除](https://github.com/flutter/engine/pull/46745)。
|
||||
|
||||
## 弃用 iOS 11
|
||||
|
||||
Flutter 不再支持 iOS 11,由于调用某些网络 API 时会发生[运行时崩溃](https://github.com/flutter/flutter/issues/136060), Flutter 3.16.6 及更高版本构建的应用将不再支持 iOS11 ,详细可见 :https://juejin.cn/post/7321410906427359258。
|
||||
|
||||
|
||||
# 最后
|
||||
|
||||
目前看来 Flutter 3.19 并没有什么大更新,属于季度正常迭代,主要是问题修复和性能高优化的版本,还是老规矩,坐等 3.19.6 版本。
|
||||
|
||||
最后,新年快乐~准备开工咯。
|
|
@ -0,0 +1,62 @@
|
|||
# 2024 Impeller:快速了解 Flutter 的渲染引擎的优势
|
||||
|
||||
> 参考原文 :https://tomicriedel.medium.com/understanding-impeller-a-deep-dive-into-flutters-rendering-engine-ba96db0c9614
|
||||
|
||||
最近,在 [Flutter 2024 路线规划](https://juejin.cn/post/7335067315452428297)里明确提出了,**今年 Flutter Team 将计划删除 iOS 上的 Skia 的支持,从而完成 iOS 到 Impeller 的完全迁移**,Android 上的 Impeller 今年预计将完成 Vulkan 和 OpenGL 支持,目前[ Flutter 发布的 3.19 ](https://juejin.cn/post/7334503381200781363)上 Android 就完成了 OpenGL 的预览支持。
|
||||
|
||||
> 所以现在我们有必要了解下 Impeller 是什么,它和 Skia 的区别在哪里。
|
||||
|
||||
Impeller 作为 Flutter 新一代的渲染引擎,**它的核心就是负责绘制应用的界面,包括布局计算、纹理映射和动画处理等等**,它会将代码转换为像素、颜色、形状,所以 Impeller 是会直接影响到应用的性能和渲染效果,这也是很多早期 Flutter 开发者从 Skia 升级到 Impeller 经常遇到的痛点,例如:
|
||||
|
||||
- 字体加载异常,字形和排版与之前不对,如 [#142974 ](https://github.com/flutter/flutter/issues/142974)、[#140475](https://github.com/flutter/flutter/issues/140475) 、[#138670 ](https://github.com/flutter/flutter/issues/138670)、[#138386](https://github.com/flutter/flutter/issues/138386)
|
||||
- 线条渲染或裁剪不一致,如 [#141563 ](https://github.com/flutter/flutter/issues/141563)、 [#137956](https://github.com/flutter/flutter/issues/137956)
|
||||
- 某些纹理合成闪烁/变形,如 [#143719](https://github.com/flutter/flutter/issues/143719) 、[#142753](https://github.com/flutter/flutter/issues/142753) 、[#142549](https://github.com/flutter/flutter/issues/142549) 、[#141850](https://github.com/flutter/flutter/issues/141850)
|
||||
- ····
|
||||
|
||||
可以看到,Impeller 在替换 Skia 这条路上有许多需要处理的 bug ,甚至很多问题在 Skia 上修复过了,在 Impeller 上还要重新修复,那为什么 Flutter 团队还要将 Skia 切换到 Impeller 呢?是 Skia 不够优秀吗?
|
||||
|
||||
首先 Skia 肯定是一个优秀的通用 2D 图形库,例如 Google Chrome 、Android、Firefox 等设备都是用了 Skia ,但是也因为它的「通用性」,所以它不属于 Flutter 的形状,它无法专门针对 Flutter 的要求去进行优化调整,例如 Skia 附带的许多功能超出了 Flutter 的要求,其中一些可能会导致不必要的开销并导致渲染时间变慢,而目前来看,**Skia 的通用性给 Flutter 带来了性能瓶颈**。
|
||||
|
||||
而 Impeller 是专门为 Flutter 而生,它主要核心就是优化 Flutter 架构的渲染过程,它渲染方法在 Flutter 上可以比 Skia 能更有效地利用 GPU ,**让设备的硬件以更少的工作量来渲染动画和复杂的 UI 元素,从而提高渲染速度**。
|
||||
|
||||
另外 **Impeller 还会采用 tessellation 和着色器编译来分解和提前优化图形渲染**,这样 Impeller 就可以减少设备上的硬件工作负载,从而实现更快的帧速率和更流畅的动画。
|
||||
|
||||
> 着色器可以在 GPU 上运行从之控制图形渲染,与 Skia 不同的是,Flutter 上 Skia 会动态编译着色器,这可能导致渲染延迟,而在 Impeller 会提前编译大部分着色器,这种预编译可以显着降低动画过程中出现卡顿,因为 GPU 不必在渲染帧时暂停来编译着色器。
|
||||
|
||||
**Impeller 还采用了新的分层架构来简化渲染过程**,架构允许 Engine 的每个组件以最大效率执行其特定任务,从而减少将 Flutter Widget 的转换为屏幕上的像素所需的步骤。
|
||||
|
||||
所以,Impeller 的设计采用了分层结构,每一层都建立在下一层的基础上执行专门的功能,这种设计使引擎更加高效,并且更易于维护和更新,因为它分离了不同的关注点。
|
||||
|
||||
首先,**Impeller 架构的顶层是 Aiks**,这一层主要作为绘图操作的高级接口,它接受来自 Flutter 框架的命令,例如绘制路径或图像,并将这些命令转换为一组更精细的 “Entities”,然后转给下一层。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20240221_Impeller/image1.png)
|
||||
|
||||
Aiks 的下一层下是 Entities Framework,它是 Impeller 架构的核心组件,当 Aiks 处理完命令时生成 Entities 后,**每一个 Entity 其实就是渲染指令的独立单元,其中包含绘制特定元素的所有必要信息**。
|
||||
|
||||
每个 Entity 都带有 transformation 矩阵(编码位置、旋转、缩放)等属性,以及保存渲染所需 GPU 指令的content object ,这些内容对象非常灵活,可以管理许多 UI 效果,如纯色、图像、渐变和文本,当时现在 Entities 还不能直接作用于 GPU, 因为 Engine 还需要和 Metal 或者 Vulkan 进行通信。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20240221_Impeller/image2.png)
|
||||
|
||||
所以 HAL(Hardware Abstraction Layer) 出现了,它构成了 Impeller 架构的基础,它为底层图形硬件提供了统一的接口,抽象了不同图形 API 的细节,该层确保了 Impeller 的跨平台能力,它将高级渲染命令转换为低级 GPU 指令,充当 Impeller 渲染逻辑和设备图形硬件之间的桥梁。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20240221_Impeller/image3.png)
|
||||
|
||||
大家都知道,**渲染引擎中最耗时的任务就是渲染管道和着色器编译**,渲染管线是 GPU 执行渲染图形的一系列步骤,这些是由 HAL 生成处理,所以在性能上 HAL 也当任和很重要的角色。
|
||||
|
||||
> 对渲染管道感兴趣的也可以简单了解下:https://juejin.cn/post/7282245376424345656
|
||||
|
||||
另外就像前面说的, Impeller 提前预编译大部分着色器,这种策略可以显着减少渲染延迟并消除与动态着色器编译相关的卡顿,而**这个预编译发生在 Flutter 应用的构建过程中**,确保着色器在应用启动后立即可用
|
||||
|
||||
> 并且一般情况下,预编译的着色器会导致应用启动时间变成和 App 大小剧增,但是因为 Impeller 专为 Flutter 而生,所以 Impeller 的着色器预编译可以依赖一组比 Skia 更简单的着色器,从而保持应用的启动时间较短且整体大小不会剧增的效果。
|
||||
|
||||
最后,如果你使用 Flutter 有一些时间,那么你应该知道,抗锯齿(Anti-Aliasing)和裁剪(Clip)是一种比较昂贵的操作,而这些在 Impeller 里也得到了底层优化。
|
||||
|
||||
在 Impeller 里抗锯齿是通过多重采样抗锯齿 (MSAA) 来解决, MSAA 的工作原理是在像素内的不同位置对每个像素进行多次采样,然后对这些样本进行平均以确定最终颜色,最后将对象的边缘与其背景平滑地融合,减少其锯齿状外观。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20240221_Impeller/image4.gif)
|
||||
|
||||
对于裁剪操作,Impeller 利用模板缓冲区 stencil buffer(GPU 的一个组件)来管理剪切过程,当 Impeller 渲染 UI 时,它会先让 GPU 使用模板缓冲区,该缓冲区主要充当过滤器,根据 clipping 蒙版确定应改显示哪些像素,最后通过优化模板缓冲区,Impeller 可确保快速执行剪切操作。
|
||||
|
||||
所以,现在你理解 Impeller 的优势了吗?
|
||||
|
||||
虽然从 Skia 到 Impeller 的切换还有需要细节需要优化,但是 2024 年 Impeller 应该毫不意外会成为 Flutter 在 Android 和 iOS 的默认引擎,而 Skia 也大概率会在 2024 和我们说再见,那么,你准备好了迎接 Impeller 的洗礼了吗?
|
|
@ -0,0 +1,447 @@
|
|||
# 2024 Flutter 重大更新,Dart 宏(Macros)编程开始支持,JSON 序列化有救
|
||||
|
||||
说起宏编程可能大家并不陌生,但是这对于 Flutter 和 Dart 开发者来说它一直是一个「遗憾」,这个「遗憾」体现在编辑过程的代码修改支持上,其中最典型的莫过于 Dart 的 JSON 序列化。
|
||||
|
||||
举个例子,目前 Dart 语言的 JSON 序列化高度依赖 `build_runner` 去生成 Dart 代码,例如在实际使用中我们需要:
|
||||
|
||||
- 依赖 `json_serializable` ,通过注解声明一个 `Event` 对象
|
||||
- 运行 `flutter packages pub run build_runner build ` 生成文件
|
||||
- 得到 `Event.g.dart` 文件,在项目中使用它去实现 JSON 的序列化和反序列化
|
||||
|
||||
| ![](http://img.cdn.guoshuyu.cn/20240202_macros/image1.png) | ![](http://img.cdn.guoshuyu.cn/20240202_macros/image2.png) |
|
||||
| ---------------------------------------------------------- | ---------------------------------------------------------- |
|
||||
|
||||
这里最大的问题在于,我们需要通过命令行去生成一个项目文件,并且这个文件我们还可以随意手动修改,从开发角度来说,这并不优雅也不方便。
|
||||
|
||||
而宏声明是用户定义的 Dart 类,它可以实现一个或多个新的内置宏接口,**Dart 中的宏是用正常的命令式 Dart 代码来开发,不存在单独的“宏语言”**。
|
||||
|
||||
> 大多数宏并不是简单地从头开始生成新代码,而是根据程序的现有属性去添加代码,例如向 Class 添加 JSON 序列化的宏,可能会查看 Class 声明的字段,并从中合成一个 `toJson()` ,将这些字段序列化为 JSON 对象。
|
||||
|
||||
我们首先看一段官方的 Demo , 如下代码所示,可以看到 :
|
||||
|
||||
- `MyState` 添加了一个自定义的 `@AutoDispose()` 注解,这是一个开发者自己实现的宏声明,并且继承了 `State` 对象,带有 `dispose` 方法。
|
||||
- 在 `MyState` 里有多个 `a `、`a2` 、`b` 和 `c` 三个对象,其中 `a `、`a2` 、`b` 都实现了 `Disposable` 接口,都有 `dispose` 方法
|
||||
- 虽然 `a `、`a2` 、`b` 和 `MyState` 的 `dispose();` 方法来自不同基类实现,但是基于 `@AutoDispose()` 的实现,在代码调用 ` state.dispose();` 时, `a `、`a2` 、`b` 变量的 `dispose` 方法也会被同步调用
|
||||
|
||||
```dart
|
||||
import 'package:macro_proposal/auto_dispose.dart';
|
||||
|
||||
void main() {
|
||||
var state = MyState(a: ADisposable(), b: BDisposable(), c: 'hello world');
|
||||
state.dispose();
|
||||
}
|
||||
|
||||
@AutoDispose()
|
||||
class MyState extends State {
|
||||
final ADisposable a;
|
||||
final ADisposable? a2;
|
||||
final BDisposable b;
|
||||
final String c;
|
||||
|
||||
MyState({required this.a, this.a2, required this.b, required this.c});
|
||||
|
||||
@override
|
||||
String toString() => 'MyState!';
|
||||
}
|
||||
|
||||
class State {
|
||||
void dispose() {
|
||||
print('disposing of $this');
|
||||
}
|
||||
}
|
||||
|
||||
class ADisposable implements Disposable {
|
||||
void dispose() {
|
||||
print('disposing of ADisposable');
|
||||
}
|
||||
}
|
||||
|
||||
class BDisposable implements Disposable {
|
||||
void dispose() {
|
||||
print('disposing of BDisposable');
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
如下图所示,可以看到,尽管 `MyState` 没用主动调用 `a `、`a2` 、`b` 变量的 `dispose` 方法,并且它们和 `MyState` 的 `dispose` 也来自不同基类,但是最终执行所有 `dispose` 方法都被成功调用,这就是`@AutoDispose()` 的宏声明实现在编译时对代码进行了调整。
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20240202_macros/image3.png)
|
||||
|
||||
如下图所示是 `@AutoDispose()` 的宏编程实现,**其中 `macro` 就是一个标志性的宏关键字**,剩下的代码可以看到基本就是 dart 脚本的实现, `macro` 里主要是实现 `ClassDeclarationsMacro` 和`buildDeclarationsForClass`方法,如下代码可以很直观看到关于 `super.dispose();` 和 `disposeCalls` 的相关实现。
|
||||
|
||||
```dart
|
||||
import 'package:_fe_analyzer_shared/src/macros/api.dart';
|
||||
|
||||
// Interface for disposable things.
|
||||
abstract class Disposable {
|
||||
void dispose();
|
||||
}
|
||||
|
||||
macro class AutoDispose implements ClassDeclarationsMacro, ClassDefinitionMacro {
|
||||
const AutoDispose();
|
||||
|
||||
@override
|
||||
void buildDeclarationsForClass(
|
||||
ClassDeclaration clazz, MemberDeclarationBuilder builder) async {
|
||||
var methods = await builder.methodsOf(clazz);
|
||||
if (methods.any((d) => d.identifier.name == 'dispose')) {
|
||||
// Don't need to add the dispose method, it already exists.
|
||||
return;
|
||||
}
|
||||
|
||||
builder.declareInType(DeclarationCode.fromParts([
|
||||
// TODO: Remove external once the CFE supports it.
|
||||
'external void dispose();',
|
||||
]));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> buildDefinitionForClass(
|
||||
ClassDeclaration clazz, TypeDefinitionBuilder builder) async {
|
||||
var disposableIdentifier =
|
||||
// ignore: deprecated_member_use
|
||||
await builder.resolveIdentifier(
|
||||
Uri.parse('package:macro_proposal/auto_dispose.dart'),
|
||||
'Disposable');
|
||||
var disposableType = await builder
|
||||
.resolve(NamedTypeAnnotationCode(name: disposableIdentifier));
|
||||
|
||||
var disposeCalls = <Code>[];
|
||||
var fields = await builder.fieldsOf(clazz);
|
||||
for (var field in fields) {
|
||||
var type = await builder.resolve(field.type.code);
|
||||
if (!await type.isSubtypeOf(disposableType)) continue;
|
||||
disposeCalls.add(RawCode.fromParts([
|
||||
'\n',
|
||||
field.identifier,
|
||||
if (field.type.isNullable) '?',
|
||||
'.dispose();',
|
||||
]));
|
||||
}
|
||||
|
||||
// Augment the dispose method by injecting all the new dispose calls after
|
||||
// either a call to `augmented()` or `super.dispose()`, depending on if
|
||||
// there already is an existing body to call.
|
||||
//
|
||||
// If there was an existing body, it is responsible for calling
|
||||
// `super.dispose()`.
|
||||
var disposeMethod = (await builder.methodsOf(clazz))
|
||||
.firstWhere((method) => method.identifier.name == 'dispose');
|
||||
var disposeBuilder = await builder.buildMethod(disposeMethod.identifier);
|
||||
disposeBuilder.augment(FunctionBodyCode.fromParts([
|
||||
'{\n',
|
||||
if (disposeMethod.hasExternal || !disposeMethod.hasBody)
|
||||
'super.dispose();'
|
||||
else
|
||||
'augmented();',
|
||||
...disposeCalls,
|
||||
'}',
|
||||
]));
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
到这里大家应该可以直观感受到宏编程的魅力,上述 Demo 来自 [dart-language](https://github.com/dart-lang/language/blob/main/working/macros/example) 的 [macros/example/auto_dispose_main](https://github.com/dart-lang/language/blob/main/working/macros/example/) ,其中 `bin/` 目录下的代码是运行的脚本示例,`lib/` 目录下的代码是宏编程实现的示例:
|
||||
|
||||
> https://github.com/dart-lang/language/tree/main/working/macros/example
|
||||
|
||||
当然,因为现在是实验性阶段,API 和稳定性还有待商榷,所以想运行这些 Demo 还需要一些额外的处理,**比如版本强关联**,例如上述的 `auto_dispose_main` 例子:
|
||||
|
||||
- 需要 dart sdk [3.4.0-97.0.dev](https://storage.googleapis.com/dart-archive/channels/dev/release/3.4.0-97.0.dev/sdk/dartsdk-macos-arm64-release.zip) ,目前你可以通过 master 分支下载这个 dark-sdk https://storage.googleapis.com/dart-archive/channels/main/raw/latest/sdk/dartsdk-macos-arm64-release.zip
|
||||
|
||||
- 将 sdk 配置到环境变量,或者进入到 dart sdk 的 bin 目录执行 ./dart --version 检查版本
|
||||
|
||||
- 进入上诉的 example 下执行 dart pub get,过程可能会有点长
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20240202_macros/image4.png)
|
||||
|
||||
- 最后,执行 `dart --enable-experiment=macros bin/auto_dispose_main.dart `,**记得这个 dart 是你指定版本的 dart** 。
|
||||
|
||||
另外,还有一个第三方例子是来自 [millsteed](https://github.com/millsteed) 的 **[macros ](https://github.com/millsteed/macros)**,这是一个简单的 JSON 序列化实现 Demo ,并且可以直接不用额外下载 dark-sdk,通过某个 flutter 内置 dart-sdk 版本就可以满足条件:`3.19.0-12.0.pre` :
|
||||
|
||||
> 在本地 Flutter 目录下,切换到 `git checkout 3.19.0-12.0.pre` ,然后执行 flutter doctor 初始化 dark sdk 即可。
|
||||
|
||||
代码的实现很简单,首先看 bin 下的示例,通过 `@Model() ` 将 `GetUsersResponse` 和 `User` 声明为 JSON 对象,然后在运行时,宏编程会自动添加 `fromJson` 和 `toJson` 方式。
|
||||
|
||||
```dart
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:macros/model.dart';
|
||||
|
||||
@Model()
|
||||
class User {
|
||||
User({
|
||||
required this.username,
|
||||
required this.password,
|
||||
});
|
||||
|
||||
final String username;
|
||||
final String password;
|
||||
}
|
||||
|
||||
@Model()
|
||||
class GetUsersResponse {
|
||||
GetUsersResponse({
|
||||
required this.users,
|
||||
required this.pageNumber,
|
||||
required this.pageSize,
|
||||
});
|
||||
|
||||
final List<User> users;
|
||||
final int pageNumber;
|
||||
final int pageSize;
|
||||
}
|
||||
|
||||
void main() {
|
||||
const body = '''
|
||||
{
|
||||
"users": [
|
||||
{
|
||||
"username": "ramon",
|
||||
"password": "12345678"
|
||||
}
|
||||
],
|
||||
"pageNumber": 1,
|
||||
"pageSize": 30
|
||||
}
|
||||
''';
|
||||
final json = jsonDecode(body) as Map<String, dynamic>;
|
||||
final response = GetUsersResponse.fromJson(json);
|
||||
final ramon = response.users.first;
|
||||
final millsteed = ramon.copyWith(username: 'millsteed', password: '87654321');
|
||||
final newResponse = response.copyWith(users: [...response.users, millsteed]);
|
||||
print(const JsonEncoder.withIndent(' ').convert(newResponse));
|
||||
}
|
||||
```
|
||||
|
||||
而 `Model` 的宏实现就相对复杂一些,但是实际上就是将类似 `freezed`/ `json_serializable` 是实现调整到宏实现了,而最终效果就是,开发者使用起来更加优雅了。
|
||||
|
||||
```dart
|
||||
// ignore_for_file: depend_on_referenced_packages, implementation_imports
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:_fe_analyzer_shared/src/macros/api.dart';
|
||||
|
||||
macro class Model implements ClassDeclarationsMacro {
|
||||
const Model();
|
||||
|
||||
static const _baseTypes = ['bool', 'double', 'int', 'num', 'String'];
|
||||
static const _collectionTypes = ['List'];
|
||||
|
||||
@override
|
||||
Future<void> buildDeclarationsForClass(
|
||||
ClassDeclaration classDeclaration,
|
||||
MemberDeclarationBuilder builder,
|
||||
) async {
|
||||
final className = classDeclaration.identifier.name;
|
||||
|
||||
final fields = await builder.fieldsOf(classDeclaration);
|
||||
|
||||
final fieldNames = <String>[];
|
||||
final fieldTypes = <String, String>{};
|
||||
final fieldGenerics = <String, List<String>>{};
|
||||
|
||||
for (final field in fields) {
|
||||
final fieldName = field.identifier.name;
|
||||
fieldNames.add(fieldName);
|
||||
|
||||
final fieldType = (field.type.code as NamedTypeAnnotationCode).name.name;
|
||||
fieldTypes[fieldName] = fieldType;
|
||||
|
||||
if (_collectionTypes.contains(fieldType)) {
|
||||
final generics = (field.type.code as NamedTypeAnnotationCode)
|
||||
.typeArguments
|
||||
.map((e) => (e as NamedTypeAnnotationCode).name.name)
|
||||
.toList();
|
||||
fieldGenerics[fieldName] = generics;
|
||||
}
|
||||
}
|
||||
|
||||
final fieldTypesWithGenerics = fieldTypes.map(
|
||||
(name, type) {
|
||||
final generics = fieldGenerics[name];
|
||||
return MapEntry(
|
||||
name,
|
||||
generics == null ? type : '$type<${generics.join(', ')}>',
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
_buildFromJson(builder, className, fieldNames, fieldTypes, fieldGenerics);
|
||||
_buildToJson(builder, fieldNames, fieldTypes);
|
||||
_buildCopyWith(builder, className, fieldNames, fieldTypesWithGenerics);
|
||||
_buildToString(builder, className, fieldNames);
|
||||
_buildEquals(builder, className, fieldNames);
|
||||
_buildHashCode(builder, fieldNames);
|
||||
}
|
||||
|
||||
void _buildFromJson(
|
||||
MemberDeclarationBuilder builder,
|
||||
String className,
|
||||
List<String> fieldNames,
|
||||
Map<String, String> fieldTypes,
|
||||
Map<String, List<String>> fieldGenerics,
|
||||
) {
|
||||
final code = [
|
||||
'factory $className.fromJson(Map<String, dynamic> json) {'.indent(2),
|
||||
'return $className('.indent(4),
|
||||
for (final fieldName in fieldNames) ...[
|
||||
if (_baseTypes.contains(fieldTypes[fieldName])) ...[
|
||||
"$fieldName: json['$fieldName'] as ${fieldTypes[fieldName]},"
|
||||
.indent(6),
|
||||
] else if (_collectionTypes.contains(fieldTypes[fieldName])) ...[
|
||||
"$fieldName: (json['$fieldName'] as List<dynamic>)".indent(6),
|
||||
'.whereType<Map<String, dynamic>>()'.indent(10),
|
||||
'.map(${fieldGenerics[fieldName]?.first}.fromJson)'.indent(10),
|
||||
'.toList(),'.indent(10),
|
||||
] else ...[
|
||||
'$fieldName: ${fieldTypes[fieldName]}'
|
||||
".fromJson(json['$fieldName'] "
|
||||
'as Map<String, dynamic>),'
|
||||
.indent(6),
|
||||
],
|
||||
],
|
||||
');'.indent(4),
|
||||
'}'.indent(2),
|
||||
].join('\n');
|
||||
builder.declareInType(DeclarationCode.fromString(code));
|
||||
}
|
||||
|
||||
void _buildToJson(
|
||||
MemberDeclarationBuilder builder,
|
||||
List<String> fieldNames,
|
||||
Map<String, String> fieldTypes,
|
||||
) {
|
||||
final code = [
|
||||
'Map<String, dynamic> toJson() {'.indent(2),
|
||||
'return {'.indent(4),
|
||||
for (final fieldName in fieldNames) ...[
|
||||
if (_baseTypes.contains(fieldTypes[fieldName])) ...[
|
||||
"'$fieldName': $fieldName,".indent(6),
|
||||
] else if (_collectionTypes.contains(fieldTypes[fieldName])) ...[
|
||||
"'$fieldName': $fieldName.map((e) => e.toJson()).toList(),".indent(6),
|
||||
] else ...[
|
||||
"'$fieldName': $fieldName.toJson(),".indent(6),
|
||||
],
|
||||
],
|
||||
'};'.indent(4),
|
||||
'}'.indent(2),
|
||||
].join('\n');
|
||||
builder.declareInType(DeclarationCode.fromString(code));
|
||||
}
|
||||
|
||||
void _buildCopyWith(
|
||||
MemberDeclarationBuilder builder,
|
||||
String className,
|
||||
List<String> fieldNames,
|
||||
Map<String, String> fieldTypes,
|
||||
) {
|
||||
final code = [
|
||||
'$className copyWith({'.indent(2),
|
||||
for (final fieldName in fieldNames) ...[
|
||||
'${fieldTypes[fieldName]}? $fieldName,'.indent(4),
|
||||
],
|
||||
'}) {'.indent(2),
|
||||
'return $className('.indent(4),
|
||||
for (final fieldName in fieldNames) ...[
|
||||
'$fieldName: $fieldName ?? this.$fieldName,'.indent(6),
|
||||
],
|
||||
');'.indent(4),
|
||||
'}'.indent(2),
|
||||
].join('\n');
|
||||
builder.declareInType(DeclarationCode.fromString(code));
|
||||
}
|
||||
|
||||
void _buildToString(
|
||||
MemberDeclarationBuilder builder,
|
||||
String className,
|
||||
List<String> fieldNames,
|
||||
) {
|
||||
final code = [
|
||||
'@override'.indent(2),
|
||||
'String toString() {'.indent(2),
|
||||
"return '$className('".indent(4),
|
||||
for (final fieldName in fieldNames) ...[
|
||||
if (fieldName != fieldNames.last) ...[
|
||||
"'$fieldName: \$$fieldName, '".indent(8),
|
||||
] else ...[
|
||||
"'$fieldName: \$$fieldName'".indent(8),
|
||||
],
|
||||
],
|
||||
"')';".indent(8),
|
||||
'}'.indent(2),
|
||||
].join('\n');
|
||||
builder.declareInType(DeclarationCode.fromString(code));
|
||||
}
|
||||
|
||||
void _buildEquals(
|
||||
MemberDeclarationBuilder builder,
|
||||
String className,
|
||||
List<String> fieldNames,
|
||||
) {
|
||||
final code = [
|
||||
'@override'.indent(2),
|
||||
'bool operator ==(Object other) {'.indent(2),
|
||||
'return other is $className &&'.indent(4),
|
||||
'runtimeType == other.runtimeType &&'.indent(8),
|
||||
for (final fieldName in fieldNames) ...[
|
||||
if (fieldName != fieldNames.last) ...[
|
||||
'$fieldName == other.$fieldName &&'.indent(8),
|
||||
] else ...[
|
||||
'$fieldName == other.$fieldName;'.indent(8),
|
||||
],
|
||||
],
|
||||
'}'.indent(2),
|
||||
].join('\n');
|
||||
builder.declareInType(DeclarationCode.fromString(code));
|
||||
}
|
||||
|
||||
void _buildHashCode(
|
||||
MemberDeclarationBuilder builder,
|
||||
List<String> fieldNames,
|
||||
) {
|
||||
final code = [
|
||||
'@override'.indent(2),
|
||||
'int get hashCode {'.indent(2),
|
||||
'return Object.hash('.indent(4),
|
||||
'runtimeType,'.indent(6),
|
||||
for (final fieldName in fieldNames) ...[
|
||||
'$fieldName,'.indent(6),
|
||||
],
|
||||
');'.indent(4),
|
||||
'}'.indent(2),
|
||||
].join('\n');
|
||||
builder.declareInType(DeclarationCode.fromString(code));
|
||||
}
|
||||
}
|
||||
|
||||
extension on String {
|
||||
String indent(int length) {
|
||||
final space = StringBuffer();
|
||||
for (var i = 0; i < length; i++) {
|
||||
space.write(' ');
|
||||
}
|
||||
return '$space$this';
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
![](http://img.cdn.guoshuyu.cn/20240202_macros/image5.png)
|
||||
|
||||
目前宏还处于试验性质的阶段,所以 API 还在调整,这也是为什么上面的例子需要指定 dart 版本的原因,另外宏目前规划里还有一些要求,例如
|
||||
|
||||
- 所有宏构造函数都必须标记为 `const`
|
||||
- 所有宏必须至少实现其中一个 `Macro` 接口
|
||||
- 宏不能是抽象对象
|
||||
- 宏 class 不能由其他宏生成
|
||||
- 宏 class 不能包含泛型类型参数
|
||||
- 每个宏接口都需要声明宏类必须实现的方法,例如,在声明阶段应用的 `ClassDeclarationsMacro `及其`buildDeclarationsForClass `方法。
|
||||
|
||||
未来规划里,宏 API 可能会作为 Pub 包提供,通过库 `dart:_macros `来提供支持 ,具体还要等正式发布时 dart 团队的决策。
|
||||
|
||||
总的来说,这对于 dart 和 flutter 是一个重大的厉害消息,虽然宏编程并不是什么新鲜概念,该是 dart 终于可以优雅地实现 JSON 序列化,并且还是用 dart 来实现,这对于 flutter 开发者来说,无疑是最好的新年礼物。
|
||||
|
||||
**所以,新年快乐~我们节后再见~**
|
|
@ -95,6 +95,7 @@
|
|||
- [Flutter 3.7 正式发布,快来看看有什么新功能吧](Flutter-370.md)
|
||||
- [ Flutter 3.10 发布,快来看看有什么更新吧](Flutter-310.md)
|
||||
- [Flutter 3.16 发布,快来看有什么更新吧](Flutter-316.md)
|
||||
- [Flutter 3.19 发布,快来看有什么更新吧](Flutter-319.md)
|
||||
- **Dart**
|
||||
- [Dart 2.12 发布,稳定空安全声明和FFI版本,Dart 未来的计划](Dart-212.md)
|
||||
- [Dart 2.14 发布,新增语言特性和共享标准 lint](Dart-214.md)
|
||||
|
@ -105,6 +106,7 @@
|
|||
- [Flutter - Dart 3α 新特性 Record 和 Patterns 的提前预览讲解](Dart-300a.md)
|
||||
- [Dart 3 发布,快来看看有什么更新吧](Dart-300.md)
|
||||
- [Dart 3.2 更新,Flutter Web 的未来越来越明朗](Dart-320.md)
|
||||
- [Dart 3.3 发布:扩展类型、JavaScript Interop 等](Dart-303.md)
|
||||
|
||||
* [番外](FWREADME.md)
|
||||
|
||||
|
@ -199,6 +201,10 @@
|
|||
* [Flutter 与 Dart 的市场应用](Flutter-WH.md)
|
||||
* [Flutter 小技巧之不一样的思路实现炫酷 3D 翻页折叠动画](Flutter-GLSL.md)
|
||||
* [Flutter 小技巧之 3.16 升级最坑 M3 默认适配技巧](Flutter-M3D.md)
|
||||
* [2024 Flutter 重大更新,Dart 宏(Macros)编程开始支持,JSON 序列化有救](Flutter-macros.md)
|
||||
* [Flutter 小技巧之升级适配 Xcode15 ](Flutter-X15.md)
|
||||
* [Flutter 2024 路线规划,更多可期待的功能正在路上](Flutter-2024.md)
|
||||
* [2024 Impeller:快速了解 Flutter 的渲染引擎的优势](Flutter-Impeller.md)
|
||||
|
||||
[Flutter 工程化选择](GCH.md)
|
||||
|
||||
|
|
10
SUMMARY.md
10
SUMMARY.md
|
@ -62,6 +62,7 @@
|
|||
- [Flutter 3.7 正式发布,快来看看有什么新功能吧](Flutter-370.md)
|
||||
- [ Flutter 3.10 发布,快来看看有什么更新吧](Flutter-310.md)
|
||||
- [Flutter 3.16 发布,快来看有什么更新吧](Flutter-316.md)
|
||||
- [Flutter 3.19 发布,快来看有什么更新吧](Flutter-319.md)
|
||||
- **Dart**
|
||||
- [Dart 2.12 发布,稳定空安全声明和FFI版本,Dart 未来的计划](Dart-212.md)
|
||||
- [Dart 2.14 发布,新增语言特性和共享标准 lint](Dart-214.md)
|
||||
|
@ -72,6 +73,7 @@
|
|||
- [Flutter - Dart 3α 新特性 Record 和 Patterns 的提前预览讲解](Dart-300a.md)
|
||||
- [Dart 3 发布,快来看看有什么更新吧](Dart-300.md)
|
||||
- [Dart 3.2 更新,Flutter Web 的未来越来越明朗](Dart-320.md)
|
||||
- [Dart 3.3 发布:扩展类型、JavaScript Interop 等](Dart-303.md)
|
||||
|
||||
* [番外](FWREADME.md)
|
||||
|
||||
|
@ -253,6 +255,14 @@
|
|||
|
||||
* [Flutter 小技巧之 3.16 升级最坑 M3 默认适配技巧](Flutter-M3D.md)
|
||||
|
||||
* [2024 Flutter 重大更新,Dart 宏(Macros)编程开始支持,JSON 序列化有救](Flutter-macros.md)
|
||||
|
||||
* [Flutter 小技巧之升级适配 Xcode15 ](Flutter-X15.md)
|
||||
|
||||
* [Flutter 2024 路线规划,更多可期待的功能正在路上](Flutter-2024.md)
|
||||
|
||||
* [2024 Impeller:快速了解 Flutter 的渲染引擎的优势](Flutter-Impeller.md)
|
||||
|
||||
* [Flutter 工程化选择](GCH.md)
|
||||
* [Flutter 工程化框架选择——搞定 Flutter 动画](Z1.md)
|
||||
* [Flutter 工程化框架选择 — 搞定 UI 生产力](Z3.md)
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
- [Flutter 3.7 正式发布,快来看看有什么新功能吧](Flutter-370.md)
|
||||
- [ Flutter 3.10 发布,快来看看有什么更新吧](Flutter-310.md)
|
||||
- [Flutter 3.16 发布,快来看有什么更新吧](Flutter-316.md)
|
||||
- [Flutter 3.19 发布,快来看有什么更新吧](Flutter-319.md)
|
||||
|
||||
|
||||
|
||||
|
@ -31,4 +32,5 @@
|
|||
- [Flutter - Dart 3α 新特性 Record 和 Patterns 的提前预览讲解](Dart-300a.md)
|
||||
- [Dart 3 发布,快来看看有什么更新吧](Dart-300.md)
|
||||
- [Dart 3.2 更新,Flutter Web 的未来越来越明朗](Dart-320.md)
|
||||
- [Dart 3.3 发布:扩展类型、JavaScript Interop 等](Dart-303.md)
|
||||
|
||||
|
|
Loading…
Reference in New Issue