迁移指南
了解在升级 Rive Flutter 运行时主要版本时如何迁移你的 Flutter 应用,包括破坏性变更和新功能。
| 版本 | 变更内容 |
|---|---|
| 0.15.0 | 创建控制器时即完成数据绑定;Rive 渲染器在原生平台使用延迟渲染(deferred rendering) |
| 0.14.0 | Rive 用 C++ 运行时替换 Dart 运行时,组件、控制器和文件加载 API 随之变化 |
版本 0.15.0
此版本有两处需要迁移:
- 创建
RiveWidgetController时即完成数据绑定,单独的dataBind调用已移除。 - Rive 渲染器(
Factory.rive)在原生平台使用延迟渲染。这只影响高级集成。
0.15.0是开发版本(dev release,当前为0.15.0-dev.2)。请按下方 API 迁移,但在稳定版发布前仍可能有变化。
要求
最低 Flutter 版本为 3.32.0(Dart 3.8.0)。0.14.x 曾误将要求声明为 3.28.0,因此更旧的 Flutter 版本能解析到该包,随后编译失败。参见 rive-flutter issue #643。
数据绑定
构造时即绑定
RiveWidgetController 在构造时完成绑定。除非你通过新的 main 和 globals 参数另行指定,画板的主 View Model 以及文件中每一个全局 View Model 都会获得默认实例:
// Default instances for the main and every global View Model
final controller = RiveWidgetController(file);
// Or choose what gets bound
final controller = RiveWidgetController(
file,
main: DataBind.byName('My Instance'),
globals: {'Theme': DataBind.byInstance(themeInstance)},
);
对不含数据绑定的内容,此次绑定是空操作(no-op)。
行为变化:以前以未绑定状态渲染的、带有 View Model 的内容,现在会渲染其在编辑器中编写的默认值,因为构造时会自动绑定。
通过 controller.viewModelInstance 和 controller.globalViewModelInstance(name) 读回已绑定实例。两者都是实时读取,每次返回同一个对象。
之后若要重新绑定,调用 controller.bind(main: ..., globals: {...})。每次调用都是对当前绑定的增量更新。未写出的槽位会保留已有实例;无论你设置多少个槽位,重新绑定只执行一次。若 bind 在控制器首次 advance 之前运行,debug 构建会警告一次,因为构造时的绑定会在任何内容渲染前被丢弃。请改为把配置传给构造函数。
已移除的 API
| 已移除 | 替换为 |
|---|---|
controller.dataBind(x) | RiveWidgetController(file, main: x),通过 controller.viewModelInstance 读回 |
RiveWidgetBuilder(dataBind: DataBind.auto()) | 删除该参数。构造时会自动绑定默认值 |
RiveWidgetBuilder(dataBind: DataBind.byInstance(x)) | controller: (file) => RiveWidgetController(file, main: DataBind.byInstance(x)) |
state.viewModelInstance(在 RiveLoaded 上) | state.controller.viewModelInstance |
新 API
final controller = RiveWidgetController(
file,
main: DataBind.byName('My Instance'),
);
final viewModelInstance = controller.viewModelInstance;
旧 API
final controller = RiveWidgetController(file);
final viewModelInstance = controller.dataBind(
DataBind.byName('My Instance'),
);
旧版桥接
若要在迁移期间让现有调用点继续编译:
import 'package:rive/legacy.dart';
这会以已弃用的扩展形式恢复 controller.dataBind 和 RiveLoaded.viewModelInstance。旧的所有权约定一并回来,因此你需要 dispose dataBind 的返回值。该库已冻结,将在未来版本中移除,请把这次 import 当作迁移 TODO。
实例所有权
控制器拥有它为你解析的每一个实例(DataBind.auto、byName、byIndex 和 empty),并在自身 dispose 时一并释放它们。请去掉对这些实例的手动 dispose。
你自行创建并通过 DataBind.byInstance 传入的实例,运行时永远不会 dispose。RiveWidgetBuilder 也不再 dispose 它们,因此调用方拥有的实例现在会在组件销毁后继续存活。若你曾让某个实例在其控制器之后继续使用,请自己创建它并通过 instance 绑定:
final vm = file.viewModelByName('My View Model')!;
final instance = vm.createInstanceByName('My Instance')!;
final controller = RiveWidgetController(
file,
main: DataBind.byInstance(instance),
);
读回结果不能再次绑定
controller.viewModelInstance 和 controller.globalViewModelInstance(name) 返回普通的 ViewModelInstance 视图。可以照常读取、写入和监听,但不能把它传给 DataBind.byInstance。后者只接受 BindableViewModelInstance,也就是 ViewModel.createInstance 及其同类方法的返回值。传入读回结果是编译错误;强制转换会在运行时抛出。若要在多个槽位或控制器之间共享状态,请创建你自己的实例。
延迟渲染
Rive 渲染器(Factory.rive)现在在原生平台使用延迟渲染,并且这是唯一的原生渲染模式。每一帧都在 UI 线程上录制为紧凑的命令流,再在专用渲染线程上回放,因此 GPU 工作不再阻塞 UI 线程。用 Factory.rive 创建的资源(path、paint、image、text)是轻量录制代理,在回放时才真正解析。
对典型的
RiveWidget、RivePanel以及基于文件的用法,渲染行为和输出没有变化。以下条目只影响高级集成。Factory.flutter不受影响。
线程
所有 Rive 调用都留在调用方(UI)线程。命令流每帧只有一个写入者,因此不支持从其他线程 advance 或绘制。这样做会损坏命令流,控制台会看到 rive replay ABORT 消息。
已移除批量 advance API
破坏性变更: Rive.batchAdvance 和 Rive.batchAdvanceAndRender 已移除,连同持有它们的 Rive 类。它们的工作线程会并发写入单写入者命令流并损坏它。请改为在调用线程上逐个 advance 并绘制每个 State Machine:
// Old API
Rive.batchAdvance(stateMachines, elapsedSeconds);
// New API
for (final stateMachine in stateMachines) {
stateMachine.advanceAndApply(elapsedSeconds);
}
对于 Rive.batchAdvanceAndRender,请像以前一样,在调用线程上 advance 对应的 State Machine 后绘制每个 Artboard。
自定义渲染纹理绘制器
RenderTexturePainter.riveFactory 指明所绘制内容使用的工厂,以便纹理可以挂上其录制会话。没有会话的纹理在原生平台上不会绘制任何内容,并会一次性打印控制台警告:texture has no deferred session, nothing will draw。
该 getter 默认为 Factory.rive,对在其上解码和创建的内容是正确的,因此大多数自定义绘制器无需改动。当内容在其他地方录制时,用已解码文件的 File.riveFactory 覆盖它;若你自行管理纹理会话,则覆盖为 null。在 Web 上,会话按文件绑定到一块纹理。因此由内容推导出的覆盖只适用于单文件内容,此时它会让纹理走按文件的会话 worker 路径。把多个文件绘制到同一块纹理的绘制器应保持默认,并通过即时回退路径渲染。
SharedTexturePainter 同样增加了 riveFactory getter(默认 null),用于 RivePanel 的绘制过程。实现 SharedTexturePainter 的类必须添加该 getter。继承它的类会获得默认实现。
每块纹理一个 Artboard 实例
在两个 RiveWidget 中显示同一个 Artboard 实例,会使后一个组件空白并打印 debug 消息。请为每个组件创建一个 Artboard 实例,例如对每个组件调用 file.artboard('MyArtboard')。
Factory.rive 与录制会话
在原生平台上,Factory.rive 解析为渲染上下文的录制会话。file.riveFactory == Factory.rive 仍然成立,直接在 Factory.rive 上创建的资源也会正确录制和渲染。
高级会话控制
package:rive_native/rive_deferred.dart 提供手动会话控制:通过 RenderTexture.useDeferredSession 附加或分离纹理会话、会话辅助方法,以及渲染线程统计。典型应用不需要它。
版本 0.14.0
这是 Rive Flutter 的一次重大更新。我们完全移除了之前用于 Rive 运行时的所有 Dart 代码,并用我们的底层 C++ 运行时取而代之。请参见 Flutter 版 Rive Native 页面了解更多详情。
这导致了底层 API 的许多变更,之前可通过 Dart 访问的大部分代码库现在通过 FFI 在 C++ 中实现。
0.14.0 的新变化
此版本的 Rive Flutter 增加了对以下内容的支持:
- Rive 渲染器
- 数据绑定
- 布局
- 滚动
- N 切片
- 矢量羽化
- 所有其他之前未在旧版 Rive Flutter 中实现的 Rive 新增功能
- 包含 Rive C++ 运行时的最新修复和改进
- 添加预构建库,并支持手动构建。更多信息请参见 rive_native 包
- 移除
rive_common包,替换为rive_native
现在 Rive Flutter 使用了核心 Rive C++ 运行时,你可以期待新的 Rive 功能更快地在 Rive Flutter 中得到支持。
你所有的 Rive 图形仍然会像以前一样显示和运行。
要求
Dart 和 Flutter 版本
此版本将版本要求提升至:
sdk: ">=3.5.0 <4.0.0"
flutter: ">=3.3.0"
必需的设置
重要提示: 你必须在应用启动时或使用 Rive 之前调用 RiveNative.init。例如,在 main.dart 中:
import 'package:rive/rive.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await RiveNative.init(); // 在使用 Rive 之前调用 init
runApp(const MyApp());
}
迁移指南
快速迁移检查清单
- ✅ 更新
pubspec.yaml依赖为0.14.0或更高版本dependencies:
rive: ^0.15.0-dev.1 - ✅ 在
main()函数中添加RiveNative.init(),或在使用 Rive 之前调用。 - ✅ 将
Rive和RiveAnimation组件替换为RiveWidget或RiveWidgetBuilder - ✅ 更新控制器以使用新的 API,参见
RiveWidgetController - ✅ 审查并更新任何自定义资源加载代码
- ✅ 测试你的图形和交互
已移除的类
以下类已完全移除:
Rive和RiveAnimation组件 → 使用RiveWidget和RiveWidgetBuilderRiveAnimationController及其子类 → 使用RiveWidgetController、SingleAnimationPainter和StateMachinePainterOneShotAnimation和SimpleAnimation→ 使用SingleAnimationPainter播放单个动画StateMachineController→ 改用StateMachine(可通过RiveWidgetController.stateMachine访问)RiveEvent→ 替换为EventSMITrigger→ 替换为TriggerInputSMIBool→ 替换为BooleanInputSMINumber→ 替换为NumberInputFileAssetLoader→ 替换为创建File时的可选回调
加载 Rive 文件
RiveFile 已移除,替换为 File。重要变更:
新 API
final file = await File.decode(bytes, factory: Factory.rive);
final artboard = file.defaultArtboard();
final artboard = file.artboard('MyArtboard');
旧 API
final file = await RiveFile.import(bytes);
final artboard = file.mainArtboard;
final artboard = file.artboardByName('MyArtboard');
提供的 Factory 决定了将使用的渲染器。使用 Factory.rive 使用 Rive 渲染器,或使用 Factory.flutter 使用内置的 Flutter 渲染器(Skia 或 Impeller)。
⚠️ 警告:矢量羽化仅适用于 Rive 渲染器。
关键变更:
- 创建 Rive File 现在需要工厂参数(
Factory.rive或Factory.flutter) - 将
RiveFile.import替换为File.decode(),返回Future<File> - 将
mainArtboard替换为defaultArtboard() - 将
artboardByName(name)替换为artboard(name) - 将
RiveFile.network替换为File.url - 将
RiveFile.file替换为File.path
组件迁移
参见更新的示例应用以获取完整的迁移指南,包括如何使用新的 RiveWidget 和 RiveWidgetBuilder API。
| 旧组件 | 新组件 | 备注 |
|---|---|---|
Rive/RiveAnimation | RiveWidget/RiveWidgetBuilder | 直接替换 |
新 API - 选项 1
class SimpleAssetAnimation extends StatefulWidget {
const SimpleAssetAnimation({Key? key}) : super(key: key);
@override
State<SimpleAssetAnimation> createState() => _SimpleAssetAnimationState();
}
class _SimpleAssetAnimationState extends State<SimpleAssetAnimation> {
late final fileLoader = FileLoader.fromAsset(
'assets/off_road_car.riv',
riveFactory: Factory.rive,
);
@override
void dispose() {
fileLoader.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Simple Animation'),
),
body: Center(
child: RiveWidgetBuilder(
fileLoader: fileLoader,
builder: (context, state) => switch (state) {
RiveLoading() => const CircularProgressIndicator(),
RiveFailed() => Text('Failed to load: ${state.error}'),
RiveLoaded() => RiveWidget(
controller: state.controller,
fit: Fit.cover,
),
},
),
),
);
}
}
新 API - 选项 2
class SimpleAssetAnimation extends StatefulWidget {
const SimpleAssetAnimation({Key? key}) : super(key: key);
@override
State<SimpleAssetAnimation> createState() => _SimpleAssetAnimationState();
}
class _SimpleAssetAnimationState extends State<SimpleAssetAnimation> {
File? file;
RiveWidgetController? controller;
bool isInitialized = false;
@override
void initState() {
super.initState();
initRive();
}
void initRive() async {
file = (await File.asset('assets/off_road_car.riv', riveFactory: Factory.rive))!;
controller = RiveWidgetController(file!);
setState(() => isInitialized = true);
}
@override
void dispose() {
controller?.dispose();
file?.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Simple Animation'),
),
body: Center(
child: isInitialized && controller != null
? RiveWidget(
controller: controller!,
fit: Fit.cover,
)
: const CircularProgressIndicator(),
),
);
}
}
旧 API
class SimpleAssetAnimation extends StatelessWidget {
const SimpleAssetAnimation({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Simple Animation'),
),
body: const Center(
child: RiveAnimation.asset(
'assets/off_road_car.riv',
fit: BoxFit.cover,
),
),
);
}
}
控制器迁移
| 旧控制器 | 新控制器 | 备注 |
|---|---|---|
RiveAnimationController | RiveWidgetController | 组件的主控制器 |
StateMachineController | StateMachine | 直接访问状态机 |
OneShotAnimation 和 SimpleAnimation | SingleAnimationPainter | 用于单个动画 |
使用新的 RiveWidgetController 的示例:
final file = await File.asset('assets/off_road_car.riv', riveFactory: Factory.rive);
final controller = RiveWidgetController(file!);
final artboard = controller.artboard; // 访问已加载的画板
final viewModelInstance = controller.dataBind(DataBind.auto()); // 自动数据绑定
可选择指定要使用的画板和状态机:
final file = await File.asset('assets/off_road_car.riv', riveFactory: Factory.rive);
final controller = RiveWidgetController(
file,
artboardSelector: ArtboardSelector.byName('Main'),
stateMachineSelector: StateMachineSelector.byName('State Machine 1'),
);
播放动画
⚠️ 警告:此功能已弃用。我们强烈建议通过状态机来播放和混合动画。
在之前的版本中,你可以通过向 RiveAnimation 传递 animations: ['myAnimation'] 来直接播放动画。
要在新版本中实现相同的效果,请使用 SingleAnimationPainter 和 RiveArtboardWidget 代替 RiveWidgetController 和 RiveWidget。
import 'package:flutter/material.dart';
import 'package:rive/rive.dart';
import 'package:rive_example/main.dart' show RiveExampleApp;
/// 这是一个替代控制器(绘制器),用于代替 [RiveWidgetController]。
///
/// 此绘制器用于绘制/推进状态机。功能上与 [RiveWidgetController] 非常相似,
/// 我们推荐大多数用例使用 [RiveWidgetController]。
class ExampleSingleAnimationPainter extends StatefulWidget {
const ExampleSingleAnimationPainter({super.key});
@override
State<ExampleSingleAnimationPainter> createState() =>
_ExampleSingleAnimationPainterState();
}
class _ExampleSingleAnimationPainterState
extends State<ExampleSingleAnimationPainter> {
late File file;
Artboard? artboard;
late SingleAnimationPainter painter;
@override
void initState() {
super.initState();
init();
}
void init() async {
file = (await File.asset(
'assets/off_road_car.riv',
riveFactory: RiveExampleApp.getCurrentFactory,
))!;
painter = SingleAnimationPainter('idle');
artboard = file.defaultArtboard();
setState(() {});
}
@override
void dispose() {
painter.dispose();
artboard?.dispose();
file.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
if (artboard == null) {
return const Center(child: CircularProgressIndicator());
}
return RiveArtboardWidget(
artboard: artboard!,
painter: painter,
);
}
}
要播放和混合多个动画,你需要创建自己的绘制器。参见
SingleAnimationPainter的实现并扩展它来创建和推进多个动画。
处理状态机输入
对于更高级的用例,建议考虑使用数据绑定
StateMachineController 已移除,替换为 StateMachine。重要变更:
状态机输入:新 API
stateMachine.trigger('myTrigger');
stateMachine.boolean('myBool');
stateMachine.number('myNumber');
状态机输入:旧 API
controller.getTriggerInput('myTrigger');
controller.getBooleanInput('myBool');
controller.getNumberInput('myNumber');
你可以从 RiveWidgetController 访问 stateMachine:
final controller = RiveWidgetController(file);
final stateMachine = controller.stateMachine;
建议在不再需要时手动释放输入:
input.dispose()
嵌套输入
你可以通过提供可选的 path 参数访问嵌套输入:
stateMachine.boolean('myBool', path: 'nested/path');
stateMachine.number('myNumber', path: 'nested/path');
stateMachine.trigger('myTrigger', path: 'nested/path');
处理 Rive 事件
对于更高级的用例,建议考虑使用数据绑定来代替事件。
RiveEvent 已移除,替换为 Event。Event 是一个密封类,有两个选项:
OpenUrlEventGeneralEvent
注册事件监听器:
Rive 事件:新 API
// 新 API
final controller = RiveWidgetController(_riveFile!);
controller?.stateMachine.addEventListener(_onRiveEvent);
void _onRiveEvent(Event event) {
// 对事件执行某些操作
}
Rive 事件:旧 API
// 旧 API
final controller =
StateMachineController.fromArtboard(artboard, 'State Machine 1')!;
controller.addEventListener(_onRiveEvent);
void _onRiveEvent(RiveEvent event) {
// 对事件执行某些操作
}
访问 properties 返回 Map<String, CustomProperty>。CustomProperty 也是一个密封类,有以下选项:
CustomNumberPropertyCustomBooleanPropertyCustomStringProperty
所有这些都具有 value 字段。在 Event 类上,有便捷的访问器:
// 便捷访问器
event.property(name); // 返回 CustomProperty
event.numberProperty(name); // 返回 CustomNumberProperty
event.booleanProperty(name); // 返回 CustomBooleanProperty
event.stringProperty(name); // 返回 CustomStringProperty
布局变更
BoxFit → Fit
之前我们使用 Flutter 的 BoxFit 类。现在我们使用自己的 Fit,它包含一个额外的选项:
// 旧 API
BoxFit.contain
// 新 API
Fit.contain
Fit.layout // 用于基于布局的适配的新选项
资源加载变更
FileAssetLoader 类及其所有子类已移除:
CDNAssetLoaderLocalAssetLoaderCallbackAssetLoaderFallbackAssetLoader
带外资源加载
新 API
资源类型:FontAsset、ImageAsset 和 AudioAsset。
参见以下演示加载随机字体的示例:
// 新 API
final fontFile = await File.asset(
'assets/acqua_text_out_of_band.riv',
riveFactory: Factory.rive,
assetLoader: (asset, bytes) {
// 替换未内嵌在 rive 文件中的字体资源
if (asset is FontAsset && bytes == null) {
final urls = [
'https://cdn.rive.app/runtime/flutter/IndieFlower-Regular.ttf',
'https://cdn.rive.app/runtime/flutter/comic-neue.ttf',
'https://cdn.rive.app/runtime/flutter/inter.ttf',
'https://cdn.rive.app/runtime/flutter/inter-tight.ttf',
'https://cdn.rive.app/runtime/flutter/josefin-sans.ttf',
'https://cdn.rive.app/runtime/flutter/send-flowers.ttf',
];
// 从字体 URL 列表中随机选择一个
http.get(Uri.parse(urls[Random().nextInt(urls.length)])).then((res) {
if (mounted) {
asset.decode(
Uint8List.view(res.bodyBytes.buffer),
);
setState(() {
// 强制重建,以防 Rive 图形不再推进
});
}
});
return true; // 告诉运行时不要自动加载该资源
} else {
return false; // 告诉运行时继续尝试加载该资源(如果存在)
}
},
);
你也可以手动创建资源类型并设置它们。这在你想预加载资源时非常有用:
Future<void?> updateImageAsset(ImageAsset asset, Uint8List bytes) async {
final renderImage = await Factory.rive.decodeImage(bytes);
if (renderImage != null) {
asset.renderImage(renderImage);
}
}
Future<void?> updateFontAsset(FontAsset asset, Uint8List bytes) async {
final font = await Factory.rive.decodeFont(bytes);
if (font != null) {
asset.font(font);
}
}
Future<void?> updateAudioAsset(AudioAsset asset, Uint8List bytes) async {
final audioSource = await Factory.rive.decodeAudio(bytes);
if (audioSource != null) {
asset.audio(audioSource);
}
}
旧 API
// 旧 API
assetLoader: (asset, bytes) async {
/* 异步操作 */
final someImage = await ImageAsset.parseBytes(bytes)
asset.image = someImage;
}
关键变更:
assetLoader不能再是异步 lambdaImageAsset.parseBytes(bytes)→riveFactory.decodeImage(bytes)或asset.decode(bytes)FontAsset.parseBytes(bytes)→riveFactory.decodeFont(bytes)或asset.decode(bytes)AudioAsset.parseBytes(bytes)→riveFactory.decodeAudio(bytes)或asset.decode(bytes)ImageAsset.image = value→ImageAsset.renderImage(value)(返回 boolean)FontAsset.font = value→FontAsset.font(value)(返回 boolean)AudioAsset.audio = value→AudioAsset.audio(value)(返回 boolean)
文本运行更新
我们建议使用数据绑定来代替在运行时更新文本。
不再可以直接访问 TextValueRun 对象。改用以下方法访问字符串值:
final controller = RiveWidgetController(riveFile);
final artboard = controller.artboard;
// 获取文本运行值
artboard.getText('textRunName')
artboard.getText('textRunName', path: 'nested/path')
// 设置文本运行值
artboard.setText('textRunName', 'new value')
artboard.setText('textRunName', 'new value', path: 'nested/path')
已知的缺失功能
以下功能在 v0.14.0 中不可用,但可能会在未来的版本中添加:
- 自动 Rive CDN 资源加载
speedMultiplieruseArtboardSizeclipRectisTouchScrollEnableddynamicLibraryHelper
已移除的代码路径
以下路径中的所有"运行时"Dart 代码已被移除:
src/controllerssrc/coresrc/generatedrive_coreutilities
获取帮助
如果在迁移过程中遇到问题:
- 查阅 Rive Flutter 文档
- 查阅数据绑定指南
- 访问 Rive 社区论坛
- 在 GitHub 仓库上报告问题